Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Features:
- :doc:`intuitive interface <pjrpc/quickstart>`
- :doc:`extensibility <pjrpc/extending>`
- :doc:`synchronous and asynchronous client backends <pjrpc/client>`
- :doc:`popular frameworks integration <pjrpc/server>` (aiohttp, flask, aio_pika)
- :doc:`popular frameworks integration <pjrpc/server>` (aiohttp, flask, fastapi, aio_pika)
- :doc:`builtin parameter validation <pjrpc/validation>`
- :doc:`pytest integration <pjrpc/testing>`
- :doc:`openapi schema generation support <pjrpc/specification>`
Expand All @@ -50,6 +50,7 @@ Extra requirements
- `aiohttp <https://aiohttp.readthedocs.io>`_
- `aio_pika <https://aio-pika.readthedocs.io>`_
- `flask <https://flask.palletsprojects.com>`_
- `fastapi <https://fastapi.tiangolo.com>`_
- `pydantic <https://pydantic-docs.helpmanual.io/>`_
- `requests <https://requests.readthedocs.io>`_
- `httpx <https://www.python-httpx.org/>`_
Expand Down
14 changes: 14 additions & 0 deletions docs/source/pjrpc/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ httpserver
:language: python


fastapi server
----------

.. literalinclude:: ../../../examples/fastapi_server.py
:language: python


middlewares
-----------

Expand Down Expand Up @@ -163,3 +170,10 @@ aiohttp OpenAPI specification

.. literalinclude:: ../../../examples/openapi_aiohttp.py
:language: python


fastapi OpenAPI specification
-----------------------------

.. literalinclude:: ../../../examples/openapi_fastapi.py
:language: python
3 changes: 2 additions & 1 deletion docs/source/pjrpc/server.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ Server


``pjrpc`` supports popular backend frameworks like `aiohttp <https://aiohttp.readthedocs.io>`_,
`flask <https://flask.palletsprojects.com>`_ and message brokers like `aio_pika <https://aio-pika.readthedocs.io>`_.
`flask <https://flask.palletsprojects.com>`_, `FastAPI <https://fastapi.tiangolo.com>`_
and message brokers like `aio_pika <https://aio-pika.readthedocs.io>`_.


Running of aiohttp based JSON-RPC server is a very simple process. Just define methods, add them to the
Expand Down
43 changes: 43 additions & 0 deletions examples/fastapi_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import logging

import fastapi

import pjrpc.server
from pjrpc.server.integration import fastapi as integration

methods = pjrpc.server.MethodRegistry()


@methods.add()
async def sum(a: int, b: int) -> int:
return a + b


@methods.add()
async def sub(a: int, b: int) -> int:
return a - b


@methods.add()
async def div(a: int, b: int) -> float:
return a / b


@methods.add()
async def mult(a: int, b: int) -> int:
return a * b


@methods.add()
async def ping() -> None:
logging.info("ping")


jsonrpc_app = integration.Application('/api/v1')
jsonrpc_app.add_methods(methods)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it worth to define a fastapi app variable like:

app = jsonrpc_app.http_app

to be able to start it with fastapi dev or fastapi run command.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more useful example is

jsonrpc_app = integration.Application('/api/v1')
jsonrpc_app.add_methods(methods)

app = fastapi.FastAPI()
app.mount("/rpc", jsonrpc_app.http_app)

The same example is described in openapi + fastapi docs

app = fastapi.FastAPI()
app.mount("/rpc", jsonrpc_app.http_app)

if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
170 changes: 170 additions & 0 deletions examples/openapi_fastapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import uuid
from typing import Annotated, Any, Mapping

import fastapi
import pydantic as pd

import pjrpc.server.specs.extractors.pydantic
from pjrpc.server.integration import fastapi as integration
from pjrpc.server.specs import extractors, openapi
from pjrpc.server.specs.openapi import ui
from pjrpc.server.validators import pydantic as validators

methods = pjrpc.server.MethodRegistry(
validator_factory=validators.PydanticValidatorFactory(exclude=integration.is_fastapi_request),
metadata_processors=[
openapi.MethodSpecificationGenerator(
extractor=extractors.pydantic.PydanticMethodInfoExtractor(
exclude=integration.is_fastapi_request,
),
),
],
)

credentials = {"admin": "admin"}


UserName = Annotated[
str,
pd.Field(description="User name", examples=["John"]),
]

UserSurname = Annotated[
str,
pd.Field(description="User surname", examples=["Doe"]),
]

UserAge = Annotated[
int,
pd.Field(description="User age", examples=[25]),
]

UserId = Annotated[
uuid.UUID,
pd.Field(description="User identifier", examples=["08b02cf9-8e07-4d06-b569-2c24309c1dc1"]),
]


class UserIn(pd.BaseModel, title="User data"):
"""
User registration data.
"""

name: UserName
surname: UserSurname
age: UserAge


class UserOut(UserIn, title="User data"):
"""
Registered user data.
"""

id: UserId


class AlreadyExistsError(pjrpc.server.exceptions.TypedError):
"""
User already registered error.
"""

CODE = 2001
MESSAGE = "user already exists"


class NotFoundError(pjrpc.server.exceptions.TypedError):
"""
User not found error.
"""

CODE = 2002
MESSAGE = "user not found"


@methods.add(
pass_context='request',
metadata=[
openapi.metadata(
summary="Creates a user",
tags=['users'],
errors=[AlreadyExistsError],
),
],
)
def add_user(request: fastapi.Request[Mapping[str, Any]], user: UserIn) -> UserOut:
"""
Creates a user.

:param request: http request

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

http request is not passed. Let's pass it with to make the example more fulfilling:

@methods.add(
    pass_context=True,
    metadata=[
        openapi.metadata(
            summary="Creates a user",
            tags=['users'],
            errors=[AlreadyExistsError],
        ),
    ],
)
def add_user(request: fastapi.Request[Mapping[str, Any]], user: UserIn) -> UserOut:
    """
    Creates a user.

    :param request: http request
    :param object user: user data
    :return object: registered user
    :raise AlreadyExistsError: user already exists
    """

:param object user: user data
:return object: registered user
:raise AlreadyExistsError: user already exists
"""


@methods.add(
pass_context='request',
metadata=[
openapi.metadata(
summary="Returns a user",
tags=['users'],
errors=[NotFoundError],
),
],
)
def get_user(request: fastapi.Request[Mapping[str, Any]], user_id: UserId) -> UserOut:
"""
Returns a user.

:param request: http request
:param object user_id: user id
:return object: registered user
:raise NotFoundError: user not found
"""


@methods.add(
pass_context='request',
metadata=[
openapi.metadata(
summary="Deletes a user",
tags=['users'],
errors=[NotFoundError],
deprecated=True,
),
],
)
def delete_user(request: fastapi.Request[Mapping[str, Any]], user_id: UserId) -> None:
"""
Deletes a user.

:param request: http request
:param object user_id: user id
:raise NotFoundError: user not found
"""


class JSONEncoder(pjrpc.server.JSONEncoder):
def default(self, o: Any) -> Any:
if isinstance(o, pd.BaseModel):
return o.model_dump()
if isinstance(o, uuid.UUID):
return str(o)

return super().default(o)


openapi_spec = openapi.OpenAPI(
info=openapi.Info(version="1.0.0", title="User storage"),
)

jsonrpc_app = integration.Application('/api/v1')
jsonrpc_app.add_methods(methods)

jsonrpc_app.add_spec(openapi_spec, path='openapi.json')
jsonrpc_app.add_spec_ui('swagger', ui.SwaggerUI(), spec_url='../openapi.json')
jsonrpc_app.add_spec_ui('redoc', ui.ReDoc(), spec_url='../openapi.json')


app = fastapi.FastAPI()
app.mount("/rpc", jsonrpc_app.http_app)
Loading
Loading