diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a07efde6..edd13b74 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -80,3 +80,22 @@ jobs: - name: Run tests run: | bash ./bin/docker-compose-it.sh + + example-tests: + name: Examples | Unit tests + needs: does-it-run + runs-on: ${{ matrix.platform }} + strategy: + fail-fast: false + matrix: + platform: + - ubuntu-latest + - macos-latest + - windows-latest + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + steps: + - name: Checkout source at ${{ matrix.platform }} + uses: actions/checkout@v4 + - name: Run tests | ${{ matrix.python-version }} + run: | + make test-examples diff --git a/Makefile b/Makefile index 680820f5..1842848f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean test test-release release publish-test publish env +.PHONY: all clean style typing test test-release release publish-test publish env VIRTUALENV_EXISTS := $(shell [ -d .venv ] && echo 1 || echo 0) @@ -6,35 +6,38 @@ all: clean test @python -c "print('OK')" clean: - @python setup.py clean - @find src/ -name "*.so" | xargs rm -rf - @find . -name "*.pyc" | xargs rm -rf - @find . -name "__pycache__" | xargs rm -rf - @find . -name ".coverage" | xargs rm -rf - @rm -rf .coverage coverage.* .eggs/ .mypy_cache/ .pytype/ .ruff_cache/ .pytest_cache/ .tox/ src/Flask_JSONRPC.egg-info/ htmlcov/ junit/ htmldoc/ build/ dist/ wheelhouse/ + @find {src,examples,tests} -regex ".*\.\(so\|pyc\|__pycache__\|\.coverage\|\.tox\|\.pytest_cache\|\.ruff_cache\)" | xargs rm -rf + @find {src,examples,tests} -name "__pycache__" -o -name ".coverage" -o -name ".tox" -o -name ".pytest_cache" -o -name ".ruff_cache" -o -name ".pkg" -o -name ".tmp" | xargs rm -rf + @rm -rf .coverage coverage.* .eggs/ .mypy_cache/ .pytype/ .ruff_cache/ .pytest_cache/ .tox/ src/*.egg-info/ htmlcov/ junit/ htmldoc/ build/ dist/ wheelhouse/ style: @ruff check . @ruff format . +typing: + @mypy --install-types --non-interactive src/ + test: clean @python -m pip install --upgrade tox - @python -m tox + @python -m tox -p all + +test-examples: clean + @find examples/ -name "tox.ini" -print0 | xargs -0 -t -I % -P 1 tox -p all -c % -test-release: clean test - @docker-compose -f docker-compose.test.yml build --build-arg VERSION=$(shell date +%s) - @docker-compose -f docker-compose.test.yml up +test-release: test + $(shell ./bin/docker-compose-test.sh) + $(shell ./bin/docker-compose-it.sh) -release: clean test +release: test @python -m pip install --upgrade -r requirements/cbuild.txt @python -m build @MYPYC_ENABLE=1 python setup.py bdist_wheel -publish-test: clean release +publish-test: release @python -m pip install --upgrade twine @python -m twine upload --repository testpypi dist/* -publish: clean release +publish: release @python -m pip install --upgrade twine @python -m twine upload dist/* diff --git a/examples/async/README.rst b/examples/async/README.rst deleted file mode 100644 index fa99531e..00000000 --- a/examples/async/README.rst +++ /dev/null @@ -1,104 +0,0 @@ -async -===== - -A async minimal application with web browsable API. - - -Testing your service -******************** - -1. Running - -:: - - $ python async_minimal.py - * Running on http://0.0.0.0:5000/ - - -2. Testing - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.index", - "params": {}, - "id": "1" - }' http://localhost:5000/api - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 78 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 12:40:08 GMT - - { - "id": "1", - "jsonrpc": "2.0", - "result": "Welcome to Flask JSON-RPC" - } - - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.hello", - "params": ["Flask"], - "id": "1" - }' http://localhost:5000/api - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 64 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 12:41:08 GMT - - { - "id": "1", - "jsonrpc": "2.0", - "result": "Hello Flask" - } - - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.notify" - }' http://localhost:5000/api - HTTP/1.0 204 NO CONTENT - Content-Type: application/json - Content-Length: 0 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 12:41:49 GMT - - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.fails", - "params": ["Flask"], - "id": "1" - }' http://localhost:5000/api - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 704 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 12:42:40 GMT - - { - "error": { - "code": 500, - "data": null, - "executable": "/usr/bin/python2", - "message": "OtherError: ", - "name": "OtherError", - "stack": "Traceback (most recent call last):\n File \"/home/nycholas/project/src/o_lalertom/flask/flask-jsonrpc/examples/../flask_jsonrpc/site.py\", line 208, in response_dict\n R = apply_version[version](method, D['params'])\n File \"/home/nycholas/project/src/o_lalertom/flask/flask-jsonrpc/examples/../flask_jsonrpc/site.py\", line 168, in \n '2.0': lambda f, p: f(**encode_kw(p)) if type(p) is dict else f(*p),\n File \"minimal.py\", line 78, in fails\n raise ValueError\nValueError\n" - }, - "id": "1", - "jsonrpc": "2.0" - } diff --git a/examples/async/async_minimal.py b/examples/async/async_minimal.py deleted file mode 100755 index 183191b8..00000000 --- a/examples/async/async_minimal.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2021-2024, Cenobit Technologies, Inc. http://cenobit.es/ -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * Neither the name of the Cenobit Technologies nor the names of -# its contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -from typing import NoReturn, Optional -import asyncio - -from flask import Flask - -from flask_jsonrpc import JSONRPC - -app = Flask('wba') -jsonrpc = JSONRPC(app, '/api', enable_web_browsable_api=True) - - -@jsonrpc.method('App.index') -async def index() -> str: - await asyncio.sleep(0) - return 'Welcome to Flask JSON-RPC' - - -@jsonrpc.method('App.hello') -async def hello(name: str) -> str: - await asyncio.sleep(0) - return f'Hello {name}' - - -@jsonrpc.method('App.helloDefaultArgs') -async def hello_default_args(string: str = 'Flask JSON-RPC') -> str: - await asyncio.sleep(0) - return f'We salute you {string}' - - -@jsonrpc.method('App.helloDefaultArgsValidate') -async def hello_default_args_validate(string: str = 'Flask JSON-RPC') -> str: - await asyncio.sleep(0) - return f'We salute you {string}' - - -@jsonrpc.method('App.args') -async def args_validate_python_mode(a1: int, a2: str, a3: bool, a4: list, a5: dict) -> str: - await asyncio.sleep(0) - return f'int: {a1}, str: {a2}, bool: {a3}, list: {a4}, dict: {a5}' - - -@jsonrpc.method('App.notify') -async def notify(_string: Optional[str]) -> None: - await asyncio.sleep(0) - - -@jsonrpc.method('App.fails') -async def fails(_string: Optional[str]) -> NoReturn: - await asyncio.sleep(0) - raise ValueError('example of fail') - - -if __name__ == '__main__': - app.run(host='0.0.0.0', debug=True) diff --git a/examples/auth/README.rst b/examples/auth/README.rst deleted file mode 100644 index f29b45c4..00000000 --- a/examples/auth/README.rst +++ /dev/null @@ -1,58 +0,0 @@ -auth -==== - -A basic method authenticator. - - -Testing your service -******************** - -1. Running - -:: - - $ python auth.py - * Running on http://0.0.0.0:5000/ - - -2. Testing - -:: - - $ curl -i -X POST -H "Content-Type: application/json" \ - -H 'X-Username: username' \ - -H 'X-Password: secret' \ - -d '{ - "jsonrpc": "2.0", - "method": "App.index", - "id": "1" - }' http://localhost:5000/api - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 78 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 12:49:37 GMT - - { - "id": "1", - "jsonrpc": "2.0", - "result": "Welcome to Flask JSON-RPC" - } - - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.index", - "params": {}, - "id": "1" - }' http://localhost:5000/api - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 502 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 12:50:14 GMT - - UnauthorizedError diff --git a/examples/cerberus_params_validator/README.md b/examples/cerberus_params_validator/README.md deleted file mode 100644 index 8614c428..00000000 --- a/examples/cerberus_params_validator/README.md +++ /dev/null @@ -1,224 +0,0 @@ -# cerberus params validator - -## Setup - -``` - $ python3 -m venv .venv - $ . .venv/bin/activate - $ pip install -r Flask cerberus -``` - -## Example 1 - -Simple example using Cerberus Validator inside the function explicitly. - -### Run - -``` - $ python example1.py -``` - -### Test - -``` -$ curl -i -X POST -H "Content-Type: application/json; indent=4" -d '{ - "jsonrpc": "2.0", - "method": "App.createUsers", - "params": {"user": {"name": "Foo", "age": 11}}, - "id": "1" - }' http://localhost:5000/api -HTTP/1.1 200 OK -Server: Werkzeug/2.2.2 Python/3.10.6 -Date: Thu, 15 Sep 2022 14:14:45 GMT -Content-Type: application/json -Content-Length: 75 -Connection: close - -{ - "id": "1", - "jsonrpc": "2.0", - "result": { - "created": true - } -} -``` - -``` -$ curl -i -X POST -H "Content-Type: application/json; indent=4" -d '{ - "jsonrpc": "2.0", - "method": "App.createUsers", - "params": {"user": {"name": "Foo", "age": 9}}, - "id": "1" - }' http://localhost:5000/api -HTTP/1.1 400 BAD REQUEST -Server: Werkzeug/2.2.2 Python/3.10.6 -Date: Thu, 15 Sep 2022 14:21:53 GMT -Content-Type: application/json -Content-Length: 1187 -Connection: close - -{ - "error": { - "code": -32602, - "data": { - "message": { - "user": [ - { - "age": [ - "min value is 10" - ] - } - ] - } - }, - "message": "Invalid params", - "name": "InvalidParamsError" - }, - "id": "1", - "jsonrpc": "2.0" -} -``` - - -## Example 2 - -Example using Cerberus Custom Validator to validate from the Python's Dataclasses type inside the function explicitly. - -### Run - -``` - $ python example2.py -``` - -### Test - -``` -$ curl -i -X POST -H "Content-Type: application/json; indent=4" -d '{ - "jsonrpc": "2.0", - "method": "App.createUsers", - "params": {"user": {"name": "Foo", "age": 11}}, - "id": "1" - }' http://localhost:5000/api -HTTP/1.1 200 OK -Server: Werkzeug/2.2.2 Python/3.10.6 -Date: Thu, 15 Sep 2022 14:14:45 GMT -Content-Type: application/json -Content-Length: 75 -Connection: close - -{ - "id": "1", - "jsonrpc": "2.0", - "result": { - "created": true - } -} -``` - -``` -$ curl -i -X POST -H "Content-Type: application/json; indent=4" -d '{ - "jsonrpc": "2.0", - "method": "App.createUsers", - "params": {"user": {"name": "Foo", "age": 9}}, - "id": "1" - }' http://localhost:5000/api -HTTP/1.1 400 BAD REQUEST -Server: Werkzeug/2.2.2 Python/3.10.6 -Date: Thu, 15 Sep 2022 14:21:53 GMT -Content-Type: application/json -Content-Length: 1187 -Connection: close - -{ - "error": { - "code": -32602, - "data": { - "message": { - "user": [ - { - "age": [ - "min value is 10" - ] - } - ] - } - }, - "message": "Invalid params", - "name": "InvalidParamsError" - }, - "id": "1", - "jsonrpc": "2.0" -} -``` - - -## Example 3 - -Example using Cerberus Custom Validator to validate by Python Decorator. - -### Run - -``` - $ python example3.py -``` - -### Test - -``` -$ curl -i -X POST -H "Content-Type: application/json; indent=4" -d '{ - "jsonrpc": "2.0", - "method": "App.createUsers", - "params": {"user": {"name": "Foo", "age": 11}}, - "id": "1" - }' http://localhost:5000/api -HTTP/1.1 200 OK -Server: Werkzeug/2.2.2 Python/3.10.6 -Date: Thu, 15 Sep 2022 14:14:45 GMT -Content-Type: application/json -Content-Length: 75 -Connection: close - -{ - "id": "1", - "jsonrpc": "2.0", - "result": { - "created": true - } -} -``` - -``` -$ curl -i -X POST -H "Content-Type: application/json; indent=4" -d '{ - "jsonrpc": "2.0", - "method": "App.createUsers", - "params": {"user": {"name": "Foo", "age": 9}}, - "id": "1" - }' http://localhost:5000/api -HTTP/1.1 400 BAD REQUEST -Server: Werkzeug/2.2.2 Python/3.10.6 -Date: Thu, 15 Sep 2022 14:21:53 GMT -Content-Type: application/json -Content-Length: 1187 -Connection: close - -{ - "error": { - "code": -32602, - "data": { - "message": { - "user": [ - { - "age": [ - "min value is 10" - ] - } - ] - } - }, - "message": "Invalid params", - "name": "InvalidParamsError" - }, - "id": "1", - "jsonrpc": "2.0" -} -``` diff --git a/examples/cerberus_params_validator/example1.py b/examples/cerberus_params_validator/example1.py deleted file mode 100755 index e115d5bc..00000000 --- a/examples/cerberus_params_validator/example1.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2022-2024, Cenobit Technologies, Inc. http://cenobit.es/ -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * Neither the name of the Cenobit Technologies nor the names of -# its contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -from typing import Any, Dict - -from flask import Flask - -from cerberus import Validator - -from flask_jsonrpc import JSONRPC -from flask_jsonrpc.exceptions import InvalidParamsError - -app = Flask('cerberus') -jsonrpc = JSONRPC(app, '/api', enable_web_browsable_api=True) - -v = Validator() -v.schema = { - 'user': { - 'type': 'dict', - 'schema': {'name': {'type': 'string', 'maxlength': 10}, 'age': {'type': 'integer', 'min': 10}}, - }, - 'vehicle': { - 'type': 'dict', - 'schema': {'make': {'type': 'string'}, 'model': {'type': 'string'}, 'year': {'type': 'integer', 'min': 1900}}, - }, -} - - -@jsonrpc.method('App.createUsers') -def create_users(user: Dict[str, Any]) -> Dict[str, Any]: - if not v.validate({'user': {**user}}): - raise InvalidParamsError(data={'message': v.errors}) - return {'created': True} - - -@jsonrpc.method('App.createVehicles') -def create_vehicles(vehicle: Dict[str, Any]) -> Dict[str, Any]: - if not v.validate({'vehicle': {**vehicle}}): - raise InvalidParamsError(data={'message': v.errors}) - return {'created': True} - - -if __name__ == '__main__': - app.run(host='0.0.0.0', debug=True) diff --git a/examples/cerberus_params_validator/example2.py b/examples/cerberus_params_validator/example2.py deleted file mode 100755 index 0728e3b5..00000000 --- a/examples/cerberus_params_validator/example2.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2022-2024, Cenobit Technologies, Inc. http://cenobit.es/ -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * Neither the name of the Cenobit Technologies nor the names of -# its contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -from typing import Any, Dict, Self, Optional -from dataclasses import dataclass - -from flask import Flask - -from cerberus import Validator - -from flask_jsonrpc import JSONRPC -from flask_jsonrpc.exceptions import InvalidParamsError - -app = Flask('cerberus') -jsonrpc = JSONRPC(app, '/api', enable_web_browsable_api=True) - - -@dataclass -class User: - name: str - age: int - - -class DataclassValidatorMixin: - default_schema = None - - def validate( - self: Self, - obj: Any, # noqa: ANN401 - schema: Optional[Dict[str, Any]] = None, - update: bool = False, - normalize: bool = True, - ) -> bool: - return super().validate( - obj.__dict__, schema=self.default_schema if schema is None else schema, update=update, normalize=normalize - ) - - -class UserValidator(DataclassValidatorMixin, Validator): - default_schema = {'name': {'type': 'string', 'maxlength': 10}, 'age': {'type': 'integer', 'min': 10}} - - -v = Validator() -v.schema = { - 'vehicle': { - 'type': 'dict', - 'schema': {'make': {'type': 'string'}, 'model': {'type': 'string'}, 'year': {'type': 'integer', 'min': 1900}}, - } -} - -user_validator = UserValidator() - - -@jsonrpc.method('App.createUsers') -def create_users(user: Dict[str, Any]) -> Dict[str, Any]: - user = User(**user) - if not user_validator.validate(user): - raise InvalidParamsError(data={'message': v.errors}) - return {'created': True} - - -@jsonrpc.method('App.createVehicles') -def create_vehicles(vehicle: Dict[str, Any]) -> Dict[str, Any]: - if not v.validate({'vehicle': {**vehicle}}): - raise InvalidParamsError(data={'message': v.errors}) - return {'created': True} - - -if __name__ == '__main__': - app.run(host='0.0.0.0', debug=True) diff --git a/examples/cerberus_params_validator/example3.py b/examples/cerberus_params_validator/example3.py deleted file mode 100755 index 564a2e4c..00000000 --- a/examples/cerberus_params_validator/example3.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2022-2024, Cenobit Technologies, Inc. http://cenobit.es/ -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * Neither the name of the Cenobit Technologies nor the names of -# its contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -from typing import Any, Dict, Self, Callable, Optional - -from flask import Flask - -from cerberus import Validator - -from flask_jsonrpc import JSONRPC -from flask_jsonrpc.exceptions import InvalidParamsError - -app = Flask('cerberus') -jsonrpc = JSONRPC(app, '/api', enable_web_browsable_api=True) - - -class SchemaValidatorMixin: - default_schema = None - - def validate( - self: Self, - obj: Any, # noqa: ANN401 - schema: Optional[Dict[str, Any]] = None, - update: bool = False, - normalize: bool = True, - ) -> bool: - return super().validate( - obj, schema=self.default_schema if schema is None else schema, update=update, normalize=normalize - ) - - -def cerberus_validator(validator: Validator) -> Callable[..., Any]: - def decorator(fn: Callable[..., Any]) -> Callable[..., Any]: - def wrapped(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401 - if not validator.validate(kwargs): - raise InvalidParamsError(data={'message': validator.errors}) - return fn(*args, **kwargs) - - return wrapped - - return decorator - - -class UserValidator(SchemaValidatorMixin, Validator): - default_schema = { - 'user': { - 'type': 'dict', - 'schema': {'name': {'type': 'string', 'maxlength': 10}, 'age': {'type': 'integer', 'min': 10}}, - } - } - - -v = Validator() -v.schema = { - 'vehicle': { - 'type': 'dict', - 'schema': {'make': {'type': 'string'}, 'model': {'type': 'string'}, 'year': {'type': 'integer', 'min': 1900}}, - } -} - - -@jsonrpc.method('App.createUsers') -@cerberus_validator(validator=UserValidator()) -def create_users(user: Dict[str, Any]) -> Dict[str, Any]: # pylint: disable=W0613 - return {'created': True} - - -@jsonrpc.method('App.createVehicles') -def create_vehicles(vehicle: Dict[str, Any]) -> Dict[str, Any]: - if not v.validate({'vehicle': {**vehicle}}): - raise InvalidParamsError(data={'message': v.errors}) - return {'created': True} - - -if __name__ == '__main__': - app.run(host='0.0.0.0', debug=True) diff --git a/examples/cerberus_params_validator/requirements.txt b/examples/cerberus_params_validator/requirements.txt deleted file mode 100644 index f6ac109f..00000000 --- a/examples/cerberus_params_validator/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -cerberus==1.3.4 # https://github.com/pyeve/cerberus diff --git a/examples/decorator/README.rst b/examples/decorator/README.rst deleted file mode 100644 index 45f23e9b..00000000 --- a/examples/decorator/README.rst +++ /dev/null @@ -1,99 +0,0 @@ -decorator -========= - -The basic method decorated dealing any extra parameter and do some thing. - - -Testing your service -******************** - -1. Running - -:: - - $ python decorator.py - * Running on http://0.0.0.0:5000/ - - -2. Testing - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.index", - "params": {}, - "id": "1", - "terminal_id": 1 - }' http://localhost:5000/api - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 67 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 12:31:50 GMT - - { - "id": "1", - "jsonrpc": "2.0", - "result": "Terminal ID: 1" - } - - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.index", - "params": {}, - "id": "1", - "terminal_id": 0 - }' http://localhost:5000/api - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 750 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 12:36:48 GMT - - { - "error": { - "code": -32000, - "data": { - "message": "Invalid terminal ID" - }, - "executable": "/home/nycholas/project/cenobit.es/src/flask-jsonrpc/.venv/bin/python", - "message": "Server error", - "name": "ServerError", - "stack": "Traceback (most recent call last):\n File \"/home/nycholas/project/cenobit.es/src/flask-jsonrpc/examples/../flask_jsonrpc/site.py\", line 88, in dispatch_request\n return self.dispatch(json_data)\n File \"/home/nycholas/project/cenobit.es/src/flask-jsonrpc/examples/../flask_jsonrpc/site.py\", line 138, in dispatch\n resp_view = view_func(**params)\n File \"/home/nycholas/project/cenobit.es/src/flask-jsonrpc/.venv/lib/python3.8/site-packages/typeguard/__init__.py\", line 840, in wrapper\n retval = func(*args, **kwargs)\n File \"/home/nycholas/project/cenobit.es/src/flask-jsonrpc/examples/decorator/decorator.py\", line 52, in wrapped\n raise ValueError('Invalid terminal ID')\nValueError: Invalid terminal ID\n" - }, - "id": "1", - "jsonrpc": "2.0" - } - - - -:: - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.decorators", - "params": {}, - "id": "1", - "terminal_id": 1 - }' http://localhost:5000/api - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 231 - X-JSONRPC-Tag: JSONRPC 2.0 - Server: Werkzeug/0.10.4 Python/3.4.3 - Date: Sun, 09 Aug 2015 17:00:16 GMT - - { - "id": "1", - "jsonrpc": "2.0", - "result": { - "headers": "Host: localhost:5000\r\nUser-Agent: curl/7.70.0\r\nAccept: */*\r\nContent-Type: application/json; indent=4\r\nContent-Length: 137\r\n\r\n", - "terminal_id": 1 - } - } diff --git a/examples/decorator/decorator.py b/examples/decorator/decorator.py deleted file mode 100755 index d616170d..00000000 --- a/examples/decorator/decorator.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2012-2024, Cenobit Technologies, Inc. http://cenobit.es/ -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * Neither the name of the Cenobit Technologies nor the names of -# its contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -from typing import Any, Callable - -from flask import Flask, request - -from flask_jsonrpc import JSONRPC - -app = Flask('decorator') -jsonrpc = JSONRPC(app, '/api') - - -def check_terminal_id(fn: Callable[..., Any]) -> Any: # noqa: ANN401 - def wrapped(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401 - terminal_id = int(request.get_json(silent=True).get('terminal_id', 0)) - if terminal_id <= 0: - raise ValueError('Invalid terminal ID') - rv = fn(*args, **kwargs) - return rv - - return wrapped - - -def jsonrpc_headers(fn: Callable[..., Any]) -> Any: # noqa: ANN401 - def wrapped(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401 - headers = {'X-JSONRPC-Tag': 'JSONRPC 2.0'} - rv = fn(*args, **kwargs) - return rv, 200, headers - - return wrapped - - -@jsonrpc.method('App.index') -@check_terminal_id -def index() -> str: - terminal_id = request.get_json(silent=True).get('terminal_id', 0) - return f'Terminal ID: {terminal_id}' - - -@jsonrpc.method('App.decorators') -@check_terminal_id -@jsonrpc_headers -def decorators() -> dict: - return {'terminal_id': request.get_json(silent=True).get('terminal_id', 0), 'headers': str(request.headers)} - - -if __name__ == '__main__': - app.run(host='0.0.0.0', debug=True) diff --git a/examples/gevent/README.md b/examples/gevent/README.md deleted file mode 100644 index 978f3151..00000000 --- a/examples/gevent/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# gevent - -### Setup - -``` - $ python3 -m venv .venv - $ . .venv/bin/activate - $ pip install -r Flask gevent -``` - -### Run - -``` - $ python run.py -``` - -### Test - -``` - $ curl -i -X POST \ - -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.index", - "params": {}, - "id": "1" - }' http://localhost:5000/api -HTTP/1.1 200 OK -Content-Type: application/json -Content-Length: 64 -Date: Wed, 17 Jun 2020 20:38:04 GMT - -{"id":"1","jsonrpc":"2.0","result":"Welcome to Flask JSON-RPC"} -``` diff --git a/examples/hrx/README.rst b/examples/hrx/README.rst deleted file mode 100644 index 830365e9..00000000 --- a/examples/hrx/README.rst +++ /dev/null @@ -1,38 +0,0 @@ -hrx -=== - -A minimal application with HRX (Ajax). - - -Testing your service -******************** - -1. Running - -:: - - $ python hrx.py - * Running on http://0.0.0.0:5000/ - - -2. Testing - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "Hello.index", - "id": "1" - }' http://localhost:5000/api/hello - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 74 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 13:03:23 GMT - - { - "id": "1", - "jsonrpc": "2.0", - "result": "Welcome to Hello API!" - } diff --git a/examples/javascript/LICENSE.txt b/examples/javascript/LICENSE.txt new file mode 100644 index 00000000..9d10aad0 --- /dev/null +++ b/examples/javascript/LICENSE.txt @@ -0,0 +1,26 @@ +Copyright (c) 2012-2024, Cenobit Technologies, Inc. http://cenobit.es/ +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the Cenobit Technologies, Inc. nor the names of + its contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/javascript/README.md b/examples/javascript/README.md new file mode 100644 index 00000000..55a58d8d --- /dev/null +++ b/examples/javascript/README.md @@ -0,0 +1,27 @@ +# javascript + +A minimal application with HRX (Ajax). + +## Install + +``` +$ python3 -m venv .venv +$ . .venv/bin/activate +$ pip install -e . +``` + +## Run + +``` +$ flask --app hrx run +``` + +Open http://127.0.0.1:5000 in a browser. + + +## Test + +``` +$ pip install -e '.[test]' +$ pytest +``` diff --git a/examples/javascript/pyproject.toml b/examples/javascript/pyproject.toml new file mode 100644 index 00000000..1f5007b3 --- /dev/null +++ b/examples/javascript/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "hrx" +version = "1.0.0" +description = "Demonstrates a minimal HRX Flask-JSONRPC application." +readme = {file = "README.md", content-type = "text/markdown"} +license = {file = "LICENSE.txt"} +authors = [{name = "Nycholas Oliveira", email = "nycholas@cenobit.es"}] +maintainers = [{name = "Cenobit Technologies Inc.", email = "hi@cenobit.es"}] +requires-python = ">=3.8" +dependencies = ["Flask-JSONRPC@git+https://github.com/cenobites/flask-jsonrpc"] + +[project.optional-dependencies] +async = ["Flask[async]>=3.0.0,<4.0"] +test = ["pytest"] + +[build-system] +requires = ["flit_core>=3.2,<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "hrx" + +[tool.pytest.ini_options] +pythonpath = "src/" +testpaths = ["src/hrx", "tests"] +filterwarnings = ["error"] diff --git a/examples/javascript/src/hrx/__init__.py b/examples/javascript/src/hrx/__init__.py new file mode 100644 index 00000000..6023f811 --- /dev/null +++ b/examples/javascript/src/hrx/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/javascript/src/hrx/__main__.py b/examples/javascript/src/hrx/__main__.py new file mode 100644 index 00000000..b2e038cf --- /dev/null +++ b/examples/javascript/src/hrx/__main__.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +from app import app + +app.run(host='0.0.0.0') diff --git a/examples/hrx/api/__init__.py b/examples/javascript/src/hrx/api/__init__.py similarity index 100% rename from examples/hrx/api/__init__.py rename to examples/javascript/src/hrx/api/__init__.py diff --git a/examples/hrx/api/hello.py b/examples/javascript/src/hrx/api/hello.py similarity index 100% rename from examples/hrx/api/hello.py rename to examples/javascript/src/hrx/api/hello.py diff --git a/examples/hrx/hrx.py b/examples/javascript/src/hrx/app.py old mode 100755 new mode 100644 similarity index 92% rename from examples/hrx/hrx.py rename to examples/javascript/src/hrx/app.py index ea807a51..51344186 --- a/examples/hrx/hrx.py +++ b/examples/javascript/src/hrx/app.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (c) 2012-2024, Cenobit Technologies, Inc. http://cenobit.es/ # All rights reserved. # @@ -27,10 +26,10 @@ # POSSIBILITY OF SUCH DAMAGE. from flask import Flask, render_template -from api.hello import hello # noqa: E402 pylint: disable=C0413,E0611 - from flask_jsonrpc import JSONRPC +from .api.hello import hello + app = Flask('hrx') app.config.from_object('hrx') jsonrpc = JSONRPC(app, '/api') @@ -40,7 +39,3 @@ @app.route('/') def index() -> str: return render_template('index.html') - - -if __name__ == '__main__': - app.run(host='0.0.0.0', debug=True) diff --git a/examples/hrx/static/js/api.js b/examples/javascript/src/hrx/static/js/api.js similarity index 100% rename from examples/hrx/static/js/api.js rename to examples/javascript/src/hrx/static/js/api.js diff --git a/examples/hrx/templates/index.html b/examples/javascript/src/hrx/templates/index.html similarity index 100% rename from examples/hrx/templates/index.html rename to examples/javascript/src/hrx/templates/index.html diff --git a/examples/javascript/tests/__init__.py b/examples/javascript/tests/__init__.py new file mode 100644 index 00000000..6023f811 --- /dev/null +++ b/examples/javascript/tests/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/javascript/tests/api/__init__.py b/examples/javascript/tests/api/__init__.py new file mode 100644 index 00000000..6023f811 --- /dev/null +++ b/examples/javascript/tests/api/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/javascript/tests/api/test_hello.py b/examples/javascript/tests/api/test_hello.py new file mode 100644 index 00000000..9b80ea14 --- /dev/null +++ b/examples/javascript/tests/api/test_hello.py @@ -0,0 +1,68 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +import typing as t + +if t.TYPE_CHECKING: + from flask.testing import FlaskClient + + +def test_index(client: 'FlaskClient') -> None: + rv = client.post('/api/hello', json={'id': 1, 'jsonrpc': '2.0', 'method': 'Hello.index'}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Welcome to Hello API!'} + assert rv.status_code == 200 + + +def test_say(client: 'FlaskClient') -> None: + rv = client.post('/api/hello', json={'id': 1, 'jsonrpc': '2.0', 'method': 'Hello.say', 'params': ['Eve']}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Hello Eve!'} + assert rv.status_code == 200 + + +def test_rpc_describe(client: 'FlaskClient') -> None: + rv = client.post('/api/hello', json={'id': 1, 'jsonrpc': '2.0', 'method': 'rpc.describe'}) + data = rv.get_json() + assert data['id'] == 1 + assert data['jsonrpc'] == '2.0' + assert data['result']['name'] == 'Flask-JSONRPC' + assert data['result']['version'] == '2.0' + assert data['result']['servers'] is not None + assert 'url' in data['result']['servers'][0] + assert data['result']['methods'] == { + 'Hello.index': { + 'options': {'notification': True, 'validate': True}, + 'params': [], + 'returns': {'type': 'String'}, + 'type': 'method', + }, + 'Hello.say': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'name', 'type': 'String'}], + 'returns': {'type': 'String'}, + 'type': 'method', + }, + 'rpc.describe': {'options': {}, 'params': [], 'returns': {'type': 'Object'}, 'type': 'method'}, + } diff --git a/examples/gevent/run.py b/examples/javascript/tests/conftest.py old mode 100755 new mode 100644 similarity index 76% rename from examples/gevent/run.py rename to examples/javascript/tests/conftest.py index 8e2f2c90..85accc29 --- a/examples/gevent/run.py +++ b/examples/javascript/tests/conftest.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python -# Copyright (c) 2020-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -25,21 +24,23 @@ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -from flask import Flask +import typing as t -from gevent.pywsgi import WSGIServer +import pytest +from hrx.app import app -from flask_jsonrpc import JSONRPC +if t.TYPE_CHECKING: + from flask import Flask + from flask.testing import FlaskClient -app = Flask('gevent') -jsonrpc = JSONRPC(app, '/api', enable_web_browsable_api=True) +@pytest.fixture(name='app') +def fixture_app() -> 't.Generator[Flask]': + app.testing = True + yield app + app.testing = False -@jsonrpc.method('App.index') -def index() -> str: - return 'Welcome to Flask JSON-RPC' - -if __name__ == '__main__': - http_server = WSGIServer(('', 5000), app) - http_server.serve_forever() +@pytest.fixture +def client(app: 'Flask') -> 'FlaskClient': + return app.test_client() diff --git a/examples/javascript/tests/test_app.py b/examples/javascript/tests/test_app.py new file mode 100644 index 00000000..afd2bdde --- /dev/null +++ b/examples/javascript/tests/test_app.py @@ -0,0 +1,57 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +import typing as t + +from flask import template_rendered + +if t.TYPE_CHECKING: + from flask import Flask + from flask.testing import FlaskClient + + +def test_index(app: 'Flask', client: 'FlaskClient') -> None: + def check(sender, template, context) -> None: # noqa: ANN001 + assert template.name == 'index.html' + + with template_rendered.connected_to(check, app): + rv = client.get('/') + assert 'Flask-JSONRPC' in rv.text + assert rv.status_code == 200 + + +def test_rpc_describe(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'rpc.describe'}) + data = rv.get_json() + assert data['id'] == 1 + assert data['jsonrpc'] == '2.0' + assert data['result']['name'] == 'Flask-JSONRPC' + assert data['result']['version'] == '2.0' + assert data['result']['servers'] is not None + assert 'url' in data['result']['servers'][0] + assert data['result']['methods'] == { + 'rpc.describe': {'options': {}, 'params': [], 'returns': {'type': 'Object'}, 'type': 'method'} + } diff --git a/examples/javascript/tox.ini b/examples/javascript/tox.ini new file mode 100644 index 00000000..ea6d3675 --- /dev/null +++ b/examples/javascript/tox.ini @@ -0,0 +1,17 @@ +[tox] +envlist = + py3{12,11,10,9,8} + py3{12,11,10,9,8}-async +skip_missing_interpreters = true + +[testenv] +package = wheel +wheel_build_env = .pkg +envtmpdir = {toxworkdir}/tmp/{envname} +constrain_package_deps = true +use_frozen_constraints = true +deps = + pytest + async: Flask[async]>=3.0.0,<4.0 +commands = + pytest -vv --tb=short --basetemp={envtmpdir} {posargs} diff --git a/examples/minimal-async/LICENSE.txt b/examples/minimal-async/LICENSE.txt new file mode 100644 index 00000000..9d10aad0 --- /dev/null +++ b/examples/minimal-async/LICENSE.txt @@ -0,0 +1,26 @@ +Copyright (c) 2012-2024, Cenobit Technologies, Inc. http://cenobit.es/ +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the Cenobit Technologies, Inc. nor the names of + its contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/minimal-async/README.md b/examples/minimal-async/README.md new file mode 100644 index 00000000..bf6131a1 --- /dev/null +++ b/examples/minimal-async/README.md @@ -0,0 +1,27 @@ +# minimal-async + +A minimal async application. + +## Install + +``` +$ python3 -m venv .venv +$ . .venv/bin/activate +$ pip install -e . +``` + +## Run + +``` +$ flask --app minimal_async run +``` + +Open http://127.0.0.1:5000 in a browser. + + +## Test + +``` +$ pip install -e '.[test]' +$ pytest +``` diff --git a/examples/minimal-async/pyproject.toml b/examples/minimal-async/pyproject.toml new file mode 100644 index 00000000..0f64aeeb --- /dev/null +++ b/examples/minimal-async/pyproject.toml @@ -0,0 +1,25 @@ +[project] +name = "minimal-async" +version = "1.0.0" +description = "Demonstrates a minimal async Flask-JSONRPC application." +readme = {file = "README.md", content-type = "text/markdown"} +license = {file = "LICENSE.txt"} +authors = [{name = "Nycholas Oliveira", email = "nycholas@cenobit.es"}] +maintainers = [{name = "Cenobit Technologies Inc.", email = "hi@cenobit.es"}] +requires-python = ">=3.8" +dependencies = ["Flask-JSONRPC@git+https://github.com/cenobites/flask-jsonrpc", "Flask[async]>=3.0.0,<4.0"] + +[project.optional-dependencies] +test = ["pytest"] + +[build-system] +requires = ["flit_core>=3.2,<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "minimal_async" + +[tool.pytest.ini_options] +pythonpath = "src/" +testpaths = ["src/minimal_async", "tests"] +filterwarnings = ["error"] diff --git a/examples/minimal-async/src/minimal_async/__init__.py b/examples/minimal-async/src/minimal_async/__init__.py new file mode 100644 index 00000000..6023f811 --- /dev/null +++ b/examples/minimal-async/src/minimal_async/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/minimal-async/src/minimal_async/__main__.py b/examples/minimal-async/src/minimal_async/__main__.py new file mode 100644 index 00000000..b2e038cf --- /dev/null +++ b/examples/minimal-async/src/minimal_async/__main__.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +from app import app + +app.run(host='0.0.0.0') diff --git a/examples/minimal-async/src/minimal_async/app.py b/examples/minimal-async/src/minimal_async/app.py new file mode 100644 index 00000000..bcb4e664 --- /dev/null +++ b/examples/minimal-async/src/minimal_async/app.py @@ -0,0 +1,155 @@ +# Copyright (c) 2021-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +import typing as t +import asyncio +from numbers import Real + +from flask import Flask, request + +from flask_jsonrpc import JSONRPC + +app = Flask('minimal-async') +jsonrpc = JSONRPC(app, '/api', enable_web_browsable_api=True) + + +def check_terminal_id(fn: t.Callable[..., t.Any]) -> t.Any: # noqa: ANN401 + async def wrapped() -> t.Any: # noqa: ANN401 + await asyncio.sleep(0) + terminal_id = int(request.get_json(silent=True).get('terminal_id', 0)) + if terminal_id <= 0: + raise ValueError('Invalid terminal ID') + rv = await fn() + return rv + + return wrapped + + +def jsonrpc_headers(fn: t.Callable[..., t.Any]) -> t.Any: # noqa: ANN401 + async def wrapped() -> t.Any: # noqa: ANN401 + await asyncio.sleep(0) + headers = {'X-JSONRPC-Tag': 'JSONRPC 2.0'} + rv = await fn() + return rv, 200, headers + + return wrapped + + +class MyException(Exception): + pass + + +@jsonrpc.errorhandler(MyException) +async def handle_my_exception(ex: MyException) -> t.Dict[str, t.Any]: + await asyncio.sleep(0) + return {'message': 'It is a custom exception', 'code': '0001'} + + +@jsonrpc.method('App.index') +async def index() -> str: + await asyncio.sleep(0) + return 'Welcome to Flask JSON-RPC' + + +@jsonrpc.method('App.greeting') +async def greeting(name: str) -> str: + await asyncio.sleep(0) + return f'Hello {name}' + + +@jsonrpc.method('App.helloDefaultArgs') +async def hello_default_args(string: str = 'Flask JSON-RPC') -> str: + await asyncio.sleep(0) + return f'We salute you {string}' + + +@jsonrpc.method('App.argsValidate') +async def args_validate(a1: int, a2: str, a3: bool, a4: t.List[t.Any], a5: t.Dict[t.Any, t.Any]) -> str: + await asyncio.sleep(0) + return f'Number: {a1}, String: {a2}, Boolean: {a3}, Array: {a4}, Object: {a5}' + + +@jsonrpc.method('App.notify') +async def notify(_string: t.Optional[str] = None) -> None: + await asyncio.sleep(0) + + +@jsonrpc.method('App.notNotify', notification=False) +async def not_notify(string: str) -> str: + await asyncio.sleep(0) + return f'Not allow notification: {string}' + + +@jsonrpc.method('App.fails') +async def fails(_string: t.Optional[str] = None) -> t.NoReturn: + await asyncio.sleep(0) + raise ValueError('example of fail') + + +@jsonrpc.method('App.failsWithCustomException') +async def fails_with_custom_exception(_string: t.Optional[str] = None) -> t.NoReturn: + await asyncio.sleep(0) + raise MyException('example of fail with custom exception that will be handled') + + +@jsonrpc.method('App.sum') +async def sum_(a: Real, b: Real) -> Real: + await asyncio.sleep(0) + return a + b + + +@jsonrpc.method('App.subtract') +async def subtract(a: t.Union[int, float], b: t.Union[int, float]) -> t.Union[int, float]: + await asyncio.sleep(0) + return a - b + + +@jsonrpc.method('App.multiply') +async def multiply(a: float, b: float) -> float: + await asyncio.sleep(0) + return a * b + + +@jsonrpc.method('App.divide') +async def divide(a: float, b: float) -> float: + await asyncio.sleep(0) + return a / float(b) + + +@jsonrpc.method('App.oneDecorator') +@check_terminal_id +async def one_decorator() -> str: + await asyncio.sleep(0) + terminal_id = request.get_json(silent=True).get('terminal_id', 0) + return f'Terminal ID: {terminal_id}' + + +@jsonrpc.method('App.multiDecorators') +@check_terminal_id +@jsonrpc_headers +async def multi_decorators() -> t.Dict[str, t.Any]: + await asyncio.sleep(0) + return {'terminal_id': request.get_json(silent=True).get('terminal_id', 0), 'headers': str(request.headers)} diff --git a/examples/minimal-async/tests/__init__.py b/examples/minimal-async/tests/__init__.py new file mode 100644 index 00000000..6023f811 --- /dev/null +++ b/examples/minimal-async/tests/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/multiplesite/multiplesite.py b/examples/minimal-async/tests/conftest.py old mode 100755 new mode 100644 similarity index 72% rename from examples/multiplesite/multiplesite.py rename to examples/minimal-async/tests/conftest.py index ddfd0732..0417cc39 --- a/examples/multiplesite/multiplesite.py +++ b/examples/minimal-async/tests/conftest.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python -# Copyright (c) 2012-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -25,24 +24,23 @@ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -from flask import Flask +import typing as t -from flask_jsonrpc import JSONRPC +import pytest +from minimal_async.app import app -app = Flask('multiplesite') -jsonrpc_v1 = JSONRPC(app, '/api/v1', enable_web_browsable_api=True) -jsonrpc_v2 = JSONRPC(app, '/api/v2', enable_web_browsable_api=True) +if t.TYPE_CHECKING: + from flask import Flask + from flask.testing import FlaskClient -@jsonrpc_v1.method('App.index') -def index_v1() -> str: - return 'Welcome to Flask JSON-RPC Version API 1' +@pytest.fixture(name='app') +def fixture_app() -> 't.Generator[Flask]': + app.testing = True + yield app + app.testing = False -@jsonrpc_v2.method('App.index') -def index_v2() -> str: - return 'Welcome to Flask JSON-RPC Version API 2' - - -if __name__ == '__main__': - app.run(host='0.0.0.0', debug=True) +@pytest.fixture +def client(app: 'Flask') -> 'FlaskClient': + return app.test_client() diff --git a/examples/minimal-async/tests/test_app.py b/examples/minimal-async/tests/test_app.py new file mode 100644 index 00000000..6d46f49a --- /dev/null +++ b/examples/minimal-async/tests/test_app.py @@ -0,0 +1,284 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +import typing as t + +from werkzeug.datastructures import Headers + +if t.TYPE_CHECKING: + from flask.testing import FlaskClient + + +def test_index(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.index'}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Welcome to Flask JSON-RPC'} + assert rv.status_code == 200 + + +def test_greeting(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.greeting', 'params': ['Tequila']}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Hello Tequila'} + assert rv.status_code == 200 + + +def test_hello_default_args(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.helloDefaultArgs'}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'We salute you Flask JSON-RPC'} + assert rv.status_code == 200 + + +def test_args_validate(client: 'FlaskClient') -> None: + rv = client.post( + '/api', + json={ + 'id': 1, + 'jsonrpc': '2.0', + 'method': 'App.argsValidate', + 'params': [1, 'a', False, [1, 'a', True], {'k1': 1, 'k2': 'v2'}], + }, + ) + assert rv.json == { + 'id': 1, + 'jsonrpc': '2.0', + 'result': "Number: 1, String: a, Boolean: False, Array: [1, 'a', True], Object: {'k1': 1, 'k2': 'v2'}", + } + assert rv.status_code == 200 + + +def test_notify(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'jsonrpc': '2.0', 'method': 'App.notify'}) + assert rv.text == '' + assert rv.status_code == 204 + + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.notify'}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': None} + assert rv.status_code == 200 + + +def test_not_notify(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'jsonrpc': '2.0', 'method': 'App.notNotify', 'params': ['method']}) + assert rv.json == { + 'id': None, + 'jsonrpc': '2.0', + 'error': { + 'code': -32600, + 'data': { + 'message': "The method 'App.notNotify' doesn't allow Notification Request " + "object (without an 'id' member)" + }, + 'message': 'Invalid Request', + 'name': 'InvalidRequestError', + }, + } + assert rv.status_code == 400 + + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.notNotify', 'params': ['method']}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Not allow notification: method'} + assert rv.status_code == 200 + + +def test_fails(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.fails'}) + assert rv.json == { + 'id': 1, + 'jsonrpc': '2.0', + 'error': { + 'code': -32000, + 'data': {'message': 'example of fail'}, + 'message': 'Server error', + 'name': 'ServerError', + }, + } + assert rv.status_code == 500 + + +def test_fails_with_custom_exception(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.failsWithCustomException'}) + assert rv.json == { + 'id': 1, + 'jsonrpc': '2.0', + 'error': { + 'code': -32000, + 'data': {'message': 'It is a custom exception', 'code': '0001'}, + 'message': 'Server error', + 'name': 'ServerError', + }, + } + assert rv.status_code == 500 + + +def test_sum(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.sum', 'params': [1, 1]}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 2} + assert rv.status_code == 200 + + +def test_subtract(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.subtract', 'params': [1, 2]}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': -1} + assert rv.status_code == 200 + + +def test_multiply(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.multiply', 'params': {'a': 2, 'b': 5}}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 10} + assert rv.status_code == 200 + + +def test_divide(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.divide', 'params': [1, 1]}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 1.0} + assert rv.status_code == 200 + + +def test_one_decorator(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.oneDecorator', 'terminal_id': 1}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Terminal ID: 1'} + assert rv.status_code == 200 + + +def test_multi_decorators(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.multiDecorators', 'terminal_id': 1}) + assert rv.json == { + 'id': 1, + 'jsonrpc': '2.0', + 'result': { + 'headers': 'User-Agent: Werkzeug/3.0.4\r\n' + 'Host: localhost\r\n' + 'Content-Type: application/json\r\n' + 'Content-Length: 78\r\n' + '\r\n', + 'terminal_id': 1, + }, + } + assert rv.headers == Headers( + [('Content-Type', 'application/json'), ('Content-Length', '174'), ('X-JSONRPC-Tag', 'JSONRPC 2.0')] + ) + assert rv.status_code == 200 + + +def test_rpc_describe(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'rpc.describe'}) + data = rv.get_json() + assert data['id'] == 1 + assert data['jsonrpc'] == '2.0' + assert data['result']['name'] == 'Flask-JSONRPC' + assert data['result']['version'] == '2.0' + assert data['result']['servers'] is not None + assert 'url' in data['result']['servers'][0] + assert data['result']['methods'] == { + 'App.argsValidate': { + 'options': {'notification': True, 'validate': True}, + 'params': [ + {'name': 'a1', 'type': 'Number'}, + {'name': 'a2', 'type': 'String'}, + {'name': 'a3', 'type': 'Boolean'}, + {'name': 'a4', 'type': 'Array'}, + {'name': 'a5', 'type': 'Object'}, + ], + 'returns': {'type': 'String'}, + 'type': 'method', + }, + 'App.divide': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'a', 'type': 'Number'}, {'name': 'b', 'type': 'Number'}], + 'returns': {'type': 'Number'}, + 'type': 'method', + }, + 'App.fails': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': '_string', 'type': 'String'}], + 'returns': {'type': 'Null'}, + 'type': 'method', + }, + 'App.failsWithCustomException': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': '_string', 'type': 'String'}], + 'returns': {'type': 'Null'}, + 'type': 'method', + }, + 'App.greeting': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'name', 'type': 'String'}], + 'returns': {'type': 'String'}, + 'type': 'method', + }, + 'App.helloDefaultArgs': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'string', 'type': 'String'}], + 'returns': {'type': 'String'}, + 'type': 'method', + }, + 'App.index': { + 'options': {'notification': True, 'validate': True}, + 'params': [], + 'returns': {'type': 'String'}, + 'type': 'method', + }, + 'App.multiDecorators': { + 'options': {'notification': True, 'validate': True}, + 'params': [], + 'returns': {'type': 'Object'}, + 'type': 'method', + }, + 'App.multiply': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'a', 'type': 'Number'}, {'name': 'b', 'type': 'Number'}], + 'returns': {'type': 'Number'}, + 'type': 'method', + }, + 'App.notNotify': { + 'options': {'notification': False, 'validate': True}, + 'params': [{'name': 'string', 'type': 'String'}], + 'returns': {'type': 'String'}, + 'type': 'method', + }, + 'App.notify': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': '_string', 'type': 'String'}], + 'returns': {'type': 'Null'}, + 'type': 'method', + }, + 'App.oneDecorator': { + 'options': {'notification': True, 'validate': True}, + 'params': [], + 'returns': {'type': 'Object'}, + 'type': 'method', + }, + 'App.subtract': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'a', 'type': 'Number'}, {'name': 'b', 'type': 'Number'}], + 'returns': {'type': 'Number'}, + 'type': 'method', + }, + 'App.sum': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'a', 'type': 'Number'}, {'name': 'b', 'type': 'Number'}], + 'returns': {'type': 'Number'}, + 'type': 'method', + }, + 'rpc.describe': {'options': {}, 'params': [], 'returns': {'type': 'Object'}, 'type': 'method'}, + } diff --git a/examples/minimal-async/tox.ini b/examples/minimal-async/tox.ini new file mode 100644 index 00000000..62abab3a --- /dev/null +++ b/examples/minimal-async/tox.ini @@ -0,0 +1,15 @@ +[tox] +envlist = + py3{12,11,10,9,8} +skip_missing_interpreters = true + +[testenv] +package = wheel +wheel_build_env = .pkg +envtmpdir = {toxworkdir}/tmp/{envname} +constrain_package_deps = true +use_frozen_constraints = true +deps = + pytest +commands = + pytest -vv --tb=short --basetemp={envtmpdir} {posargs} diff --git a/examples/minimal/LICENSE.txt b/examples/minimal/LICENSE.txt new file mode 100644 index 00000000..9d10aad0 --- /dev/null +++ b/examples/minimal/LICENSE.txt @@ -0,0 +1,26 @@ +Copyright (c) 2012-2024, Cenobit Technologies, Inc. http://cenobit.es/ +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the Cenobit Technologies, Inc. nor the names of + its contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/minimal/README.md b/examples/minimal/README.md new file mode 100644 index 00000000..dd31a7e0 --- /dev/null +++ b/examples/minimal/README.md @@ -0,0 +1,27 @@ +# minimal + +A minimal application. + +## Install + +``` +$ python3 -m venv .venv +$ . .venv/bin/activate +$ pip install -e . +``` + +## Run + +``` +$ flask --app minimal run +``` + +Open http://127.0.0.1:5000 in a browser. + + +## Test + +``` +$ pip install -e '.[test]' +$ pytest +``` diff --git a/examples/minimal/README.rst b/examples/minimal/README.rst deleted file mode 100644 index 42cb6e79..00000000 --- a/examples/minimal/README.rst +++ /dev/null @@ -1,104 +0,0 @@ -minimal -======= - -A minimal application. - - -Testing your service -******************** - -1. Running - -:: - - $ python minimal.py - * Running on http://0.0.0.0:5000/ - - -2. Testing - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.index", - "params": {}, - "id": "1" - }' http://localhost:5000/api - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 78 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 12:40:08 GMT - - { - "id": "1", - "jsonrpc": "2.0", - "result": "Welcome to Flask JSON-RPC" - } - - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.hello", - "params": ["Flask"], - "id": "1" - }' http://localhost:5000/api - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 64 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 12:41:08 GMT - - { - "id": "1", - "jsonrpc": "2.0", - "result": "Hello Flask" - } - - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.notify" - }' http://localhost:5000/api - HTTP/1.0 204 NO CONTENT - Content-Type: application/json - Content-Length: 0 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 12:41:49 GMT - - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.fails", - "params": ["Flask"], - "id": "1" - }' http://localhost:5000/api - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 704 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 12:42:40 GMT - - { - "error": { - "code": 500, - "data": null, - "executable": "/usr/bin/python2", - "message": "OtherError: ", - "name": "OtherError", - "stack": "Traceback (most recent call last):\n File \"/home/nycholas/project/src/o_lalertom/flask/flask-jsonrpc/examples/../flask_jsonrpc/site.py\", line 208, in response_dict\n R = apply_version[version](method, D['params'])\n File \"/home/nycholas/project/src/o_lalertom/flask/flask-jsonrpc/examples/../flask_jsonrpc/site.py\", line 168, in \n '2.0': lambda f, p: f(**encode_kw(p)) if type(p) is dict else f(*p),\n File \"minimal.py\", line 78, in fails\n raise ValueError\nValueError\n" - }, - "id": "1", - "jsonrpc": "2.0" - } diff --git a/examples/minimal/pyproject.toml b/examples/minimal/pyproject.toml new file mode 100644 index 00000000..061e3861 --- /dev/null +++ b/examples/minimal/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "minimal" +version = "1.0.0" +description = "Demonstrates a minimal Flask-JSONRPC application." +readme = {file = "README.md", content-type = "text/markdown"} +license = {file = "LICENSE.txt"} +authors = [{name = "Nycholas Oliveira", email = "nycholas@cenobit.es"}] +maintainers = [{name = "Cenobit Technologies Inc.", email = "hi@cenobit.es"}] +requires-python = ">=3.8" +dependencies = ["Flask-JSONRPC@git+https://github.com/cenobites/flask-jsonrpc"] + +[project.optional-dependencies] +async = ["Flask[async]>=3.0.0,<4.0"] +test = ["pytest"] + +[build-system] +requires = ["flit_core>=3.2,<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "minimal" + +[tool.pytest.ini_options] +pythonpath = "src/" +testpaths = ["src/minimal", "tests"] +filterwarnings = ["error"] diff --git a/examples/minimal/src/minimal/__init__.py b/examples/minimal/src/minimal/__init__.py new file mode 100644 index 00000000..6023f811 --- /dev/null +++ b/examples/minimal/src/minimal/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/minimal/src/minimal/__main__.py b/examples/minimal/src/minimal/__main__.py new file mode 100644 index 00000000..b2e038cf --- /dev/null +++ b/examples/minimal/src/minimal/__main__.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +from app import app + +app.run(host='0.0.0.0') diff --git a/examples/minimal/minimal.py b/examples/minimal/src/minimal/app.py old mode 100755 new mode 100644 similarity index 76% rename from examples/minimal/minimal.py rename to examples/minimal/src/minimal/app.py index 48d0d597..6c23fd31 --- a/examples/minimal/minimal.py +++ b/examples/minimal/src/minimal/app.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (c) 2012-2024, Cenobit Technologies, Inc. http://cenobit.es/ # All rights reserved. # @@ -28,7 +27,7 @@ import typing as t from numbers import Real -from flask import Flask +from flask import Flask, request from flask_jsonrpc import JSONRPC @@ -36,6 +35,26 @@ jsonrpc = JSONRPC(app, '/api', enable_web_browsable_api=True) +def check_terminal_id(fn: t.Callable[..., t.Any]) -> t.Any: # noqa: ANN401 + def wrapped() -> t.Any: # noqa: ANN401 + terminal_id = int(request.get_json(silent=True).get('terminal_id', 0)) + if terminal_id <= 0: + raise ValueError('Invalid terminal ID') + rv = fn() + return rv + + return wrapped + + +def jsonrpc_headers(fn: t.Callable[..., t.Any]) -> t.Any: # noqa: ANN401 + def wrapped() -> t.Any: # noqa: ANN401 + headers = {'X-JSONRPC-Tag': 'JSONRPC 2.0'} + rv = fn() + return rv, 200, headers + + return wrapped + + class MyException(Exception): pass @@ -101,9 +120,19 @@ def multiply(a: float, b: float) -> float: @jsonrpc.method('App.divide') -def divide(a: Real, b: Real) -> Real: +def divide(a: float, b: float) -> float: return a / float(b) -if __name__ == '__main__': - app.run(host='0.0.0.0', debug=True) +@jsonrpc.method('App.oneDecorator') +@check_terminal_id +def one_decorator() -> str: + terminal_id = request.get_json(silent=True).get('terminal_id', 0) + return f'Terminal ID: {terminal_id}' + + +@jsonrpc.method('App.multiDecorators') +@check_terminal_id +@jsonrpc_headers +def multi_decorators() -> t.Dict[str, t.Any]: + return {'terminal_id': request.get_json(silent=True).get('terminal_id', 0), 'headers': str(request.headers)} diff --git a/examples/minimal/tests/__init__.py b/examples/minimal/tests/__init__.py new file mode 100644 index 00000000..6023f811 --- /dev/null +++ b/examples/minimal/tests/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/minimal/tests/conftest.py b/examples/minimal/tests/conftest.py new file mode 100644 index 00000000..f29459ec --- /dev/null +++ b/examples/minimal/tests/conftest.py @@ -0,0 +1,46 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +import typing as t + +import pytest +from minimal.app import app + +if t.TYPE_CHECKING: + from flask import Flask + from flask.testing import FlaskClient + + +@pytest.fixture(name='app') +def fixture_app() -> 't.Generator[Flask]': + app.testing = True + yield app + app.testing = False + + +@pytest.fixture +def client(app: 'Flask') -> 'FlaskClient': + return app.test_client() diff --git a/examples/minimal/tests/test_app.py b/examples/minimal/tests/test_app.py new file mode 100644 index 00000000..6d46f49a --- /dev/null +++ b/examples/minimal/tests/test_app.py @@ -0,0 +1,284 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +import typing as t + +from werkzeug.datastructures import Headers + +if t.TYPE_CHECKING: + from flask.testing import FlaskClient + + +def test_index(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.index'}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Welcome to Flask JSON-RPC'} + assert rv.status_code == 200 + + +def test_greeting(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.greeting', 'params': ['Tequila']}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Hello Tequila'} + assert rv.status_code == 200 + + +def test_hello_default_args(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.helloDefaultArgs'}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'We salute you Flask JSON-RPC'} + assert rv.status_code == 200 + + +def test_args_validate(client: 'FlaskClient') -> None: + rv = client.post( + '/api', + json={ + 'id': 1, + 'jsonrpc': '2.0', + 'method': 'App.argsValidate', + 'params': [1, 'a', False, [1, 'a', True], {'k1': 1, 'k2': 'v2'}], + }, + ) + assert rv.json == { + 'id': 1, + 'jsonrpc': '2.0', + 'result': "Number: 1, String: a, Boolean: False, Array: [1, 'a', True], Object: {'k1': 1, 'k2': 'v2'}", + } + assert rv.status_code == 200 + + +def test_notify(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'jsonrpc': '2.0', 'method': 'App.notify'}) + assert rv.text == '' + assert rv.status_code == 204 + + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.notify'}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': None} + assert rv.status_code == 200 + + +def test_not_notify(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'jsonrpc': '2.0', 'method': 'App.notNotify', 'params': ['method']}) + assert rv.json == { + 'id': None, + 'jsonrpc': '2.0', + 'error': { + 'code': -32600, + 'data': { + 'message': "The method 'App.notNotify' doesn't allow Notification Request " + "object (without an 'id' member)" + }, + 'message': 'Invalid Request', + 'name': 'InvalidRequestError', + }, + } + assert rv.status_code == 400 + + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.notNotify', 'params': ['method']}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Not allow notification: method'} + assert rv.status_code == 200 + + +def test_fails(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.fails'}) + assert rv.json == { + 'id': 1, + 'jsonrpc': '2.0', + 'error': { + 'code': -32000, + 'data': {'message': 'example of fail'}, + 'message': 'Server error', + 'name': 'ServerError', + }, + } + assert rv.status_code == 500 + + +def test_fails_with_custom_exception(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.failsWithCustomException'}) + assert rv.json == { + 'id': 1, + 'jsonrpc': '2.0', + 'error': { + 'code': -32000, + 'data': {'message': 'It is a custom exception', 'code': '0001'}, + 'message': 'Server error', + 'name': 'ServerError', + }, + } + assert rv.status_code == 500 + + +def test_sum(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.sum', 'params': [1, 1]}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 2} + assert rv.status_code == 200 + + +def test_subtract(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.subtract', 'params': [1, 2]}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': -1} + assert rv.status_code == 200 + + +def test_multiply(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.multiply', 'params': {'a': 2, 'b': 5}}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 10} + assert rv.status_code == 200 + + +def test_divide(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.divide', 'params': [1, 1]}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 1.0} + assert rv.status_code == 200 + + +def test_one_decorator(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.oneDecorator', 'terminal_id': 1}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Terminal ID: 1'} + assert rv.status_code == 200 + + +def test_multi_decorators(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.multiDecorators', 'terminal_id': 1}) + assert rv.json == { + 'id': 1, + 'jsonrpc': '2.0', + 'result': { + 'headers': 'User-Agent: Werkzeug/3.0.4\r\n' + 'Host: localhost\r\n' + 'Content-Type: application/json\r\n' + 'Content-Length: 78\r\n' + '\r\n', + 'terminal_id': 1, + }, + } + assert rv.headers == Headers( + [('Content-Type', 'application/json'), ('Content-Length', '174'), ('X-JSONRPC-Tag', 'JSONRPC 2.0')] + ) + assert rv.status_code == 200 + + +def test_rpc_describe(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'rpc.describe'}) + data = rv.get_json() + assert data['id'] == 1 + assert data['jsonrpc'] == '2.0' + assert data['result']['name'] == 'Flask-JSONRPC' + assert data['result']['version'] == '2.0' + assert data['result']['servers'] is not None + assert 'url' in data['result']['servers'][0] + assert data['result']['methods'] == { + 'App.argsValidate': { + 'options': {'notification': True, 'validate': True}, + 'params': [ + {'name': 'a1', 'type': 'Number'}, + {'name': 'a2', 'type': 'String'}, + {'name': 'a3', 'type': 'Boolean'}, + {'name': 'a4', 'type': 'Array'}, + {'name': 'a5', 'type': 'Object'}, + ], + 'returns': {'type': 'String'}, + 'type': 'method', + }, + 'App.divide': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'a', 'type': 'Number'}, {'name': 'b', 'type': 'Number'}], + 'returns': {'type': 'Number'}, + 'type': 'method', + }, + 'App.fails': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': '_string', 'type': 'String'}], + 'returns': {'type': 'Null'}, + 'type': 'method', + }, + 'App.failsWithCustomException': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': '_string', 'type': 'String'}], + 'returns': {'type': 'Null'}, + 'type': 'method', + }, + 'App.greeting': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'name', 'type': 'String'}], + 'returns': {'type': 'String'}, + 'type': 'method', + }, + 'App.helloDefaultArgs': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'string', 'type': 'String'}], + 'returns': {'type': 'String'}, + 'type': 'method', + }, + 'App.index': { + 'options': {'notification': True, 'validate': True}, + 'params': [], + 'returns': {'type': 'String'}, + 'type': 'method', + }, + 'App.multiDecorators': { + 'options': {'notification': True, 'validate': True}, + 'params': [], + 'returns': {'type': 'Object'}, + 'type': 'method', + }, + 'App.multiply': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'a', 'type': 'Number'}, {'name': 'b', 'type': 'Number'}], + 'returns': {'type': 'Number'}, + 'type': 'method', + }, + 'App.notNotify': { + 'options': {'notification': False, 'validate': True}, + 'params': [{'name': 'string', 'type': 'String'}], + 'returns': {'type': 'String'}, + 'type': 'method', + }, + 'App.notify': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': '_string', 'type': 'String'}], + 'returns': {'type': 'Null'}, + 'type': 'method', + }, + 'App.oneDecorator': { + 'options': {'notification': True, 'validate': True}, + 'params': [], + 'returns': {'type': 'Object'}, + 'type': 'method', + }, + 'App.subtract': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'a', 'type': 'Number'}, {'name': 'b', 'type': 'Number'}], + 'returns': {'type': 'Number'}, + 'type': 'method', + }, + 'App.sum': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'a', 'type': 'Number'}, {'name': 'b', 'type': 'Number'}], + 'returns': {'type': 'Number'}, + 'type': 'method', + }, + 'rpc.describe': {'options': {}, 'params': [], 'returns': {'type': 'Object'}, 'type': 'method'}, + } diff --git a/examples/minimal/tox.ini b/examples/minimal/tox.ini new file mode 100644 index 00000000..ea6d3675 --- /dev/null +++ b/examples/minimal/tox.ini @@ -0,0 +1,17 @@ +[tox] +envlist = + py3{12,11,10,9,8} + py3{12,11,10,9,8}-async +skip_missing_interpreters = true + +[testenv] +package = wheel +wheel_build_env = .pkg +envtmpdir = {toxworkdir}/tmp/{envname} +constrain_package_deps = true +use_frozen_constraints = true +deps = + pytest + async: Flask[async]>=3.0.0,<4.0 +commands = + pytest -vv --tb=short --basetemp={envtmpdir} {posargs} diff --git a/examples/modular/LICENSE.txt b/examples/modular/LICENSE.txt new file mode 100644 index 00000000..9d10aad0 --- /dev/null +++ b/examples/modular/LICENSE.txt @@ -0,0 +1,26 @@ +Copyright (c) 2012-2024, Cenobit Technologies, Inc. http://cenobit.es/ +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the Cenobit Technologies, Inc. nor the names of + its contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/modular/README.md b/examples/modular/README.md new file mode 100644 index 00000000..d74925e6 --- /dev/null +++ b/examples/modular/README.md @@ -0,0 +1,27 @@ +# modular + +A modular application with BluePrints. + +## Install + +``` +$ python3 -m venv .venv +$ . .venv/bin/activate +$ pip install -e . +``` + +## Run + +``` +$ flask --app modular run +``` + +Open http://127.0.0.1:5000 in a browser. + + +## Test + +``` +$ pip install -e '.[test]' +$ pytest +``` diff --git a/examples/modular/README.rst b/examples/modular/README.rst deleted file mode 100644 index ee5cc94b..00000000 --- a/examples/modular/README.rst +++ /dev/null @@ -1,59 +0,0 @@ -modular -======= - -A modular application with BluePrints. - - -Testing your service -******************** - -1. Running - -:: - - $ python modular.py - * Running on http://0.0.0.0:5000/ - - -2. Testing - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "Article.index", - "id": "1" - }' http://localhost:5000/api/article - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 75 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 13:14:00 GMT - - { - "id": "1", - "jsonrpc": "2.0", - "result": "Welcome to Article API" - } - - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "User.index", - "id": "1" - }' http://localhost:5000/api/user - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 72 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 13:14:17 GMT - - { - "id": "1", - "jsonrpc": "2.0", - "result": "Welcome to User API" - } diff --git a/examples/modular/pyproject.toml b/examples/modular/pyproject.toml new file mode 100644 index 00000000..bfbf2f0a --- /dev/null +++ b/examples/modular/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "modular" +version = "1.0.0" +description = "Demonstrates a minimal modular Flask-JSONRPC application." +readme = {file = "README.md", content-type = "text/markdown"} +license = {file = "LICENSE.txt"} +authors = [{name = "Nycholas Oliveira", email = "nycholas@cenobit.es"}] +maintainers = [{name = "Cenobit Technologies Inc.", email = "hi@cenobit.es"}] +requires-python = ">=3.8" +dependencies = ["Flask-JSONRPC@git+https://github.com/cenobites/flask-jsonrpc"] + +[project.optional-dependencies] +async = ["Flask[async]>=3.0.0,<4.0"] +test = ["pytest"] + +[build-system] +requires = ["flit_core>=3.2,<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "modular" + +[tool.pytest.ini_options] +pythonpath = "src/" +testpaths = ["src/modular", "tests"] +filterwarnings = ["error"] diff --git a/examples/modular/src/modular/__init__.py b/examples/modular/src/modular/__init__.py new file mode 100644 index 00000000..6023f811 --- /dev/null +++ b/examples/modular/src/modular/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/modular/src/modular/__main__.py b/examples/modular/src/modular/__main__.py new file mode 100644 index 00000000..b2e038cf --- /dev/null +++ b/examples/modular/src/modular/__main__.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +from app import app + +app.run(host='0.0.0.0') diff --git a/examples/modular/api/__init__.py b/examples/modular/src/modular/api/__init__.py similarity index 100% rename from examples/modular/api/__init__.py rename to examples/modular/src/modular/api/__init__.py diff --git a/examples/modular/api/article.py b/examples/modular/src/modular/api/article.py similarity index 100% rename from examples/modular/api/article.py rename to examples/modular/src/modular/api/article.py diff --git a/examples/modular/api/user.py b/examples/modular/src/modular/api/user.py similarity index 100% rename from examples/modular/api/user.py rename to examples/modular/src/modular/api/user.py diff --git a/examples/modular/modular.py b/examples/modular/src/modular/app.py old mode 100755 new mode 100644 similarity index 90% rename from examples/modular/modular.py rename to examples/modular/src/modular/app.py index 75b2e0b9..3eb7c060 --- a/examples/modular/modular.py +++ b/examples/modular/src/modular/app.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (c) 2012-2024, Cenobit Technologies, Inc. http://cenobit.es/ # All rights reserved. # @@ -29,11 +28,11 @@ from flask import Flask -from api.user import user # noqa: E402 pylint: disable=C0413,E0611 -from api.article import article # noqa: E402 pylint: disable=C0413,E0611 - from flask_jsonrpc import JSONRPC +from .api.user import user +from .api.article import article + app = Flask('modular') jsonrpc = JSONRPC(app, '/api', enable_web_browsable_api=True) jsonrpc.register_blueprint(app, user, url_prefix='/user', enable_web_browsable_api=True) @@ -49,7 +48,3 @@ def handle_value_error_exception(ex: ValueError) -> t.Dict[str, t.Any]: @jsonrpc.method('App.index') def index() -> str: return 'Welcome to Flask JSON-RPC' - - -if __name__ == '__main__': - app.run(host='0.0.0.0', debug=True) diff --git a/examples/modular/tests/__init__.py b/examples/modular/tests/__init__.py new file mode 100644 index 00000000..6023f811 --- /dev/null +++ b/examples/modular/tests/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/modular/tests/api/__init__.py b/examples/modular/tests/api/__init__.py new file mode 100644 index 00000000..6023f811 --- /dev/null +++ b/examples/modular/tests/api/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/modular/tests/api/test_article.py b/examples/modular/tests/api/test_article.py new file mode 100644 index 00000000..cef04499 --- /dev/null +++ b/examples/modular/tests/api/test_article.py @@ -0,0 +1,81 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +import typing as t + +if t.TYPE_CHECKING: + from flask.testing import FlaskClient + + +def test_index(client: 'FlaskClient') -> None: + rv = client.post('/api/article', json={'id': 1, 'jsonrpc': '2.0', 'method': 'Article.index'}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Welcome to Article API'} + assert rv.status_code == 200 + + +def test_get_user(client: 'FlaskClient') -> None: + rv = client.post('/api/article', json={'id': 1, 'jsonrpc': '2.0', 'method': 'Article.getArticle', 'params': [1]}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': {'id': 1, 'name': 'Founded'}} + assert rv.status_code == 200 + + rv = client.post('/api/article', json={'id': 1, 'jsonrpc': '2.0', 'method': 'Article.getArticle', 'params': [11]}) + assert rv.json == { + 'id': 1, + 'jsonrpc': '2.0', + 'error': { + 'code': -32000, + 'data': {'code': '2001', 'message': 'Article 11 not found'}, + 'message': 'Server error', + 'name': 'ServerError', + }, + } + assert rv.status_code == 500 + + +def test_rpc_describe(client: 'FlaskClient') -> None: + rv = client.post('/api/article', json={'id': 1, 'jsonrpc': '2.0', 'method': 'rpc.describe'}) + data = rv.get_json() + assert data['id'] == 1 + assert data['jsonrpc'] == '2.0' + assert data['result']['name'] == 'Flask-JSONRPC' + assert data['result']['version'] == '2.0' + assert data['result']['servers'] is not None + assert 'url' in data['result']['servers'][0] + assert data['result']['methods'] == { + 'Article.getArticle': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'id', 'type': 'Number'}], + 'returns': {'type': 'Object'}, + 'type': 'method', + }, + 'Article.index': { + 'options': {'notification': True, 'validate': True}, + 'params': [], + 'returns': {'type': 'String'}, + 'type': 'method', + }, + 'rpc.describe': {'options': {}, 'params': [], 'returns': {'type': 'Object'}, 'type': 'method'}, + } diff --git a/examples/modular/tests/api/test_user.py b/examples/modular/tests/api/test_user.py new file mode 100644 index 00000000..455215be --- /dev/null +++ b/examples/modular/tests/api/test_user.py @@ -0,0 +1,106 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +import typing as t + +if t.TYPE_CHECKING: + from flask.testing import FlaskClient + + +def test_index(client: 'FlaskClient') -> None: + rv = client.post('/api/user', json={'id': 1, 'jsonrpc': '2.0', 'method': 'User.index'}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Welcome to User API'} + assert rv.status_code == 200 + + +def test_get_user(client: 'FlaskClient') -> None: + rv = client.post('/api/user', json={'id': 1, 'jsonrpc': '2.0', 'method': 'User.getUser', 'params': [1]}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': {'id': 1, 'name': 'Founded'}} + assert rv.status_code == 200 + + rv = client.post('/api/user', json={'id': 1, 'jsonrpc': '2.0', 'method': 'User.getUser', 'params': [11]}) + assert rv.json == { + 'id': 1, + 'jsonrpc': '2.0', + 'error': { + 'code': -32000, + 'data': {'code': '1001', 'message': 'User 11 not found'}, + 'message': 'Server error', + 'name': 'ServerError', + }, + } + assert rv.status_code == 500 + + +def test_remove_user(client: 'FlaskClient') -> None: + rv = client.post('/api/user', json={'id': 1, 'jsonrpc': '2.0', 'method': 'User.removeUser', 'params': [1]}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': {'id': 1, 'name': 'Removed'}} + assert rv.status_code == 200 + + rv = client.post('/api/user', json={'id': 1, 'jsonrpc': '2.0', 'method': 'User.removeUser', 'params': [11]}) + assert rv.json == { + 'id': 1, + 'jsonrpc': '2.0', + 'error': { + 'code': -32000, + 'data': {'message': 'User not found'}, + 'message': 'Server error', + 'name': 'ServerError', + }, + } + assert rv.status_code == 500 + + +def test_rpc_describe(client: 'FlaskClient') -> None: + rv = client.post('/api/user', json={'id': 1, 'jsonrpc': '2.0', 'method': 'rpc.describe'}) + data = rv.get_json() + assert data['id'] == 1 + assert data['jsonrpc'] == '2.0' + assert data['result']['name'] == 'Flask-JSONRPC' + assert data['result']['version'] == '2.0' + assert data['result']['servers'] is not None + assert 'url' in data['result']['servers'][0] + assert data['result']['methods'] == { + 'User.getUser': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'id', 'type': 'Number'}], + 'returns': {'type': 'Object'}, + 'type': 'method', + }, + 'User.index': { + 'options': {'notification': True, 'validate': True}, + 'params': [], + 'returns': {'type': 'String'}, + 'type': 'method', + }, + 'User.removeUser': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'id', 'type': 'Number'}], + 'returns': {'type': 'Object'}, + 'type': 'method', + }, + 'rpc.describe': {'options': {}, 'params': [], 'returns': {'type': 'Object'}, 'type': 'method'}, + } diff --git a/examples/modular/tests/conftest.py b/examples/modular/tests/conftest.py new file mode 100644 index 00000000..ba979ff3 --- /dev/null +++ b/examples/modular/tests/conftest.py @@ -0,0 +1,46 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +import typing as t + +import pytest +from modular.app import app + +if t.TYPE_CHECKING: + from flask import Flask + from flask.testing import FlaskClient + + +@pytest.fixture(name='app') +def fixture_app() -> 't.Generator[Flask]': + app.testing = True + yield app + app.testing = False + + +@pytest.fixture +def client(app: 'Flask') -> 'FlaskClient': + return app.test_client() diff --git a/examples/modular/tests/test_app.py b/examples/modular/tests/test_app.py new file mode 100644 index 00000000..deb5159b --- /dev/null +++ b/examples/modular/tests/test_app.py @@ -0,0 +1,56 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +import typing as t + +if t.TYPE_CHECKING: + from flask.testing import FlaskClient + + +def test_index(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.index'}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Welcome to Flask JSON-RPC'} + assert rv.status_code == 200 + + +def test_rpc_describe(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'rpc.describe'}) + data = rv.get_json() + assert data['id'] == 1 + assert data['jsonrpc'] == '2.0' + assert data['result']['name'] == 'Flask-JSONRPC' + assert data['result']['version'] == '2.0' + assert data['result']['servers'] is not None + assert 'url' in data['result']['servers'][0] + assert data['result']['methods'] == { + 'App.index': { + 'options': {'notification': True, 'validate': True}, + 'params': [], + 'returns': {'type': 'String'}, + 'type': 'method', + }, + 'rpc.describe': {'options': {}, 'params': [], 'returns': {'type': 'Object'}, 'type': 'method'}, + } diff --git a/examples/modular/tox.ini b/examples/modular/tox.ini new file mode 100644 index 00000000..ea6d3675 --- /dev/null +++ b/examples/modular/tox.ini @@ -0,0 +1,17 @@ +[tox] +envlist = + py3{12,11,10,9,8} + py3{12,11,10,9,8}-async +skip_missing_interpreters = true + +[testenv] +package = wheel +wheel_build_env = .pkg +envtmpdir = {toxworkdir}/tmp/{envname} +constrain_package_deps = true +use_frozen_constraints = true +deps = + pytest + async: Flask[async]>=3.0.0,<4.0 +commands = + pytest -vv --tb=short --basetemp={envtmpdir} {posargs} diff --git a/examples/multiplesite/LICENSE.txt b/examples/multiplesite/LICENSE.txt new file mode 100644 index 00000000..9d10aad0 --- /dev/null +++ b/examples/multiplesite/LICENSE.txt @@ -0,0 +1,26 @@ +Copyright (c) 2012-2024, Cenobit Technologies, Inc. http://cenobit.es/ +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the Cenobit Technologies, Inc. nor the names of + its contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/multiplesite/README.md b/examples/multiplesite/README.md new file mode 100644 index 00000000..10b892a7 --- /dev/null +++ b/examples/multiplesite/README.md @@ -0,0 +1,27 @@ +# multiplesite + +Multiples sites in same application. + +## Install + +``` +$ python3 -m venv .venv +$ . .venv/bin/activate +$ pip install -e . +``` + +## Run + +``` +$ flask --app multiplesite run +``` + +Open http://127.0.0.1:5000 in a browser. + + +## Test + +``` +$ pip install -e '.[test]' +$ pytest +``` diff --git a/examples/multiplesite/README.rst b/examples/multiplesite/README.rst deleted file mode 100644 index 809d6c88..00000000 --- a/examples/multiplesite/README.rst +++ /dev/null @@ -1,59 +0,0 @@ -multiplesite -============ - -Multiples sites in same application. - - -Testing your service -******************** - -1. Running - -:: - - $ python multiplesite.py - * Running on http://0.0.0.0:5000/ - - -2. Testing - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.index", - "id": "1" - }' http://localhost:5000/api/v1 - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 92 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 13:15:44 GMT - - { - "id": "1", - "jsonrpc": "2.0", - "result": "Welcome to Flask JSON-RPC Version API 1" - } - - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.index", - "id": "1" - }' http://localhost:5000/api/v2 - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 92 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 13:15:59 GMT - - { - "id": "1", - "jsonrpc": "2.0", - "result": "Welcome to Flask JSON-RPC Version API 2" - } diff --git a/examples/multiplesite/pyproject.toml b/examples/multiplesite/pyproject.toml new file mode 100644 index 00000000..48a215d5 --- /dev/null +++ b/examples/multiplesite/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "multiplesite" +version = "1.0.0" +description = "Demonstrates a minimal multiplesite Flask-JSONRPC application." +readme = {file = "README.md", content-type = "text/markdown"} +license = {file = "LICENSE.txt"} +authors = [{name = "Nycholas Oliveira", email = "nycholas@cenobit.es"}] +maintainers = [{name = "Cenobit Technologies Inc.", email = "hi@cenobit.es"}] +requires-python = ">=3.8" +dependencies = ["Flask-JSONRPC@git+https://github.com/cenobites/flask-jsonrpc"] + +[project.optional-dependencies] +async = ["Flask[async]>=3.0.0,<4.0"] +test = ["pytest"] + +[build-system] +requires = ["flit_core>=3.2,<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "multiplesite" + +[tool.pytest.ini_options] +pythonpath = "src/" +testpaths = ["src/multiplesite", "tests"] +filterwarnings = ["error"] diff --git a/examples/multiplesite/src/multiplesite/__init__.py b/examples/multiplesite/src/multiplesite/__init__.py new file mode 100644 index 00000000..6023f811 --- /dev/null +++ b/examples/multiplesite/src/multiplesite/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/multiplesite/src/multiplesite/__main__.py b/examples/multiplesite/src/multiplesite/__main__.py new file mode 100644 index 00000000..b2e038cf --- /dev/null +++ b/examples/multiplesite/src/multiplesite/__main__.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +from app import app + +app.run(host='0.0.0.0') diff --git a/examples/auth/auth.py b/examples/multiplesite/src/multiplesite/app.py old mode 100755 new mode 100644 similarity index 84% rename from examples/auth/auth.py rename to examples/multiplesite/src/multiplesite/app.py index 7437756f..1793c9c6 --- a/examples/auth/auth.py +++ b/examples/multiplesite/src/multiplesite/app.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (c) 2012-2024, Cenobit Technologies, Inc. http://cenobit.es/ # All rights reserved. # @@ -25,16 +24,15 @@ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. - import typing as t from flask import Flask, request +from flask_jsonrpc import JSONRPC, JSONRPCView + if t.TYPE_CHECKING: from flask.typing import ResponseReturnValue -from flask_jsonrpc import JSONRPC, JSONRPCView - class UnauthorizedError(Exception): pass @@ -52,19 +50,16 @@ def dispatch_request(self: t.Self) -> 'ResponseReturnValue': return super().dispatch_request() -app = Flask('auth') -jsonrpc = JSONRPC(app, '/api', jsonrpc_site_api=AuthorizationView) - - -@jsonrpc.method('App.index') -def index() -> str: - return 'Welcome to Flask JSON-RPC' +app = Flask('multiplesite') +jsonrpc_v1 = JSONRPC(app, '/api/v1', enable_web_browsable_api=True) +jsonrpc_v2 = JSONRPC(app, '/api/v2', enable_web_browsable_api=True, jsonrpc_site_api=AuthorizationView) -@jsonrpc.method('App.echo') -def echo(name: str = 'Flask JSON-RPC') -> str: - return f'Hello {name}' +@jsonrpc_v1.method('App.index') +def index_v1() -> str: + return 'Welcome to Flask JSON-RPC Version API 1' -if __name__ == '__main__': - app.run(host='0.0.0.0', debug=True) +@jsonrpc_v2.method('App.index') +def index_v2() -> str: + return 'Welcome to Flask JSON-RPC Version API 2' diff --git a/examples/multiplesite/tests/__init__.py b/examples/multiplesite/tests/__init__.py new file mode 100644 index 00000000..6023f811 --- /dev/null +++ b/examples/multiplesite/tests/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/multiplesite/tests/conftest.py b/examples/multiplesite/tests/conftest.py new file mode 100644 index 00000000..24bf5d9c --- /dev/null +++ b/examples/multiplesite/tests/conftest.py @@ -0,0 +1,46 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +import typing as t + +import pytest +from multiplesite.app import app + +if t.TYPE_CHECKING: + from flask import Flask + from flask.testing import FlaskClient + + +@pytest.fixture(name='app') +def fixture_app() -> 't.Generator[Flask]': + app.testing = True + yield app + app.testing = False + + +@pytest.fixture +def client(app: 'Flask') -> 'FlaskClient': + return app.test_client() diff --git a/examples/multiplesite/tests/test_app.py b/examples/multiplesite/tests/test_app.py new file mode 100644 index 00000000..77465c8e --- /dev/null +++ b/examples/multiplesite/tests/test_app.py @@ -0,0 +1,90 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +import typing as t + +if t.TYPE_CHECKING: + from flask.testing import FlaskClient + + +def test_index_v1(client: 'FlaskClient') -> None: + rv = client.post('/api/v1', json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.index'}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Welcome to Flask JSON-RPC Version API 1'} + assert rv.status_code == 200 + + +def test_index_v2(client: 'FlaskClient') -> None: + rv = client.post( + '/api/v2', + json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.index'}, + headers={'X-Username': 'username', 'X-Password': 'secret'}, + ) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': 'Welcome to Flask JSON-RPC Version API 2'} + assert rv.status_code == 200 + + +def test_rpc_describe_v1(client: 'FlaskClient') -> None: + rv = client.post('/api/v1', json={'id': 1, 'jsonrpc': '2.0', 'method': 'rpc.describe'}) + data = rv.get_json() + assert data['id'] == 1 + assert data['jsonrpc'] == '2.0' + assert data['result']['name'] == 'Flask-JSONRPC' + assert data['result']['version'] == '2.0' + assert data['result']['servers'] is not None + assert 'url' in data['result']['servers'][0] + assert data['result']['methods'] == { + 'App.index': { + 'options': {'notification': True, 'validate': True}, + 'params': [], + 'returns': {'type': 'String'}, + 'type': 'method', + }, + 'rpc.describe': {'options': {}, 'params': [], 'returns': {'type': 'Object'}, 'type': 'method'}, + } + + +def test_rpc_describe_v2(client: 'FlaskClient') -> None: + rv = client.post( + '/api/v2', + json={'id': 1, 'jsonrpc': '2.0', 'method': 'rpc.describe'}, + headers={'X-Username': 'username', 'X-Password': 'secret'}, + ) + data = rv.get_json() + assert data['id'] == 1 + assert data['jsonrpc'] == '2.0' + assert data['result']['name'] == 'Flask-JSONRPC' + assert data['result']['version'] == '2.0' + assert data['result']['servers'] is not None + assert 'url' in data['result']['servers'][0] + assert data['result']['methods'] == { + 'App.index': { + 'options': {'notification': True, 'validate': True}, + 'params': [], + 'returns': {'type': 'String'}, + 'type': 'method', + }, + 'rpc.describe': {'options': {}, 'params': [], 'returns': {'type': 'Object'}, 'type': 'method'}, + } diff --git a/examples/multiplesite/tox.ini b/examples/multiplesite/tox.ini new file mode 100644 index 00000000..ea6d3675 --- /dev/null +++ b/examples/multiplesite/tox.ini @@ -0,0 +1,17 @@ +[tox] +envlist = + py3{12,11,10,9,8} + py3{12,11,10,9,8}-async +skip_missing_interpreters = true + +[testenv] +package = wheel +wheel_build_env = .pkg +envtmpdir = {toxworkdir}/tmp/{envname} +constrain_package_deps = true +use_frozen_constraints = true +deps = + pytest + async: Flask[async]>=3.0.0,<4.0 +commands = + pytest -vv --tb=short --basetemp={envtmpdir} {posargs} diff --git a/examples/openrpc/LICENSE.txt b/examples/openrpc/LICENSE.txt new file mode 100644 index 00000000..9d10aad0 --- /dev/null +++ b/examples/openrpc/LICENSE.txt @@ -0,0 +1,26 @@ +Copyright (c) 2012-2024, Cenobit Technologies, Inc. http://cenobit.es/ +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the Cenobit Technologies, Inc. nor the names of + its contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/openrpc/README.md b/examples/openrpc/README.md new file mode 100644 index 00000000..ee56fdbd --- /dev/null +++ b/examples/openrpc/README.md @@ -0,0 +1,27 @@ +# openrpc + +A petstore application with OpenRPC. + +## Install + +``` +$ python3 -m venv .venv +$ . .venv/bin/activate +$ pip install -e . +``` + +## Run + +``` +$ flask --app petstore run +``` + +Open http://127.0.0.1:5000 in a browser. + + +## Test + +``` +$ pip install -e '.[test]' +$ pytest +``` diff --git a/examples/openrpc/README.rst b/examples/openrpc/README.rst deleted file mode 100644 index 6924814c..00000000 --- a/examples/openrpc/README.rst +++ /dev/null @@ -1,115 +0,0 @@ -petstore -======= - -A petstore application. - - -Testing your service -******************** - -1. Running - -:: - - $ python petstore.py - * Running on http://0.0.0.0:5000/ - -2. Testing - -:: - $ curl 'http://localhost:5000/api' -X POST -H "Content-Type: application/json; indent=4" \ - --data-raw '{ - "jsonrpc": "2.0", - "method": "Petstore.create_pet", - "params": { - "name": "Jhon", - "tag": "cat" - }, - "id": "1c7fb3b2-7a87-4cf7-8e28-aafc33dae71d" - }' - { - "id": "1c7fb3b2-7a87-4cf7-8e28-aafc33dae71d", - "jsonrpc": "2.0", - "result": { - "id": 32, - "name": "Jhon", - "tag": "cat" - } - } - - - -:: - $ curl 'http://localhost:5000/api' -X POST -H "Content-Type: application/json; indent=4" \ - --data-raw '{ - "jsonrpc": "2.0", - "method": "Petstore.get_pets", - "params": {}, - "id": "16ebeed1-748c-4983-ba19-2848692c873a" - }' - { - "id": "16ebeed1-748c-4983-ba19-2848692c873a", - "jsonrpc": "2.0", - "result": [ - { - "id": 1, - "name": "Bob", - "tag": "dog" - }, - { - "id": 2, - "name": "Eve", - "tag": "cat" - }, - { - "id": 3, - "name": "Alice", - "tag": "bird" - }, - { - "id": 32, - "name": "Jhon", - "tag": "cat" - } - ] - } - - - -:: - $ curl 'http://localhost:5000/api' -X POST -H "Content-Type: application/json; indent=4" \ - --data-raw '{ - "jsonrpc": "2.0", - "method": "Petstore.get_pet_by_id", - "params": { - "id": 32 - }, - "id": "5dfbd1c0-6919-4ce2-a05e-0b4a4aa2aeb2" - }' - { - "id": "5dfbd1c0-6919-4ce2-a05e-0b4a4aa2aeb2", - "jsonrpc": "2.0", - "result": { - "id": 32, - "name": "Jhon", - "tag": "cat" - } - } - - - -:: - $ curl 'http://localhost:5000/api' -X POST -H 'Content-Type: application/json;charset=utf-8' \ - --data-raw '{ - "jsonrpc": "2.0", - "method": "Petstore.delete_pet_by_id", - "params": { - "id": 32 - }, - "id": "706cf9c3-5b5d-4288-8555-a67c8b5de481" - }' - { - "id": "706cf9c3-5b5d-4288-8555-a67c8b5de481", - "jsonrpc": "2.0", - "result": null - } diff --git a/examples/openrpc/petstore-expanded-openrpc.json b/examples/openrpc/petstore-expanded-openrpc.json deleted file mode 100644 index 491b6e6a..00000000 --- a/examples/openrpc/petstore-expanded-openrpc.json +++ /dev/null @@ -1,178 +0,0 @@ -{ - "components": { - "schemas": { - "NewPet": { - "properties": { - "name": { - "type": "string" - }, - "tag": { - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "Pet": { - "allOf": [ - { - "$ref": "#/components/schemas/NewPet" - }, - { - "properties": { - "id": { - "type": "integer" - } - }, - "required": [ - "id" - ] - } - ] - } - } - }, - "externalDocs": { - "url": "https://github.com/open-rpc/examples/blob/master/service-descriptions/petstore-expanded-openrpc.json" - }, - "info": { - "contact": { - "email": "doesntexist@open-rpc.org", - "name": "OpenRPC Team", - "url": "https://open-rpc.org" - }, - "description": "A sample API that uses a petstore as an example to demonstrate features in the OpenRPC specification", - "license": { - "name": "Apache 2.0", - "url": "https://www.apache.org/licenses/LICENSE-2.0.html" - }, - "termsOfService": "https://open-rpc.org", - "title": "Petstore Expanded", - "version": "1.0.0" - }, - "methods": [ - { - "name": "rpc.describe", - "params": [], - "result": { - "name": "default", - "schema": { - "type": "object" - } - } - }, - { - "description": "Returns an OpenRPC schema as a description of this service", - "name": "rpc.discover", - "params": [], - "result": { - "name": "OpenRPC Schema", - "schema": { - "$ref": "https://raw.githubusercontent.com/open-rpc/meta-schema/master/schema.json" - } - } - }, - { - "description": "Returns all pets from the system that the user has access to\nNam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam.", - "name": "Petstore.get_pets", - "params": [ - { - "description": "tags to filter by", - "name": "tags", - "schema": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - { - "description": "maximum number of results to return", - "name": "limit", - "schema": { - "type": "integer" - } - } - ], - "result": { - "description": "pet response", - "name": "pet", - "schema": { - "items": { - "$ref": "#/components/schemas/Pet" - }, - "type": "array" - } - } - }, - { - "description": "Creates a new pet in the store. Duplicates are allowed", - "name": "Petstore.create_pet", - "params": [ - { - "description": "Pet to add to the store.", - "name": "newPet", - "schema": { - "$ref": "#/components/schemas/NewPet" - } - } - ], - "result": { - "description": "the newly created pet", - "name": "pet", - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - { - "description": "Returns a user based on a single ID, if the user does not have access to the pet", - "name": "Petstore.get_pet_by_id", - "params": [ - { - "description": "ID of pet to fetch", - "name": "id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "result": { - "description": "pet response", - "name": "pet", - "schema": { - "$ref": "#/components/schemas/Pet" - } - } - }, - { - "description": "deletes a single pet based on the ID supplied", - "name": "Petstore.delete_pet_by_id", - "params": [ - { - "description": "ID of pet to delete", - "name": "id", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "result": { - "description": "pet deleted", - "name": "pet", - "schema": {} - } - } - ], - "openrpc": "1.0.0-rc1", - "servers": [ - { - "name": "default", - "url": "http://petstore.open-rpc.org" - } - ] -} diff --git a/examples/openrpc/pyproject.toml b/examples/openrpc/pyproject.toml new file mode 100644 index 00000000..e00f4a1f --- /dev/null +++ b/examples/openrpc/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "openrpc" +version = "1.0.0" +description = "Demonstrates a minimal OpenRPC Flask-JSONRPC application." +readme = {file = "README.md", content-type = "text/markdown"} +license = {file = "LICENSE.txt"} +authors = [{name = "Nycholas Oliveira", email = "nycholas@cenobit.es"}] +maintainers = [{name = "Cenobit Technologies Inc.", email = "hi@cenobit.es"}] +requires-python = ">=3.8" +dependencies = ["Flask-JSONRPC@git+https://github.com/cenobites/flask-jsonrpc"] + +[project.optional-dependencies] +async = ["Flask[async]>=3.0.0,<4.0"] +test = ["pytest"] + +[build-system] +requires = ["flit_core>=3.2,<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "petstore" + +[tool.pytest.ini_options] +pythonpath = "src/" +testpaths = ["src/petstore", "tests"] +filterwarnings = ["error"] diff --git a/examples/openrpc/src/petstore/__init__.py b/examples/openrpc/src/petstore/__init__.py new file mode 100644 index 00000000..6023f811 --- /dev/null +++ b/examples/openrpc/src/petstore/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/openrpc/src/petstore/__main__.py b/examples/openrpc/src/petstore/__main__.py new file mode 100644 index 00000000..b2e038cf --- /dev/null +++ b/examples/openrpc/src/petstore/__main__.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +from app import app + +app.run(host='0.0.0.0') diff --git a/examples/openrpc/petstore.py b/examples/openrpc/src/petstore/app.py old mode 100755 new mode 100644 similarity index 98% rename from examples/openrpc/petstore.py rename to examples/openrpc/src/petstore/app.py index ecbfd95e..d3f57536 --- a/examples/openrpc/petstore.py +++ b/examples/openrpc/src/petstore/app.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ # All rights reserved. # @@ -185,7 +184,3 @@ def get_pet_by_id(id: int) -> Pet: def delete_pet_by_id(id: int) -> None: global PETS PETS = [pet for pet in PETS if pet.id != id] # noqa: F823, F841 - - -if __name__ == '__main__': - app.run(host='0.0.0.0', debug=True) diff --git a/examples/openrpc/tests/__init__.py b/examples/openrpc/tests/__init__.py new file mode 100644 index 00000000..6023f811 --- /dev/null +++ b/examples/openrpc/tests/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/openrpc/tests/conftest.py b/examples/openrpc/tests/conftest.py new file mode 100644 index 00000000..ff713ad5 --- /dev/null +++ b/examples/openrpc/tests/conftest.py @@ -0,0 +1,46 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +import typing as t + +import pytest +from petstore.app import app + +if t.TYPE_CHECKING: + from flask import Flask + from flask.testing import FlaskClient + + +@pytest.fixture(name='app') +def fixture_app() -> 't.Generator[Flask]': + app.testing = True + yield app + app.testing = False + + +@pytest.fixture +def client(app: 'Flask') -> 'FlaskClient': + return app.test_client() diff --git a/examples/openrpc/tests/test_app.py b/examples/openrpc/tests/test_app.py new file mode 100644 index 00000000..e4091e2f --- /dev/null +++ b/examples/openrpc/tests/test_app.py @@ -0,0 +1,224 @@ +# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Cenobit Technologies nor the names of +# its contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +import typing as t + +if t.TYPE_CHECKING: + from flask.testing import FlaskClient + + +def test_get_pets(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'Petstore.get_pets', 'params': []}) + json_data = rv.get_json() + assert json_data['id'] == 1 + assert json_data['jsonrpc'] == '2.0' + assert len(json_data['result']) > 1 + assert json_data['result'][0] == {'id': 1, 'name': 'Bob', 'tag': 'dog'} + assert rv.status_code == 200 + + +def test_create_pet(client: 'FlaskClient') -> None: + rv = client.post( + '/api', + json={'id': 1, 'jsonrpc': '2.0', 'method': 'Petstore.create_pet', 'params': {'name': 'Tequila', 'tag': 'cat'}}, + ) + json_data = rv.get_json() + assert json_data['id'] == 1 + assert json_data['jsonrpc'] == '2.0' + assert json_data['result']['id'] is not None + assert json_data['result']['name'] == 'Tequila' + assert json_data['result']['tag'] == 'cat' + assert rv.status_code == 200 + + +def test_get_by_id(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'Petstore.get_pet_by_id', 'params': [1]}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': {'id': 1, 'name': 'Bob', 'tag': 'dog'}} + assert rv.status_code == 200 + + +def test_delete_by_id(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'Petstore.delete_pet_by_id', 'params': [2]}) + assert rv.json == {'id': 1, 'jsonrpc': '2.0', 'result': None} + assert rv.status_code == 200 + + +def test_rpc_discover(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'rpc.discover'}) + data = rv.get_json() + assert data['id'] == 1 + assert data['jsonrpc'] == '2.0' + assert data['result'] == { + 'components': { + 'schemas': { + 'NewPet': { + 'properties': {'name': {'type': 'string'}, 'tag': {'type': 'string'}}, + 'required': ['name'], + 'type': 'object', + }, + 'Pet': { + 'allOf': [ + {'$ref': '#/components/schemas/NewPet'}, + {'properties': {'id': {'type': 'integer'}}, 'required': ['id']}, + ] + }, + } + }, + 'externalDocs': { + 'url': 'https://github.com/open-rpc/examples/blob/master/service-descriptions/petstore-expanded-openrpc.json' + }, + 'info': { + 'contact': {'email': 'doesntexist@open-rpc.org', 'name': 'OpenRPC Team', 'url': 'https://open-rpc.org'}, + 'description': 'A sample API that uses a petstore as an example to demonstrate ' + 'features in the OpenRPC specification', + 'license': {'name': 'Apache 2.0', 'url': 'https://www.apache.org/licenses/LICENSE-2.0.html'}, + 'termsOfService': 'https://open-rpc.org', + 'title': 'Petstore Expanded', + 'version': '1.0.0', + }, + 'methods': [ + {'name': 'rpc.describe', 'params': [], 'result': {'name': 'default', 'schema': {'type': 'object'}}}, + { + 'description': 'Returns an OpenRPC schema as a description of this service', + 'name': 'rpc.discover', + 'params': [], + 'result': { + 'name': 'OpenRPC Schema', + 'schema': {'$ref': 'https://raw.githubusercontent.com/open-rpc/meta-schema/master/schema.json'}, + }, + }, + { + 'description': 'Returns all pets from the system that the user has access to\n' + 'Nam sed condimentum est. Maecenas tempor sagittis sapien, nec ' + 'rhoncus sem sagittis sit amet. Aenean at gravida augue, ac ' + 'iaculis sem. Curabitur odio lorem, ornare eget elementum nec, ' + 'cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt ' + 'varius justo. In hac habitasse platea dictumst. Integer at ' + 'adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante ' + 'molestie imperdiet. Vivamus id aliquam diam.', + 'name': 'Petstore.get_pets', + 'params': [ + { + 'description': 'tags to filter by', + 'name': 'tags', + 'schema': {'items': {'type': 'string'}, 'type': 'array'}, + }, + { + 'description': 'maximum number of results to return', + 'name': 'limit', + 'schema': {'type': 'integer'}, + }, + ], + 'result': { + 'description': 'pet response', + 'name': 'pet', + 'schema': {'items': {'$ref': '#/components/schemas/Pet'}, 'type': 'array'}, + }, + }, + { + 'description': 'Creates a new pet in the store. Duplicates are allowed', + 'name': 'Petstore.create_pet', + 'params': [ + { + 'description': 'Pet to add to the store.', + 'name': 'newPet', + 'schema': {'$ref': '#/components/schemas/NewPet'}, + } + ], + 'result': { + 'description': 'the newly created pet', + 'name': 'pet', + 'schema': {'$ref': '#/components/schemas/Pet'}, + }, + }, + { + 'description': 'Returns a user based on a single ID, if the user does not have ' 'access to the pet', + 'name': 'Petstore.get_pet_by_id', + 'params': [ + {'description': 'ID of pet to fetch', 'name': 'id', 'required': True, 'schema': {'type': 'integer'}} + ], + 'result': { + 'description': 'pet response', + 'name': 'pet', + 'schema': {'$ref': '#/components/schemas/Pet'}, + }, + }, + { + 'description': 'deletes a single pet based on the ID supplied', + 'name': 'Petstore.delete_pet_by_id', + 'params': [ + { + 'description': 'ID of pet to delete', + 'name': 'id', + 'required': True, + 'schema': {'type': 'integer'}, + } + ], + 'result': {'description': 'pet deleted', 'name': 'pet', 'schema': {}}, + }, + ], + 'openrpc': '1.0.0-rc1', + 'servers': [{'name': 'default', 'url': 'http://petstore.open-rpc.org'}], + } + + +def test_rpc_describe(client: 'FlaskClient') -> None: + rv = client.post('/api', json={'id': 1, 'jsonrpc': '2.0', 'method': 'rpc.describe'}) + data = rv.get_json() + assert data['id'] == 1 + assert data['jsonrpc'] == '2.0' + assert data['result']['name'] == 'Flask-JSONRPC' + assert data['result']['version'] == '2.0' + assert data['result']['servers'] is not None + assert 'url' in data['result']['servers'][0] + assert data['result']['methods'] == { + 'Petstore.create_pet': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'name', 'type': 'String'}, {'name': 'tag', 'type': 'String'}], + 'returns': {'type': 'Object'}, + 'type': 'method', + }, + 'Petstore.delete_pet_by_id': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'id', 'type': 'Number'}], + 'returns': {'type': 'Null'}, + 'type': 'method', + }, + 'Petstore.get_pet_by_id': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'id', 'type': 'Number'}], + 'returns': {'type': 'Object'}, + 'type': 'method', + }, + 'Petstore.get_pets': { + 'options': {'notification': True, 'validate': True}, + 'params': [{'name': 'tags', 'type': 'Object'}, {'name': 'limit', 'type': 'Number'}], + 'returns': {'type': 'Array'}, + 'type': 'method', + }, + 'rpc.describe': {'options': {}, 'params': [], 'returns': {'type': 'Object'}, 'type': 'method'}, + 'rpc.discover': {'options': {}, 'params': [], 'returns': {'type': 'Null'}, 'type': 'method'}, + } diff --git a/examples/openrpc/tox.ini b/examples/openrpc/tox.ini new file mode 100644 index 00000000..ea6d3675 --- /dev/null +++ b/examples/openrpc/tox.ini @@ -0,0 +1,17 @@ +[tox] +envlist = + py3{12,11,10,9,8} + py3{12,11,10,9,8}-async +skip_missing_interpreters = true + +[testenv] +package = wheel +wheel_build_env = .pkg +envtmpdir = {toxworkdir}/tmp/{envname} +constrain_package_deps = true +use_frozen_constraints = true +deps = + pytest + async: Flask[async]>=3.0.0,<4.0 +commands = + pytest -vv --tb=short --basetemp={envtmpdir} {posargs} diff --git a/examples/pydantic/README.rst b/examples/pydantic/README.rst deleted file mode 100644 index f56edb9b..00000000 --- a/examples/pydantic/README.rst +++ /dev/null @@ -1,117 +0,0 @@ -petstore -======= - -A petstore application. - - -Testing your service -******************** - -1. Running - -:: - - $ python petstore.py - * Running on http://0.0.0.0:5000/ - -2. Testing - -:: - $ curl 'http://localhost:5000/api' -X POST -H "Content-Type: application/json; indent=4" \ - --data-raw '{ - "jsonrpc": "2.0", - "method": "Petstore.create_pet", - "params": { - "pet": { - "name": "Jhon", - "tag": "cat" - } - }, - "id": "1c7fb3b2-7a87-4cf7-8e28-aafc33dae71d" - }' - { - "id": "1c7fb3b2-7a87-4cf7-8e28-aafc33dae71d", - "jsonrpc": "2.0",P - "result": { - "id": 32, - "name": "Jhon", - "tag": "cat" - } - } - - - -:: - $ curl 'http://localhost:5000/api' -X POST -H "Content-Type: application/json; indent=4" \ - --data-raw '{ - "jsonrpc": "2.0", - "method": "Petstore.get_pets", - "params": {}, - "id": "16ebeed1-748c-4983-ba19-2848692c873a" - }' - { - "id": "16ebeed1-748c-4983-ba19-2848692c873a", - "jsonrpc": "2.0", - "result": [ - { - "id": 1, - "name": "Bob", - "tag": "dog" - }, - { - "id": 2, - "name": "Eve", - "tag": "cat" - }, - { - "id": 3, - "name": "Alice", - "tag": "bird" - }, - { - "id": 32, - "name": "Jhon", - "tag": "cat" - } - ] - } - - - -:: - $ curl 'http://localhost:5000/api' -X POST -H "Content-Type: application/json; indent=4" \ - --data-raw '{ - "jsonrpc": "2.0", - "method": "Petstore.get_pet_by_id", - "params": { - "id": 32 - }, - "id": "5dfbd1c0-6919-4ce2-a05e-0b4a4aa2aeb2" - }' - { - "id": "5dfbd1c0-6919-4ce2-a05e-0b4a4aa2aeb2", - "jsonrpc": "2.0", - "result": { - "id": 32, - "name": "Jhon", - "tag": "cat" - } - } - - - -:: - $ curl 'http://localhost:5000/api' -X POST -H 'Content-Type: application/json;charset=utf-8' \ - --data-raw '{ - "jsonrpc": "2.0", - "method": "Petstore.delete_pet_by_id", - "params": { - "id": 32 - }, - "id": "706cf9c3-5b5d-4288-8555-a67c8b5de481" - }' - { - "id": "706cf9c3-5b5d-4288-8555-a67c8b5de481", - "jsonrpc": "2.0", - "result": null - } diff --git a/examples/pydantic/run.py b/examples/pydantic/run.py deleted file mode 100755 index cf86f266..00000000 --- a/examples/pydantic/run.py +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2024-2024, Cenobit Technologies, Inc. http://cenobit.es/ -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * Neither the name of the Cenobit Technologies nor the names of -# its contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -import random -import typing as t - -from flask import Flask - -from pydantic import BaseModel - -from flask_jsonrpc import JSONRPC -from flask_jsonrpc.contrib.openrpc import OpenRPC, typing as st - -app = Flask('openrpc') -jsonrpc = JSONRPC(app, '/api', enable_web_browsable_api=True) -openrpc = OpenRPC( - app, - jsonrpc, - openrpc_schema=st.OpenRPCSchema( - openrpc='1.0.0-rc1', - info=st.Info( - version='1.0.0', - title='Petstore Expanded', - description=( - 'A sample API that uses a petstore as an example to ' - 'demonstrate features in the OpenRPC specification' - ), - terms_of_service='https://open-rpc.org', - contact=st.Contact(name='OpenRPC Team', email='doesntexist@open-rpc.org', url='https://open-rpc.org'), - license=st.License(name='Apache 2.0', url='https://www.apache.org/licenses/LICENSE-2.0.html'), - ), - servers=[st.Server(url='http://petstore.open-rpc.org')], - components=st.Components( - schemas={ - 'Pet': st.Schema( - all_of=[ - st.Schema(ref='#/components/schemas/NewPet'), - st.Schema(required=['id'], properties={'id': st.Schema(type=st.SchemaDataType.INTEGER)}), - ] - ), - 'NewPet': st.Schema( - type=st.SchemaDataType.OBJECT, - required=['name'], - properties={ - 'name': st.Schema(type=st.SchemaDataType.STRING), - 'tag': st.Schema(type=st.SchemaDataType.STRING), - }, - ), - } - ), - external_docs=st.ExternalDocumentation( - url='https://github.com/open-rpc/examples/blob/master/service-descriptions/petstore-expanded-openrpc.json' - ), - ), -) - - -class NewPet(BaseModel): - name: str - tag: str - - -class Pet(NewPet): - id: int - - -PETS = [Pet(id=1, name='Bob', tag='dog'), Pet(id=2, name='Eve', tag='cat'), Pet(id=3, name='Alice', tag='bird')] - - -@openrpc.extend_schema( - name='Petstore.get_pets', - description=( - 'Returns all pets from the system that the user has access to\n' - 'Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem ' - 'sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio ' - 'lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ' - 'ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer ' - 'at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie ' - 'imperdiet. Vivamus id aliquam diam.' - ), - params=[ - st.ContentDescriptor( - name='tags', - description='tags to filter by', - schema=st.Schema(type=st.SchemaDataType.ARRAY, items=st.Schema(type=st.SchemaDataType.STRING)), - ), - st.ContentDescriptor( - name='limit', - description='maximum number of results to return', - schema=st.Schema(type=st.SchemaDataType.INTEGER), - ), - ], - result=st.ContentDescriptor( - name='pet', - description='pet response', - schema=st.Schema(type=st.SchemaDataType.ARRAY, items=st.Schema(ref='#/components/schemas/Pet')), - ), -) -@jsonrpc.method('Petstore.get_pets') -def get_pets(tags: t.Optional[t.List[str]] = None, limit: t.Optional[int] = None) -> t.List[Pet]: - pets = PETS - if tags is not None: - pets = [pet for pet in pets if pet.tag in tags] - if limit is not None: - pets = pets[:limit] - return pets - - -@openrpc.extend_schema( - name='Petstore.create_pet', - description='Creates a new pet in the store. Duplicates are allowed', - params=[ - st.ContentDescriptor( - name='newPet', description='Pet to add to the store.', schema=st.Schema(ref='#/components/schemas/NewPet') - ) - ], - result=st.ContentDescriptor( - name='pet', description='the newly created pet', schema=st.Schema(ref='#/components/schemas/Pet') - ), -) -@jsonrpc.method('Petstore.create_pet') -def create_pet(pet: NewPet) -> Pet: - pet = Pet(id=random.randint(4, 100), name=pet.name, tag=pet.tag) - PETS.append(pet) - return pet - - -@openrpc.extend_schema( - name='Petstore.get_pet_by_id', - description='Returns a user based on a single ID, if the user does not have access to the pet', - params=[ - st.ContentDescriptor( - name='id', description='ID of pet to fetch', required=True, schema=st.Schema(type=st.SchemaDataType.INTEGER) - ) - ], - result=st.ContentDescriptor( - name='pet', description='pet response', schema=st.Schema(ref='#/components/schemas/Pet') - ), -) -@jsonrpc.method('Petstore.get_pet_by_id') -def get_pet_by_id(id: int) -> t.Optional[Pet]: - pet = [pet for pet in PETS if pet.id == id] - return None if len(pet) == 0 else pet[0] - - -@openrpc.extend_schema( - name='Petstore.delete_pet_by_id', - description='deletes a single pet based on the ID supplied', - params=[ - st.ContentDescriptor( - name='id', - description='ID of pet to delete', - required=True, - schema=st.Schema(type=st.SchemaDataType.INTEGER), - ) - ], - result=st.ContentDescriptor(name='pet', description='pet deleted', schema=st.Schema()), -) -@jsonrpc.method('Petstore.delete_pet_by_id') -def delete_pet_by_id(id: int) -> None: - global PETS - PETS = [pet for pet in PETS if pet.id != id] # noqa: F823, F841 - - -if __name__ == '__main__': - app.run(host='0.0.0.0', debug=True) diff --git a/examples/register_view_func/run.py b/examples/register_view_func/run.py deleted file mode 100644 index bc6cc09e..00000000 --- a/examples/register_view_func/run.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2012-2024, Cenobit Technologies, Inc. http://cenobit.es/ -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * Neither the name of the Cenobit Technologies nor the names of -# its contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -import typing as t -from typing import Any, Dict, List, Union, NoReturn, Optional -from numbers import Real - -from flask import Flask - -from flask_jsonrpc import JSONRPC - - -class MyApp: - def index(self: t.Self) -> str: - return 'Welcome to Flask JSON-RPC' - - def greeting(self: t.Self, name: str) -> str: - return f'Hello {name}' - - def args_validate(self: t.Self, a1: int, a2: str, a3: bool, a4: List[Any], a5: Dict[Any, Any]) -> str: - return f'Number: {a1}, String: {a2}, Boolean: {a3}, Array: {a4}, Object: {a5}' - - def notify(self: t.Self, _string: Optional[str] = None) -> None: - pass - - def fails(self: t.Self, _string: Optional[str] = None) -> NoReturn: - raise ValueError('example of fail') - - def sum_(self: t.Self, a: Real, b: Real) -> Real: - return a + b - - @classmethod - def multiply(cls: t.Type[t.Self], a: float, b: float) -> float: - return a * b - - @staticmethod - def divide(a: Real, b: Real) -> Real: - return a / float(b) - - -app = Flask('register_view_func') -jsonrpc = JSONRPC(app, '/api', enable_web_browsable_api=True) - - -@jsonrpc.method('subtract') -def subtract(a: Union[int, float], b: Union[int, float]) -> Union[int, float]: - return a - b - - -def hello_default_args(string: str = 'Flask JSON-RPC') -> str: - return f'We salute you {string}' - - -jsonrpc.register(hello_default_args) - -my_app = MyApp() -jsonrpc.register(my_app.index) -jsonrpc.register(my_app.greeting) -jsonrpc.register(my_app.args_validate) -jsonrpc.register(my_app.notify) -jsonrpc.register(my_app.fails) -jsonrpc.register(my_app.sum_, name='sum') -jsonrpc.register(my_app.multiply) -jsonrpc.register(my_app.divide) - - -if __name__ == '__main__': - app.run(host='0.0.0.0', debug=True) diff --git a/examples/wba/README.rst b/examples/wba/README.rst deleted file mode 100644 index 5322ffca..00000000 --- a/examples/wba/README.rst +++ /dev/null @@ -1,104 +0,0 @@ -minimal -======= - -A minimal application with web browsable API. - - -Testing your service -******************** - -1. Running - -:: - - $ python minimal.py - * Running on http://0.0.0.0:5000/ - - -2. Testing - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.index", - "params": {}, - "id": "1" - }' http://localhost:5000/api - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 78 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 12:40:08 GMT - - { - "id": "1", - "jsonrpc": "2.0", - "result": "Welcome to Flask JSON-RPC" - } - - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.hello", - "params": ["Flask"], - "id": "1" - }' http://localhost:5000/api - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 64 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 12:41:08 GMT - - { - "id": "1", - "jsonrpc": "2.0", - "result": "Hello Flask" - } - - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.notify" - }' http://localhost:5000/api - HTTP/1.0 204 NO CONTENT - Content-Type: application/json - Content-Length: 0 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 12:41:49 GMT - - -:: - - $ curl -i -X POST -H "Content-Type: application/json; indent=4" \ - -d '{ - "jsonrpc": "2.0", - "method": "App.fails", - "params": ["Flask"], - "id": "1" - }' http://localhost:5000/api - HTTP/1.0 200 OK - Content-Type: application/json - Content-Length: 704 - Server: Werkzeug/0.8.3 Python/2.7.7 - Date: Mon, 07 Jul 2014 12:42:40 GMT - - { - "error": { - "code": 500, - "data": null, - "executable": "/usr/bin/python2", - "message": "OtherError: ", - "name": "OtherError", - "stack": "Traceback (most recent call last):\n File \"/home/nycholas/project/src/o_lalertom/flask/flask-jsonrpc/examples/../flask_jsonrpc/site.py\", line 208, in response_dict\n R = apply_version[version](method, D['params'])\n File \"/home/nycholas/project/src/o_lalertom/flask/flask-jsonrpc/examples/../flask_jsonrpc/site.py\", line 168, in \n '2.0': lambda f, p: f(**encode_kw(p)) if type(p) is dict else f(*p),\n File \"minimal.py\", line 78, in fails\n raise ValueError\nValueError\n" - }, - "id": "1", - "jsonrpc": "2.0" - } diff --git a/examples/wba/minimal.py b/examples/wba/minimal.py deleted file mode 100755 index 7bcead49..00000000 --- a/examples/wba/minimal.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python -# Copyright (c) 2012-2024, Cenobit Technologies, Inc. http://cenobit.es/ -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * Neither the name of the Cenobit Technologies nor the names of -# its contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -from typing import NoReturn, Optional - -from flask import Flask - -from flask_jsonrpc import JSONRPC - -app = Flask('wba') -jsonrpc = JSONRPC(app, '/api', enable_web_browsable_api=True) - - -@jsonrpc.method('App.index') -def index() -> str: - return 'Welcome to Flask JSON-RPC' - - -@jsonrpc.method('App.hello') -def hello(name: str) -> str: - return f'Hello {name}' - - -@jsonrpc.method('App.helloDefaultArgs') -def hello_default_args(string: str = 'Flask JSON-RPC') -> str: - return f'We salute you {string}' - - -@jsonrpc.method('App.helloDefaultArgsValidate') -def hello_default_args_validate(string: str = 'Flask JSON-RPC') -> str: - return f'We salute you {string}' - - -@jsonrpc.method('App.args') -def args_validate_python_mode(a1: int, a2: str, a3: bool, a4: list, a5: dict) -> str: - return f'int: {a1}, str: {a2}, bool: {a3}, list: {a4}, dict: {a5}' - - -@jsonrpc.method('App.notify') -def notify(_string: Optional[str]) -> None: - pass - - -@jsonrpc.method('App.fails') -def fails(_string: Optional[str]) -> NoReturn: - raise ValueError('example of fail') - - -@jsonrpc.method('App.notValidate', validate=False) -def not_validate(s='Oops!'): # noqa: ANN001,ANN202,ANN201 - return f'Not validate: {s}' - - -@jsonrpc.method('App.mixinNotValidate', validate=False) -def mixin_not_validate(s, t: int, u, v: str, x, z): # noqa: ANN001,ANN202,ANN201 - return f'Not validate: {s} {t} {u} {v} {x} {z}' - - -@jsonrpc.method('App.mixinNotValidateReturn', validate=False) -def mixin_not_validate_with_no_return(_s, _t: int, _u, _v: str, _x, _z): # noqa: ANN001,ANN202,ANN201 - pass - - -if __name__ == '__main__': - app.run(host='0.0.0.0', debug=True) diff --git a/tox.ini b/tox.ini index 730d81f4..a8fa505e 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ constrain_package_deps = true use_frozen_constraints = true deps = -r requirements/tests.txt - async: Flask[async] + async: Flask[async]>=3.0.0,<4.0 commands = pytest -vv --tb=short --basetemp={envtmpdir} {posargs}