JSON-RPC server based on fastapi

smagafurov, updated 🕥 2022-12-09 09:12:34

|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

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(..., example='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)

Go to:

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)

Go to:

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
    

Issues

build(deps): bump certifi from 2022.9.24 to 2022.12.7

opened on 2022-12-09 09:12:33 by dependabot[bot]

Bumps certifi from 2022.9.24 to 2022.12.7.

Commits


Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) - `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language - `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language - `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language - `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/smagafurov/fastapi-jsonrpc/network/alerts).

Multiple OpenAPI examples

opened on 2022-11-30 09:53:22 by v--

I understand that I can add examples to endpoints via python @api.method() def endpoint(value1: str = Body(example='test'), value2: int = Body(example=3)) ...

What to do if I want to add multiple examples as discussed in https://fastapi.tiangolo.com/tutorial/schema-extra-example/?h=examples#body-with-multiple-examples ? The obvious thing to do would be to use list the examples in the Body, as it is done in plain FastAPI, but that doesn't work. Perhaps the Entrypoint#method method can accept an examples parameter (i.e. add it to the MethodRoute class)?

Support by-position parameters

opened on 2022-11-21 10:01:34 by hongqn

According to JSON-RPC 2.0 Specification , the params field of request object may be either by-position (Array) or by-name (Object).

However the current implementation of fastapi-jsonrpc forces to use by-name parameter:

https://github.com/smagafurov/fastapi-jsonrpc/blob/1329be64ea635a844cdb529eaf31a1ac3055ae58/fastapi_jsonrpc/init.py#L369

This causes this awesome library is not usable in some circumstances. Can we support by-position parameters in the future version?

Add bind_entrypoints option to reproduce FastApi's router feature

opened on 2021-10-31 11:39:52 by Smlep

closes #18

Fastapi offers a router functionality detailed here. It can be really useful to split the methods into many files.

This PR adds an option to bind_entrypoint so that multiple Entrypoint with the same path can be merged.

Here is an example of how I would use this:

```python3

main.py

from items import items_router from products import products_router

app = jsonrpc.API()

app.bind_entrypoint(items_router)

app.bind_entrypoint(products_router, add_to_existing_path=True)

```

```python3

items.py

import fastapi_jsonrpc as jsonrpc

items_router = jsonrpc.Entrypoint("/api")

@items_router.method() def list_items() -> dict: return {"from": "list_items"} ```

```python3

products.py

import fastapi_jsonrpc as jsonrpc

products_router = jsonrpc.Entrypoint("/api")

@products_router.method() def list_products() -> dict: return {"from": "list_products"} ```

With this example, both list_items and list_products methods can be accessed from /api.

I have also added some tests for this feature.

Allow using routers to split the code

opened on 2021-04-14 20:18:40 by Smlep

Hi,

Fastapi offers a router functionality detailed here. It can be really useful to split the methods into many files.

It would be nice to be able to do the same things with the JSONRPC methods. Currently, since Entrypoint inherits from fastapi's APIRouter, trying to bind another Entrypoint with the same path won't allow access to the methods from the second Entrypoint.

There should be a way to bind 2 Entrypoint with the same path to one app, allowing both of their methods to be considered.

Really great library btw, good work!

Websocket support

opened on 2020-12-23 16:28:21 by pjotterplotter

Nice library! I was wondering if there are any plans to also support websockets (which is a nice extra fastapi provides), like these libraries do?:

https://jsonrpcclient.readthedocs.io/en/latest/ https://github.com/codemation/easyrpc

Releases

refs/tags/v2.4.1 2022-11-09 13:16:04

refs/tags/v2.4.0 2022-11-09 12:54:18

refs/tags/v2.3.0 2022-08-05 18:18:17

refs/tags/v2.2.2 2022-05-13 15:44:08

refs/tags/v2.2.1 2022-05-12 16:53:56

v2.1.6 2022-01-01 20:45:18

What's Changed

  • Fix func annotation for adding a new method by @mahenzon in https://github.com/smagafurov/fastapi-jsonrpc/pull/30

New Contributors

  • @mahenzon made their first contribution in https://github.com/smagafurov/fastapi-jsonrpc/pull/30

Full Changelog: https://github.com/smagafurov/fastapi-jsonrpc/compare/v2.1.5...v2.1.6

json-rpc json-rpc-server asgi swagger openapi fastapi pydantic starlette