fastapi-jsonrpc


Namefastapi-jsonrpc JSON
Version 3.1.1 PyPI version JSON
download
home_pagehttps://github.com/smagafurov/fastapi-jsonrpc
SummaryJSON-RPC server based on fastapi
upload_time2024-03-12 07:48:48
maintainer
docs_urlNone
authorSergey Magafurov
requires_python>=3.8,<4.0
licenseMIT
keywords json-rpc asgi swagger openapi fastapi pydantic starlette
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            |tests|

.. |tests| image:: https://github.com/smagafurov/fastapi-jsonrpc/actions/workflows/tests.yml/badge.svg
   :target: https://github.com/smagafurov/fastapi-jsonrpc/actions/workflows/tests.yml


Description
===========

JSON-RPC server based on fastapi:

    https://fastapi.tiangolo.com

OpenRPC supported too.

Motivation
^^^^^^^^^^

Autogenerated **OpenAPI** and **Swagger** (thanks to fastapi) for JSON-RPC!!!

Installation
============

.. code-block:: bash

    pip install fastapi-jsonrpc

Documentation
=============

Read FastAPI documentation and see usage examples bellow

Simple usage example
====================

.. code-block:: bash

    pip install uvicorn

example1.py

.. code-block:: python

    import fastapi_jsonrpc as jsonrpc
    from pydantic import BaseModel
    from fastapi import Body


    app = jsonrpc.API()

    api_v1 = jsonrpc.Entrypoint('/api/v1/jsonrpc')


    class MyError(jsonrpc.BaseError):
        CODE = 5000
        MESSAGE = 'My error'

        class DataModel(BaseModel):
            details: str


    @api_v1.method(errors=[MyError])
    def echo(
        data: str = Body(..., examples=['123']),
    ) -> str:
        if data == 'error':
            raise MyError(data={'details': 'error'})
        else:
            return data


    app.bind_entrypoint(api_v1)


    if __name__ == '__main__':
        import uvicorn
        uvicorn.run('example1:app', port=5000, debug=True, access_log=False)

OpenRPC:

    http://127.0.0.1:5000/openrpc.json

Swagger:

    http://127.0.0.1:5000/docs

FastAPI dependencies usage example
==================================

.. code-block:: bash

    pip install uvicorn

example2.py

.. code-block:: python

    import logging
    from contextlib import asynccontextmanager

    from pydantic import BaseModel, Field
    import fastapi_jsonrpc as jsonrpc
    from fastapi import Body, Header, Depends


    logger = logging.getLogger(__name__)


    # database models

    class User:
        def __init__(self, name):
            self.name = name

        def __eq__(self, other):
            if not isinstance(other, User):
                return False
            return self.name == other.name


    class Account:
        def __init__(self, account_id, owner, amount, currency):
            self.account_id = account_id
            self.owner = owner
            self.amount = amount
            self.currency = currency

        def owned_by(self, user: User):
            return self.owner == user


    # fake database

    users = {
        '1': User('user1'),
        '2': User('user2'),
    }

    accounts = {
        '1.1': Account('1.1', users['1'], 100, 'USD'),
        '1.2': Account('1.2', users['1'], 200, 'EUR'),
        '2.1': Account('2.1', users['2'], 300, 'USD'),
    }


    def get_user_by_token(auth_token) -> User:
        return users[auth_token]


    def get_account_by_id(account_id) -> Account:
        return accounts[account_id]


    # schemas

    class Balance(BaseModel):
        """Account balance"""
        amount: int = Field(..., example=100)
        currency: str = Field(..., example='USD')


    # errors

    class AuthError(jsonrpc.BaseError):
        CODE = 7000
        MESSAGE = 'Auth error'


    class AccountNotFound(jsonrpc.BaseError):
        CODE = 6000
        MESSAGE = 'Account not found'


    class NotEnoughMoney(jsonrpc.BaseError):
        CODE = 6001
        MESSAGE = 'Not enough money'

        class DataModel(BaseModel):
            balance: Balance


    # dependencies

    def get_auth_user(
        # this will become the header-parameter of json-rpc method that uses this dependency
        auth_token: str = Header(
            None,
            alias='user-auth-token',
        ),
    ) -> User:
        if not auth_token:
            raise AuthError

        try:
            return get_user_by_token(auth_token)
        except KeyError:
            raise AuthError


    def get_account(
        # this will become the parameter of the json-rpc method that uses this dependency
        account_id: str = Body(..., example='1.1'),
        user: User = Depends(get_auth_user),
    ) -> Account:
        try:
            account = get_account_by_id(account_id)
        except KeyError:
            raise AccountNotFound

        if not account.owned_by(user):
            raise AccountNotFound

        return account


    # JSON-RPC middlewares

    @asynccontextmanager
    async def logging_middleware(ctx: jsonrpc.JsonRpcContext):
        logger.info('Request: %r', ctx.raw_request)
        try:
            yield
        finally:
            logger.info('Response: %r', ctx.raw_response)


    # JSON-RPC entrypoint

    common_errors = [AccountNotFound, AuthError]
    common_errors.extend(jsonrpc.Entrypoint.default_errors)

    api_v1 = jsonrpc.Entrypoint(
        # Swagger shows for entrypoint common parameters gathered by dependencies and common_dependencies:
        #    - json-rpc-parameter 'account_id'
        #    - header parameter 'user-auth-token'
        '/api/v1/jsonrpc',
        errors=common_errors,
        middlewares=[logging_middleware],
        # this dependencies called once for whole json-rpc batch request
        dependencies=[Depends(get_auth_user)],
        # this dependencies called separately for every json-rpc request in batch request
        common_dependencies=[Depends(get_account)],
    )


    # JSON-RPC methods of this entrypoint

    # this json-rpc method has one json-rpc-parameter 'account_id' and one header parameter 'user-auth-token'
    @api_v1.method()
    def get_balance(
        account: Account = Depends(get_account),
    ) -> Balance:
        return Balance(
            amount=account.amount,
            currency=account.currency,
        )


    # this json-rpc method has two json-rpc-parameters 'account_id', 'amount' and one header parameter 'user-auth-token'
    @api_v1.method(errors=[NotEnoughMoney])
    def withdraw(
        account: Account = Depends(get_account),
        amount: int = Body(..., gt=0, example=10),
    ) -> Balance:
        if account.amount - amount < 0:
            raise NotEnoughMoney(data={'balance': get_balance(account)})
        account.amount -= amount
        return get_balance(account)


    # JSON-RPC API

    app = jsonrpc.API()
    app.bind_entrypoint(api_v1)


    if __name__ == '__main__':
        import uvicorn
        uvicorn.run('example2:app', port=5000, debug=True, access_log=False)

OpenRPC:

    http://127.0.0.1:5000/openrpc.json

Swagger:

    http://127.0.0.1:5000/docs

.. image:: ./images/fastapi-jsonrpc.png

Development
===========

* Install poetry

    https://github.com/sdispater/poetry#installation

* Install dependencies

    .. code-block:: bash

        poetry update

* Regenerate README.rst

    .. code-block:: bash

        rst_include include -q README.src.rst README.rst

* Change dependencies

    Edit ``pyproject.toml``

    .. code-block:: bash

        poetry update

* Bump version

    .. code-block:: bash

        poetry version patch
        poetry version minor
        poetry version major

* Publish to pypi

    .. code-block:: bash

        poetry publish --build


            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/smagafurov/fastapi-jsonrpc",
    "name": "fastapi-jsonrpc",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.8,<4.0",
    "maintainer_email": "",
    "keywords": "json-rpc,asgi,swagger,openapi,fastapi,pydantic,starlette",
    "author": "Sergey Magafurov",
    "author_email": "magafurov@tochka.com",
    "download_url": "https://files.pythonhosted.org/packages/23/5f/3e04420d0a510df3f1fe3360d9812df955f66b89a73dbd103e41d966b64f/fastapi_jsonrpc-3.1.1.tar.gz",
    "platform": null,
    "description": "|tests|\n\n.. |tests| image:: https://github.com/smagafurov/fastapi-jsonrpc/actions/workflows/tests.yml/badge.svg\n   :target: https://github.com/smagafurov/fastapi-jsonrpc/actions/workflows/tests.yml\n\n\nDescription\n===========\n\nJSON-RPC server based on fastapi:\n\n    https://fastapi.tiangolo.com\n\nOpenRPC supported too.\n\nMotivation\n^^^^^^^^^^\n\nAutogenerated **OpenAPI** and **Swagger** (thanks to fastapi) for JSON-RPC!!!\n\nInstallation\n============\n\n.. code-block:: bash\n\n    pip install fastapi-jsonrpc\n\nDocumentation\n=============\n\nRead FastAPI documentation and see usage examples bellow\n\nSimple usage example\n====================\n\n.. code-block:: bash\n\n    pip install uvicorn\n\nexample1.py\n\n.. code-block:: python\n\n    import fastapi_jsonrpc as jsonrpc\n    from pydantic import BaseModel\n    from fastapi import Body\n\n\n    app = jsonrpc.API()\n\n    api_v1 = jsonrpc.Entrypoint('/api/v1/jsonrpc')\n\n\n    class MyError(jsonrpc.BaseError):\n        CODE = 5000\n        MESSAGE = 'My error'\n\n        class DataModel(BaseModel):\n            details: str\n\n\n    @api_v1.method(errors=[MyError])\n    def echo(\n        data: str = Body(..., examples=['123']),\n    ) -> str:\n        if data == 'error':\n            raise MyError(data={'details': 'error'})\n        else:\n            return data\n\n\n    app.bind_entrypoint(api_v1)\n\n\n    if __name__ == '__main__':\n        import uvicorn\n        uvicorn.run('example1:app', port=5000, debug=True, access_log=False)\n\nOpenRPC:\n\n    http://127.0.0.1:5000/openrpc.json\n\nSwagger:\n\n    http://127.0.0.1:5000/docs\n\nFastAPI dependencies usage example\n==================================\n\n.. code-block:: bash\n\n    pip install uvicorn\n\nexample2.py\n\n.. code-block:: python\n\n    import logging\n    from contextlib import asynccontextmanager\n\n    from pydantic import BaseModel, Field\n    import fastapi_jsonrpc as jsonrpc\n    from fastapi import Body, Header, Depends\n\n\n    logger = logging.getLogger(__name__)\n\n\n    # database models\n\n    class User:\n        def __init__(self, name):\n            self.name = name\n\n        def __eq__(self, other):\n            if not isinstance(other, User):\n                return False\n            return self.name == other.name\n\n\n    class Account:\n        def __init__(self, account_id, owner, amount, currency):\n            self.account_id = account_id\n            self.owner = owner\n            self.amount = amount\n            self.currency = currency\n\n        def owned_by(self, user: User):\n            return self.owner == user\n\n\n    # fake database\n\n    users = {\n        '1': User('user1'),\n        '2': User('user2'),\n    }\n\n    accounts = {\n        '1.1': Account('1.1', users['1'], 100, 'USD'),\n        '1.2': Account('1.2', users['1'], 200, 'EUR'),\n        '2.1': Account('2.1', users['2'], 300, 'USD'),\n    }\n\n\n    def get_user_by_token(auth_token) -> User:\n        return users[auth_token]\n\n\n    def get_account_by_id(account_id) -> Account:\n        return accounts[account_id]\n\n\n    # schemas\n\n    class Balance(BaseModel):\n        \"\"\"Account balance\"\"\"\n        amount: int = Field(..., example=100)\n        currency: str = Field(..., example='USD')\n\n\n    # errors\n\n    class AuthError(jsonrpc.BaseError):\n        CODE = 7000\n        MESSAGE = 'Auth error'\n\n\n    class AccountNotFound(jsonrpc.BaseError):\n        CODE = 6000\n        MESSAGE = 'Account not found'\n\n\n    class NotEnoughMoney(jsonrpc.BaseError):\n        CODE = 6001\n        MESSAGE = 'Not enough money'\n\n        class DataModel(BaseModel):\n            balance: Balance\n\n\n    # dependencies\n\n    def get_auth_user(\n        # this will become the header-parameter of json-rpc method that uses this dependency\n        auth_token: str = Header(\n            None,\n            alias='user-auth-token',\n        ),\n    ) -> User:\n        if not auth_token:\n            raise AuthError\n\n        try:\n            return get_user_by_token(auth_token)\n        except KeyError:\n            raise AuthError\n\n\n    def get_account(\n        # this will become the parameter of the json-rpc method that uses this dependency\n        account_id: str = Body(..., example='1.1'),\n        user: User = Depends(get_auth_user),\n    ) -> Account:\n        try:\n            account = get_account_by_id(account_id)\n        except KeyError:\n            raise AccountNotFound\n\n        if not account.owned_by(user):\n            raise AccountNotFound\n\n        return account\n\n\n    # JSON-RPC middlewares\n\n    @asynccontextmanager\n    async def logging_middleware(ctx: jsonrpc.JsonRpcContext):\n        logger.info('Request: %r', ctx.raw_request)\n        try:\n            yield\n        finally:\n            logger.info('Response: %r', ctx.raw_response)\n\n\n    # JSON-RPC entrypoint\n\n    common_errors = [AccountNotFound, AuthError]\n    common_errors.extend(jsonrpc.Entrypoint.default_errors)\n\n    api_v1 = jsonrpc.Entrypoint(\n        # Swagger shows for entrypoint common parameters gathered by dependencies and common_dependencies:\n        #    - json-rpc-parameter 'account_id'\n        #    - header parameter 'user-auth-token'\n        '/api/v1/jsonrpc',\n        errors=common_errors,\n        middlewares=[logging_middleware],\n        # this dependencies called once for whole json-rpc batch request\n        dependencies=[Depends(get_auth_user)],\n        # this dependencies called separately for every json-rpc request in batch request\n        common_dependencies=[Depends(get_account)],\n    )\n\n\n    # JSON-RPC methods of this entrypoint\n\n    # this json-rpc method has one json-rpc-parameter 'account_id' and one header parameter 'user-auth-token'\n    @api_v1.method()\n    def get_balance(\n        account: Account = Depends(get_account),\n    ) -> Balance:\n        return Balance(\n            amount=account.amount,\n            currency=account.currency,\n        )\n\n\n    # this json-rpc method has two json-rpc-parameters 'account_id', 'amount' and one header parameter 'user-auth-token'\n    @api_v1.method(errors=[NotEnoughMoney])\n    def withdraw(\n        account: Account = Depends(get_account),\n        amount: int = Body(..., gt=0, example=10),\n    ) -> Balance:\n        if account.amount - amount < 0:\n            raise NotEnoughMoney(data={'balance': get_balance(account)})\n        account.amount -= amount\n        return get_balance(account)\n\n\n    # JSON-RPC API\n\n    app = jsonrpc.API()\n    app.bind_entrypoint(api_v1)\n\n\n    if __name__ == '__main__':\n        import uvicorn\n        uvicorn.run('example2:app', port=5000, debug=True, access_log=False)\n\nOpenRPC:\n\n    http://127.0.0.1:5000/openrpc.json\n\nSwagger:\n\n    http://127.0.0.1:5000/docs\n\n.. image:: ./images/fastapi-jsonrpc.png\n\nDevelopment\n===========\n\n* Install poetry\n\n    https://github.com/sdispater/poetry#installation\n\n* Install dependencies\n\n    .. code-block:: bash\n\n        poetry update\n\n* Regenerate README.rst\n\n    .. code-block:: bash\n\n        rst_include include -q README.src.rst README.rst\n\n* Change dependencies\n\n    Edit ``pyproject.toml``\n\n    .. code-block:: bash\n\n        poetry update\n\n* Bump version\n\n    .. code-block:: bash\n\n        poetry version patch\n        poetry version minor\n        poetry version major\n\n* Publish to pypi\n\n    .. code-block:: bash\n\n        poetry publish --build\n\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "JSON-RPC server based on fastapi",
    "version": "3.1.1",
    "project_urls": {
        "Homepage": "https://github.com/smagafurov/fastapi-jsonrpc",
        "Repository": "https://github.com/smagafurov/fastapi-jsonrpc"
    },
    "split_keywords": [
        "json-rpc",
        "asgi",
        "swagger",
        "openapi",
        "fastapi",
        "pydantic",
        "starlette"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "01ce8ee43e115a8566d66d93d3e84715f40f50405b4152b8ce7c25ece8ca8341",
                "md5": "313e66545a57fc8333584f33075d78fc",
                "sha256": "cbf6582ea8b671f44d761b053e3bacbadef049ddbf653fbbd9c3fc1358424add"
            },
            "downloads": -1,
            "filename": "fastapi_jsonrpc-3.1.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "313e66545a57fc8333584f33075d78fc",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8,<4.0",
            "size": 15019,
            "upload_time": "2024-03-12T07:48:42",
            "upload_time_iso_8601": "2024-03-12T07:48:42.156214Z",
            "url": "https://files.pythonhosted.org/packages/01/ce/8ee43e115a8566d66d93d3e84715f40f50405b4152b8ce7c25ece8ca8341/fastapi_jsonrpc-3.1.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "235f3e04420d0a510df3f1fe3360d9812df955f66b89a73dbd103e41d966b64f",
                "md5": "c23d7533f4f32fcabcd437b7c787f1a8",
                "sha256": "115ca02326c28d9ecf8607f53200693994761466ddea722ba19fb368a52d3c97"
            },
            "downloads": -1,
            "filename": "fastapi_jsonrpc-3.1.1.tar.gz",
            "has_sig": false,
            "md5_digest": "c23d7533f4f32fcabcd437b7c787f1a8",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8,<4.0",
            "size": 16104,
            "upload_time": "2024-03-12T07:48:48",
            "upload_time_iso_8601": "2024-03-12T07:48:48.312607Z",
            "url": "https://files.pythonhosted.org/packages/23/5f/3e04420d0a510df3f1fe3360d9812df955f66b89a73dbd103e41d966b64f/fastapi_jsonrpc-3.1.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-03-12 07:48:48",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "smagafurov",
    "github_project": "fastapi-jsonrpc",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "fastapi-jsonrpc"
}
        
Elapsed time: 0.21757s