|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
JSON-RPC server based on fastapi:
https://fastapi.tiangolo.com
Motivation ^^^^^^^^^^
Autogenerated OpenAPI and Swagger (thanks to fastapi) for JSON-RPC!!!
.. code-block:: bash
pip install fastapi-jsonrpc
Read FastAPI documentation and see usage examples bellow
.. 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
.. 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
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
Bumps certifi from 2022.9.24 to 2022.12.7.
9e9e840
2022.12.07Dependabot 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
.
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)?
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?
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
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
import fastapi_jsonrpc as jsonrpc
items_router = jsonrpc.Entrypoint("/api")
@items_router.method() def list_items() -> dict: return {"from": "list_items"} ```
```python3
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.
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!
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
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