pjrpc


Namepjrpc JSON
Version 1.10.1 PyPI version JSON
download
home_pagehttps://github.com/dapper91/pjrpc
SummaryExtensible JSON-RPC library
upload_time2024-11-12 19:03:42
maintainerNone
docs_urlNone
authorDmitry Pershin
requires_python<4.0,>=3.9
licenseUnlicense
keywords json-rpc rpc jsonrpc-client jsonrpc-server requests aiohttp flask httpx aio-pika kombu openapi openrpc starlette django
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            =====
pjrpc
=====

.. image:: https://static.pepy.tech/personalized-badge/pjrpc?period=month&units=international_system&left_color=grey&right_color=orange&left_text=Downloads/month
    :target: https://pepy.tech/project/pjrpc
    :alt: Downloads/month
.. image:: https://github.com/dapper91/pjrpc/actions/workflows/test.yml/badge.svg?branch=master
    :target: https://github.com/dapper91/pjrpc/actions/workflows/test.yml
    :alt: Build status
.. image:: https://img.shields.io/pypi/l/pjrpc.svg
    :target: https://pypi.org/project/pjrpc
    :alt: License
.. image:: https://img.shields.io/pypi/pyversions/pjrpc.svg
    :target: https://pypi.org/project/pjrpc
    :alt: Supported Python versions
.. image:: https://codecov.io/gh/dapper91/pjrpc/branch/master/graph/badge.svg
    :target: https://codecov.io/gh/dapper91/pjrpc
    :alt: Code coverage
.. image:: https://readthedocs.org/projects/pjrpc/badge/?version=stable&style=flat
   :alt: ReadTheDocs status
   :target: https://pjrpc.readthedocs.io/en/stable/


``pjrpc`` is an extensible `JSON-RPC <https://www.jsonrpc.org>`_ client/server library with an intuitive interface
that can be easily extended and integrated in your project without writing a lot of boilerplate code.

Features:

- framework agnostic
- intuitive api
- extendability
- synchronous and asynchronous client backed
- synchronous and asynchronous server support
- popular frameworks integration
- builtin parameter validation
- pytest integration
- openapi schema generation support
- web ui support (SwaggerUI, RapiDoc, ReDoc)

Installation
------------

You can install pjrpc with pip:

.. code-block:: console

    $ pip install pjrpc


Extra requirements
------------------

- `aiohttp <https://aiohttp.readthedocs.io>`_
- `aio_pika <https://aio-pika.readthedocs.io>`_
- `flask <https://flask.palletsprojects.com>`_
- `jsonschema <https://python-jsonschema.readthedocs.io>`_
- `kombu <https://kombu.readthedocs.io/en/stable/>`_
- `pydantic <https://pydantic-docs.helpmanual.io/>`_
- `requests <https://requests.readthedocs.io>`_
- `httpx <https://www.python-httpx.org/>`_
- `openapi-ui-bundles <https://github.com/dapper91/python-openapi-ui-bundles>`_
- `starlette <https://www.starlette.io/>`_
- `django <https://www.djangoproject.com>`_


Documentation
-------------

Documentation is available at `Read the Docs <https://pjrpc.readthedocs.io>`_.


Quickstart
----------

Client requests
_______________

``pjrpc`` client interface is very simple and intuitive. Methods may be called by name, using proxy object
or by sending handmade ``pjrpc.common.Request`` class object. Notification requests can be made using
``pjrpc.client.AbstractClient.notify`` method or by sending a ``pjrpc.common.Request`` object without id.

.. code-block:: python

    import pjrpc
    from pjrpc.client.backend import requests as pjrpc_client


    client = pjrpc_client.Client('http://localhost/api/v1')

    response: pjrpc.Response = client.send(pjrpc.Request('sum', params=[1, 2], id=1))
    print(f"1 + 2 = {response.result}")

    result = client('sum', a=1, b=2)
    print(f"1 + 2 = {result}")

    result = client.proxy.sum(1, 2)
    print(f"1 + 2 = {result}")

    client.notify('tick')


Asynchronous client api looks pretty much the same:

.. code-block:: python

    import pjrpc
    from pjrpc.client.backend import aiohttp as pjrpc_client


    client = pjrpc_client.Client('http://localhost/api/v1')

    response = await client.send(pjrpc.Request('sum', params=[1, 2], id=1))
    print(f"1 + 2 = {response.result}")

    result = await client('sum', a=1, b=2)
    print(f"1 + 2 = {result}")

    result = await client.proxy.sum(1, 2)
    print(f"1 + 2 = {result}")

    await client.notify('tick')


Batch requests
______________

Batch requests also supported. You can build ``pjrpc.common.BatchRequest`` request by your hand and then send it to the
server. The result is a ``pjrpc.common.BatchResponse`` instance you can iterate over to get all the results or get
each one by index:

.. code-block:: python

    import pjrpc
    from pjrpc.client.backend import requests as pjrpc_client


    client = pjrpc_client.Client('http://localhost/api/v1')

    batch_response = await client.batch.send(pjrpc.BatchRequest(
        pjrpc.Request('sum', [2, 2], id=1),
        pjrpc.Request('sub', [2, 2], id=2),
        pjrpc.Request('div', [2, 2], id=3),
        pjrpc.Request('mult', [2, 2], id=4),
    ))
    print(f"2 + 2 = {batch_response[0].result}")
    print(f"2 - 2 = {batch_response[1].result}")
    print(f"2 / 2 = {batch_response[2].result}")
    print(f"2 * 2 = {batch_response[3].result}")


There are also several alternative approaches which are a syntactic sugar for the first one (note that the result
is not a ``pjrpc.common.BatchResponse`` object anymore but a tuple of "plain" method invocation results):

- using chain call notation:

.. code-block:: python

    result = await client.batch('sum', 2, 2)('sub', 2, 2)('div', 2, 2)('mult', 2, 2).call()
    print(f"2 + 2 = {result[0]}")
    print(f"2 - 2 = {result[1]}")
    print(f"2 / 2 = {result[2]}")
    print(f"2 * 2 = {result[3]}")


- using subscription operator:

.. code-block:: python

    result = await client.batch[
        ('sum', 2, 2),
        ('sub', 2, 2),
        ('div', 2, 2),
        ('mult', 2, 2),
    ]
    print(f"2 + 2 = {result[0]}")
    print(f"2 - 2 = {result[1]}")
    print(f"2 / 2 = {result[2]}")
    print(f"2 * 2 = {result[3]}")


- using proxy chain call:

.. code-block:: python

    result = await client.batch.proxy.sum(2, 2).sub(2, 2).div(2, 2).mult(2, 2).call()
    print(f"2 + 2 = {result[0]}")
    print(f"2 - 2 = {result[1]}")
    print(f"2 / 2 = {result[2]}")
    print(f"2 * 2 = {result[3]}")


Which one to use is up to you but be aware that if any of the requests returns an error the result of the other ones
will be lost. In such case the first approach can be used to iterate over all the responses and get the results of
the succeeded ones like this:

.. code-block:: python

    import pjrpc
    from pjrpc.client.backend import requests as pjrpc_client


    client = pjrpc_client.Client('http://localhost/api/v1')

    batch_response = client.batch.send(pjrpc.BatchRequest(
        pjrpc.Request('sum', [2, 2], id=1),
        pjrpc.Request('sub', [2, 2], id=2),
        pjrpc.Request('div', [2, 2], id=3),
        pjrpc.Request('mult', [2, 2], id=4),
    ))

    for response in batch_response:
        if response.is_success:
            print(response.result)
        else:
            print(response.error)


Batch notifications:

.. code-block:: python

    import pjrpc
    from pjrpc.client.backend import requests as pjrpc_client


    client = pjrpc_client.Client('http://localhost/api/v1')

    client.batch.notify('tick').notify('tack').notify('tick').notify('tack').call()



Server
______

``pjrpc`` supports popular backend frameworks like `aiohttp <https://aiohttp.readthedocs.io>`_,
`flask <https://flask.palletsprojects.com>`_ and message brokers like `kombu <https://kombu.readthedocs.io/en/stable/>`_
and `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
registry and run the server:

.. code-block:: python

    import uuid

    from aiohttp import web

    import pjrpc.server
    from pjrpc.server.integration import aiohttp

    methods = pjrpc.server.MethodRegistry()


    @methods.add(context='request')
    async def add_user(request: web.Request, user: dict):
        user_id = uuid.uuid4().hex
        request.app['users'][user_id] = user

        return {'id': user_id, **user}


    jsonrpc_app = aiohttp.Application('/api/v1')
    jsonrpc_app.dispatcher.add_methods(methods)
    jsonrpc_app.app['users'] = {}

    if __name__ == "__main__":
        web.run_app(jsonrpc_app.app, host='localhost', port=8080)


Parameter validation
____________________

Very often besides dumb method parameters validation it is necessary to implement more "deep" validation and provide
comprehensive errors description to clients. Fortunately ``pjrpc`` has builtin parameter validation based on
`pydantic <https://pydantic-docs.helpmanual.io/>`_ library which uses python type annotation for validation.
Look at the following example: all you need to annotate method parameters (or describe more complex types beforehand if
necessary). ``pjrpc`` will be validating method parameters and returning informative errors to clients.


.. code-block:: python

    import enum
    import uuid
    from typing import List

    import pydantic
    from aiohttp import web

    import pjrpc.server
    from pjrpc.server.validators import pydantic as validators
    from pjrpc.server.integration import aiohttp

    methods = pjrpc.server.MethodRegistry()
    validator = validators.PydanticValidator()


    class ContactType(enum.Enum):
        PHONE = 'phone'
        EMAIL = 'email'


    class Contact(pydantic.BaseModel):
        type: ContactType
        value: str


    class User(pydantic.BaseModel):
        name: str
        surname: str
        age: int
        contacts: List[Contact]


    @methods.add(context='request')
    @validator.validate
    async def add_user(request: web.Request, user: User):
        user_id = uuid.uuid4()
        request.app['users'][user_id] = user

        return {'id': user_id, **user.dict()}


    class JSONEncoder(pjrpc.server.JSONEncoder):

        def default(self, o):
            if isinstance(o, uuid.UUID):
                return o.hex
            if isinstance(o, enum.Enum):
                return o.value

            return super().default(o)


    jsonrpc_app = aiohttp.Application('/api/v1', json_encoder=JSONEncoder)
    jsonrpc_app.dispatcher.add_methods(methods)
    jsonrpc_app.app['users'] = {}

    if __name__ == "__main__":
        web.run_app(jsonrpc_app.app, host='localhost', port=8080)


Error handling
______________

``pjrpc`` implements all the errors listed in `protocol specification <https://www.jsonrpc.org/specification#error_object>`_
which can be found in ``pjrpc.common.exceptions`` module so that error handling is very simple and "pythonic-way":

.. code-block:: python

    import pjrpc
    from pjrpc.client.backend import requests as pjrpc_client

    client = pjrpc_client.Client('http://localhost/api/v1')

    try:
        result = client.proxy.sum(1, 2)
    except pjrpc.MethodNotFound as e:
        print(e)


Default error list can be easily extended. All you need to create an error class inherited from
``pjrpc.exc.JsonRpcError`` and define an error code and a description message. ``pjrpc`` will be automatically
deserializing custom errors for you:

.. code-block:: python

    import pjrpc
    from pjrpc.client.backend import requests as pjrpc_client

    class UserNotFound(pjrpc.exc.JsonRpcError):
        code = 1
        message = 'user not found'


    client = pjrpc_client.Client('http://localhost/api/v1')

    try:
        result = client.proxy.get_user(user_id=1)
    except UserNotFound as e:
        print(e)


On the server side everything is also pretty straightforward:

.. code-block:: python

    import uuid

    import flask

    import pjrpc
    from pjrpc.server import MethodRegistry
    from pjrpc.server.integration import flask as integration

    app = flask.Flask(__name__)

    methods = pjrpc.server.MethodRegistry()


    class UserNotFound(pjrpc.exc.JsonRpcError):
        code = 1
        message = 'user not found'


    @methods.add
    def add_user(user: dict):
        user_id = uuid.uuid4().hex
        flask.current_app.users[user_id] = user

        return {'id': user_id, **user}

    @methods.add
     def get_user(self, user_id: str):
        user = flask.current_app.users.get(user_id)
        if not user:
            raise UserNotFound(data=user_id)

        return user


    json_rpc = integration.JsonRPC('/api/v1')
    json_rpc.dispatcher.add_methods(methods)

    app.users = {}

    json_rpc.init_app(app)

    if __name__ == "__main__":
        app.run(port=80)



Open API specification
______________________

``pjrpc`` has built-in `OpenAPI <https://swagger.io/specification/>`_ and `OpenRPC <https://spec.open-rpc.org/#introduction>`_
specification generation support and integrated web UI as an extra dependency. Three UI types are supported:

- SwaggerUI (`<https://swagger.io/tools/swagger-ui/>`_)
- RapiDoc (`<https://mrin9.github.io/RapiDoc/>`_)
- ReDoc (`<https://github.com/Redocly/redoc>`_)

Web UI extra dependency can be installed using the following code:

.. code-block:: console

    $ pip install pjrpc[openapi-ui-bundles]

The following example illustrates how to configure OpenAPI specification generation
and Swagger UI web tool with basic auth:

.. code-block:: python

    import uuid
    from typing import Annotated, Any, Optional

    import flask
    import flask_cors
    import flask_httpauth
    import pydantic as pd
    from werkzeug import security

    import pjrpc.server.specs.extractors.pydantic
    from pjrpc.server.integration import flask as integration
    from pjrpc.server.specs import extractors
    from pjrpc.server.specs import openapi as specs
    from pjrpc.server.validators import pydantic as validators

    app = flask.Flask('myapp')
    flask_cors.CORS(app, resources={"/myapp/api/v1/*": {"origins": "*"}})

    methods = pjrpc.server.MethodRegistry()
    validator = validators.PydanticValidator()

    auth = flask_httpauth.HTTPBasicAuth()
    credentials = {"admin": security.generate_password_hash("admin")}


    @auth.verify_password
    def verify_password(username: str, password: str) -> Optional[str]:
        if username in credentials and security.check_password_hash(credentials.get(username), password):
            return username


    class AuthenticatedJsonRPC(integration.JsonRPC):
        @auth.login_required
        def _rpc_handle(self, dispatcher: pjrpc.server.Dispatcher) -> flask.Response:
            return super()._rpc_handle(dispatcher)


    class JSONEncoder(pjrpc.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)


    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=["c47726c6-a232-45f1-944f-60b98966ff1b"]),
    ]


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

        name: UserName
        surname: UserSurname
        age: UserAge


    class UserOut(UserIn):
        """
        Registered user data.
        """

        id: UserId


    class AlreadyExistsError(pjrpc.exc.JsonRpcError):
        """
        User already registered error.
        """

        code = 2001
        message = "user already exists"


    class NotFoundError(pjrpc.exc.JsonRpcError):
        """
        User not found error.
        """

        code = 2002
        message = "user not found"


    @specs.annotate(
        summary='Creates a user',
        tags=['users'],
        errors=[AlreadyExistsError],
    )
    @methods.add
    @validator.validate
    def add_user(user: UserIn) -> UserOut:
        """
        Creates a user.

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

        for existing_user in flask.current_app.users_db.values():
            if user.name == existing_user.name:
                raise AlreadyExistsError()

        user_id = uuid.uuid4().hex
        flask.current_app.users_db[user_id] = user

        return UserOut(id=user_id, **user.model_dump())


    @specs.annotate(
        summary='Returns a user',
        tags=['users'],
        errors=[NotFoundError],
    )
    @methods.add
    @validator.validate
    def get_user(user_id: UserId) -> UserOut:
        """
        Returns a user.

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

        user = flask.current_app.users_db.get(user_id.hex)
        if not user:
            raise NotFoundError()

        return UserOut(id=user_id, **user.model_dump())


    @specs.annotate(
        summary='Deletes a user',
        tags=['users'],
        errors=[NotFoundError],
    )
    @methods.add
    @validator.validate
    def delete_user(user_id: UserId) -> None:
        """
        Deletes a user.

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

        user = flask.current_app.users_db.pop(user_id.hex, None)
        if not user:
            raise NotFoundError()


    json_rpc = AuthenticatedJsonRPC(
        '/api/v1',
        json_encoder=JSONEncoder,
        spec=specs.OpenAPI(
            info=specs.Info(version="1.0.0", title="User storage"),
            servers=[
                specs.Server(
                    url='http://127.0.0.1:8080',
                ),
            ],
            security_schemes=dict(
                basicAuth=specs.SecurityScheme(
                    type=specs.SecuritySchemeType.HTTP,
                    scheme='basic',
                ),
            ),
            security=[
                dict(basicAuth=[]),
            ],
            schema_extractor=extractors.pydantic.PydanticSchemaExtractor(),
            ui=specs.SwaggerUI(),
        ),
    )
    json_rpc.dispatcher.add_methods(methods)

    app.users_db = {}

    myapp = flask.Blueprint('myapp', __name__, url_prefix='/myapp')
    json_rpc.init_app(myapp)

    app.register_blueprint(myapp)

    if __name__ == "__main__":
        app.run(port=8080)


Specification is available on http://localhost:8080/myapp/api/v1/openapi.json

Web UI is running on http://localhost:8080/myapp/api/v1/ui/

Swagger UI:
~~~~~~~~~~~

.. image:: docs/source/_static/swagger-ui-screenshot.png
  :width: 1024
  :alt: Open API full example

RapiDoc:
~~~~~~~~

.. image:: docs/source/_static/rapidoc-screenshot.png
  :width: 1024
  :alt: Open API cli example

Redoc:
~~~~~~

.. image:: docs/source/_static/redoc-screenshot.png
  :width: 1024
  :alt: Open API method example

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/dapper91/pjrpc",
    "name": "pjrpc",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4.0,>=3.9",
    "maintainer_email": null,
    "keywords": "json-rpc, rpc, jsonrpc-client, jsonrpc-server, requests, aiohttp, flask, httpx, aio-pika, kombu, openapi, openrpc, starlette, django",
    "author": "Dmitry Pershin",
    "author_email": "dapper1291@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/b2/38/c1d68d1b2056bd30d1e7cd3436144717d9880121769b2398381033a9071d/pjrpc-1.10.1.tar.gz",
    "platform": null,
    "description": "=====\npjrpc\n=====\n\n.. image:: https://static.pepy.tech/personalized-badge/pjrpc?period=month&units=international_system&left_color=grey&right_color=orange&left_text=Downloads/month\n    :target: https://pepy.tech/project/pjrpc\n    :alt: Downloads/month\n.. image:: https://github.com/dapper91/pjrpc/actions/workflows/test.yml/badge.svg?branch=master\n    :target: https://github.com/dapper91/pjrpc/actions/workflows/test.yml\n    :alt: Build status\n.. image:: https://img.shields.io/pypi/l/pjrpc.svg\n    :target: https://pypi.org/project/pjrpc\n    :alt: License\n.. image:: https://img.shields.io/pypi/pyversions/pjrpc.svg\n    :target: https://pypi.org/project/pjrpc\n    :alt: Supported Python versions\n.. image:: https://codecov.io/gh/dapper91/pjrpc/branch/master/graph/badge.svg\n    :target: https://codecov.io/gh/dapper91/pjrpc\n    :alt: Code coverage\n.. image:: https://readthedocs.org/projects/pjrpc/badge/?version=stable&style=flat\n   :alt: ReadTheDocs status\n   :target: https://pjrpc.readthedocs.io/en/stable/\n\n\n``pjrpc`` is an extensible `JSON-RPC <https://www.jsonrpc.org>`_ client/server library with an intuitive interface\nthat can be easily extended and integrated in your project without writing a lot of boilerplate code.\n\nFeatures:\n\n- framework agnostic\n- intuitive api\n- extendability\n- synchronous and asynchronous client backed\n- synchronous and asynchronous server support\n- popular frameworks integration\n- builtin parameter validation\n- pytest integration\n- openapi schema generation support\n- web ui support (SwaggerUI, RapiDoc, ReDoc)\n\nInstallation\n------------\n\nYou can install pjrpc with pip:\n\n.. code-block:: console\n\n    $ pip install pjrpc\n\n\nExtra requirements\n------------------\n\n- `aiohttp <https://aiohttp.readthedocs.io>`_\n- `aio_pika <https://aio-pika.readthedocs.io>`_\n- `flask <https://flask.palletsprojects.com>`_\n- `jsonschema <https://python-jsonschema.readthedocs.io>`_\n- `kombu <https://kombu.readthedocs.io/en/stable/>`_\n- `pydantic <https://pydantic-docs.helpmanual.io/>`_\n- `requests <https://requests.readthedocs.io>`_\n- `httpx <https://www.python-httpx.org/>`_\n- `openapi-ui-bundles <https://github.com/dapper91/python-openapi-ui-bundles>`_\n- `starlette <https://www.starlette.io/>`_\n- `django <https://www.djangoproject.com>`_\n\n\nDocumentation\n-------------\n\nDocumentation is available at `Read the Docs <https://pjrpc.readthedocs.io>`_.\n\n\nQuickstart\n----------\n\nClient requests\n_______________\n\n``pjrpc`` client interface is very simple and intuitive. Methods may be called by name, using proxy object\nor by sending handmade ``pjrpc.common.Request`` class object. Notification requests can be made using\n``pjrpc.client.AbstractClient.notify`` method or by sending a ``pjrpc.common.Request`` object without id.\n\n.. code-block:: python\n\n    import pjrpc\n    from pjrpc.client.backend import requests as pjrpc_client\n\n\n    client = pjrpc_client.Client('http://localhost/api/v1')\n\n    response: pjrpc.Response = client.send(pjrpc.Request('sum', params=[1, 2], id=1))\n    print(f\"1 + 2 = {response.result}\")\n\n    result = client('sum', a=1, b=2)\n    print(f\"1 + 2 = {result}\")\n\n    result = client.proxy.sum(1, 2)\n    print(f\"1 + 2 = {result}\")\n\n    client.notify('tick')\n\n\nAsynchronous client api looks pretty much the same:\n\n.. code-block:: python\n\n    import pjrpc\n    from pjrpc.client.backend import aiohttp as pjrpc_client\n\n\n    client = pjrpc_client.Client('http://localhost/api/v1')\n\n    response = await client.send(pjrpc.Request('sum', params=[1, 2], id=1))\n    print(f\"1 + 2 = {response.result}\")\n\n    result = await client('sum', a=1, b=2)\n    print(f\"1 + 2 = {result}\")\n\n    result = await client.proxy.sum(1, 2)\n    print(f\"1 + 2 = {result}\")\n\n    await client.notify('tick')\n\n\nBatch requests\n______________\n\nBatch requests also supported. You can build ``pjrpc.common.BatchRequest`` request by your hand and then send it to the\nserver. The result is a ``pjrpc.common.BatchResponse`` instance you can iterate over to get all the results or get\neach one by index:\n\n.. code-block:: python\n\n    import pjrpc\n    from pjrpc.client.backend import requests as pjrpc_client\n\n\n    client = pjrpc_client.Client('http://localhost/api/v1')\n\n    batch_response = await client.batch.send(pjrpc.BatchRequest(\n        pjrpc.Request('sum', [2, 2], id=1),\n        pjrpc.Request('sub', [2, 2], id=2),\n        pjrpc.Request('div', [2, 2], id=3),\n        pjrpc.Request('mult', [2, 2], id=4),\n    ))\n    print(f\"2 + 2 = {batch_response[0].result}\")\n    print(f\"2 - 2 = {batch_response[1].result}\")\n    print(f\"2 / 2 = {batch_response[2].result}\")\n    print(f\"2 * 2 = {batch_response[3].result}\")\n\n\nThere are also several alternative approaches which are a syntactic sugar for the first one (note that the result\nis not a ``pjrpc.common.BatchResponse`` object anymore but a tuple of \"plain\" method invocation results):\n\n- using chain call notation:\n\n.. code-block:: python\n\n    result = await client.batch('sum', 2, 2)('sub', 2, 2)('div', 2, 2)('mult', 2, 2).call()\n    print(f\"2 + 2 = {result[0]}\")\n    print(f\"2 - 2 = {result[1]}\")\n    print(f\"2 / 2 = {result[2]}\")\n    print(f\"2 * 2 = {result[3]}\")\n\n\n- using subscription operator:\n\n.. code-block:: python\n\n    result = await client.batch[\n        ('sum', 2, 2),\n        ('sub', 2, 2),\n        ('div', 2, 2),\n        ('mult', 2, 2),\n    ]\n    print(f\"2 + 2 = {result[0]}\")\n    print(f\"2 - 2 = {result[1]}\")\n    print(f\"2 / 2 = {result[2]}\")\n    print(f\"2 * 2 = {result[3]}\")\n\n\n- using proxy chain call:\n\n.. code-block:: python\n\n    result = await client.batch.proxy.sum(2, 2).sub(2, 2).div(2, 2).mult(2, 2).call()\n    print(f\"2 + 2 = {result[0]}\")\n    print(f\"2 - 2 = {result[1]}\")\n    print(f\"2 / 2 = {result[2]}\")\n    print(f\"2 * 2 = {result[3]}\")\n\n\nWhich one to use is up to you but be aware that if any of the requests returns an error the result of the other ones\nwill be lost. In such case the first approach can be used to iterate over all the responses and get the results of\nthe succeeded ones like this:\n\n.. code-block:: python\n\n    import pjrpc\n    from pjrpc.client.backend import requests as pjrpc_client\n\n\n    client = pjrpc_client.Client('http://localhost/api/v1')\n\n    batch_response = client.batch.send(pjrpc.BatchRequest(\n        pjrpc.Request('sum', [2, 2], id=1),\n        pjrpc.Request('sub', [2, 2], id=2),\n        pjrpc.Request('div', [2, 2], id=3),\n        pjrpc.Request('mult', [2, 2], id=4),\n    ))\n\n    for response in batch_response:\n        if response.is_success:\n            print(response.result)\n        else:\n            print(response.error)\n\n\nBatch notifications:\n\n.. code-block:: python\n\n    import pjrpc\n    from pjrpc.client.backend import requests as pjrpc_client\n\n\n    client = pjrpc_client.Client('http://localhost/api/v1')\n\n    client.batch.notify('tick').notify('tack').notify('tick').notify('tack').call()\n\n\n\nServer\n______\n\n``pjrpc`` supports popular backend frameworks like `aiohttp <https://aiohttp.readthedocs.io>`_,\n`flask <https://flask.palletsprojects.com>`_ and message brokers like `kombu <https://kombu.readthedocs.io/en/stable/>`_\nand `aio_pika <https://aio-pika.readthedocs.io>`_.\n\n\nRunning of aiohttp based JSON-RPC server is a very simple process. Just define methods, add them to the\nregistry and run the server:\n\n.. code-block:: python\n\n    import uuid\n\n    from aiohttp import web\n\n    import pjrpc.server\n    from pjrpc.server.integration import aiohttp\n\n    methods = pjrpc.server.MethodRegistry()\n\n\n    @methods.add(context='request')\n    async def add_user(request: web.Request, user: dict):\n        user_id = uuid.uuid4().hex\n        request.app['users'][user_id] = user\n\n        return {'id': user_id, **user}\n\n\n    jsonrpc_app = aiohttp.Application('/api/v1')\n    jsonrpc_app.dispatcher.add_methods(methods)\n    jsonrpc_app.app['users'] = {}\n\n    if __name__ == \"__main__\":\n        web.run_app(jsonrpc_app.app, host='localhost', port=8080)\n\n\nParameter validation\n____________________\n\nVery often besides dumb method parameters validation it is necessary to implement more \"deep\" validation and provide\ncomprehensive errors description to clients. Fortunately ``pjrpc`` has builtin parameter validation based on\n`pydantic <https://pydantic-docs.helpmanual.io/>`_ library which uses python type annotation for validation.\nLook at the following example: all you need to annotate method parameters (or describe more complex types beforehand if\nnecessary). ``pjrpc`` will be validating method parameters and returning informative errors to clients.\n\n\n.. code-block:: python\n\n    import enum\n    import uuid\n    from typing import List\n\n    import pydantic\n    from aiohttp import web\n\n    import pjrpc.server\n    from pjrpc.server.validators import pydantic as validators\n    from pjrpc.server.integration import aiohttp\n\n    methods = pjrpc.server.MethodRegistry()\n    validator = validators.PydanticValidator()\n\n\n    class ContactType(enum.Enum):\n        PHONE = 'phone'\n        EMAIL = 'email'\n\n\n    class Contact(pydantic.BaseModel):\n        type: ContactType\n        value: str\n\n\n    class User(pydantic.BaseModel):\n        name: str\n        surname: str\n        age: int\n        contacts: List[Contact]\n\n\n    @methods.add(context='request')\n    @validator.validate\n    async def add_user(request: web.Request, user: User):\n        user_id = uuid.uuid4()\n        request.app['users'][user_id] = user\n\n        return {'id': user_id, **user.dict()}\n\n\n    class JSONEncoder(pjrpc.server.JSONEncoder):\n\n        def default(self, o):\n            if isinstance(o, uuid.UUID):\n                return o.hex\n            if isinstance(o, enum.Enum):\n                return o.value\n\n            return super().default(o)\n\n\n    jsonrpc_app = aiohttp.Application('/api/v1', json_encoder=JSONEncoder)\n    jsonrpc_app.dispatcher.add_methods(methods)\n    jsonrpc_app.app['users'] = {}\n\n    if __name__ == \"__main__\":\n        web.run_app(jsonrpc_app.app, host='localhost', port=8080)\n\n\nError handling\n______________\n\n``pjrpc`` implements all the errors listed in `protocol specification <https://www.jsonrpc.org/specification#error_object>`_\nwhich can be found in ``pjrpc.common.exceptions`` module so that error handling is very simple and \"pythonic-way\":\n\n.. code-block:: python\n\n    import pjrpc\n    from pjrpc.client.backend import requests as pjrpc_client\n\n    client = pjrpc_client.Client('http://localhost/api/v1')\n\n    try:\n        result = client.proxy.sum(1, 2)\n    except pjrpc.MethodNotFound as e:\n        print(e)\n\n\nDefault error list can be easily extended. All you need to create an error class inherited from\n``pjrpc.exc.JsonRpcError`` and define an error code and a description message. ``pjrpc`` will be automatically\ndeserializing custom errors for you:\n\n.. code-block:: python\n\n    import pjrpc\n    from pjrpc.client.backend import requests as pjrpc_client\n\n    class UserNotFound(pjrpc.exc.JsonRpcError):\n        code = 1\n        message = 'user not found'\n\n\n    client = pjrpc_client.Client('http://localhost/api/v1')\n\n    try:\n        result = client.proxy.get_user(user_id=1)\n    except UserNotFound as e:\n        print(e)\n\n\nOn the server side everything is also pretty straightforward:\n\n.. code-block:: python\n\n    import uuid\n\n    import flask\n\n    import pjrpc\n    from pjrpc.server import MethodRegistry\n    from pjrpc.server.integration import flask as integration\n\n    app = flask.Flask(__name__)\n\n    methods = pjrpc.server.MethodRegistry()\n\n\n    class UserNotFound(pjrpc.exc.JsonRpcError):\n        code = 1\n        message = 'user not found'\n\n\n    @methods.add\n    def add_user(user: dict):\n        user_id = uuid.uuid4().hex\n        flask.current_app.users[user_id] = user\n\n        return {'id': user_id, **user}\n\n    @methods.add\n     def get_user(self, user_id: str):\n        user = flask.current_app.users.get(user_id)\n        if not user:\n            raise UserNotFound(data=user_id)\n\n        return user\n\n\n    json_rpc = integration.JsonRPC('/api/v1')\n    json_rpc.dispatcher.add_methods(methods)\n\n    app.users = {}\n\n    json_rpc.init_app(app)\n\n    if __name__ == \"__main__\":\n        app.run(port=80)\n\n\n\nOpen API specification\n______________________\n\n``pjrpc`` has built-in `OpenAPI <https://swagger.io/specification/>`_ and `OpenRPC <https://spec.open-rpc.org/#introduction>`_\nspecification generation support and integrated web UI as an extra dependency. Three UI types are supported:\n\n- SwaggerUI (`<https://swagger.io/tools/swagger-ui/>`_)\n- RapiDoc (`<https://mrin9.github.io/RapiDoc/>`_)\n- ReDoc (`<https://github.com/Redocly/redoc>`_)\n\nWeb UI extra dependency can be installed using the following code:\n\n.. code-block:: console\n\n    $ pip install pjrpc[openapi-ui-bundles]\n\nThe following example illustrates how to configure OpenAPI specification generation\nand Swagger UI web tool with basic auth:\n\n.. code-block:: python\n\n    import uuid\n    from typing import Annotated, Any, Optional\n\n    import flask\n    import flask_cors\n    import flask_httpauth\n    import pydantic as pd\n    from werkzeug import security\n\n    import pjrpc.server.specs.extractors.pydantic\n    from pjrpc.server.integration import flask as integration\n    from pjrpc.server.specs import extractors\n    from pjrpc.server.specs import openapi as specs\n    from pjrpc.server.validators import pydantic as validators\n\n    app = flask.Flask('myapp')\n    flask_cors.CORS(app, resources={\"/myapp/api/v1/*\": {\"origins\": \"*\"}})\n\n    methods = pjrpc.server.MethodRegistry()\n    validator = validators.PydanticValidator()\n\n    auth = flask_httpauth.HTTPBasicAuth()\n    credentials = {\"admin\": security.generate_password_hash(\"admin\")}\n\n\n    @auth.verify_password\n    def verify_password(username: str, password: str) -> Optional[str]:\n        if username in credentials and security.check_password_hash(credentials.get(username), password):\n            return username\n\n\n    class AuthenticatedJsonRPC(integration.JsonRPC):\n        @auth.login_required\n        def _rpc_handle(self, dispatcher: pjrpc.server.Dispatcher) -> flask.Response:\n            return super()._rpc_handle(dispatcher)\n\n\n    class JSONEncoder(pjrpc.JSONEncoder):\n        def default(self, o: Any) -> Any:\n            if isinstance(o, pd.BaseModel):\n                return o.model_dump()\n            if isinstance(o, uuid.UUID):\n                return str(o)\n\n            return super().default(o)\n\n\n    UserName = Annotated[\n        str,\n        pd.Field(description=\"User name\", examples=[\"John\"]),\n    ]\n\n    UserSurname = Annotated[\n        str,\n        pd.Field(description=\"User surname\", examples=['Doe']),\n    ]\n\n    UserAge = Annotated[\n        int,\n        pd.Field(description=\"User age\", examples=[25]),\n    ]\n\n    UserId = Annotated[\n        uuid.UUID,\n        pd.Field(description=\"User identifier\", examples=[\"c47726c6-a232-45f1-944f-60b98966ff1b\"]),\n    ]\n\n\n    class UserIn(pd.BaseModel):\n        \"\"\"\n        User registration data.\n        \"\"\"\n\n        name: UserName\n        surname: UserSurname\n        age: UserAge\n\n\n    class UserOut(UserIn):\n        \"\"\"\n        Registered user data.\n        \"\"\"\n\n        id: UserId\n\n\n    class AlreadyExistsError(pjrpc.exc.JsonRpcError):\n        \"\"\"\n        User already registered error.\n        \"\"\"\n\n        code = 2001\n        message = \"user already exists\"\n\n\n    class NotFoundError(pjrpc.exc.JsonRpcError):\n        \"\"\"\n        User not found error.\n        \"\"\"\n\n        code = 2002\n        message = \"user not found\"\n\n\n    @specs.annotate(\n        summary='Creates a user',\n        tags=['users'],\n        errors=[AlreadyExistsError],\n    )\n    @methods.add\n    @validator.validate\n    def add_user(user: UserIn) -> UserOut:\n        \"\"\"\n        Creates a user.\n\n        :param object user: user data\n        :return object: registered user\n        :raise AlreadyExistsError: user already exists\n        \"\"\"\n\n        for existing_user in flask.current_app.users_db.values():\n            if user.name == existing_user.name:\n                raise AlreadyExistsError()\n\n        user_id = uuid.uuid4().hex\n        flask.current_app.users_db[user_id] = user\n\n        return UserOut(id=user_id, **user.model_dump())\n\n\n    @specs.annotate(\n        summary='Returns a user',\n        tags=['users'],\n        errors=[NotFoundError],\n    )\n    @methods.add\n    @validator.validate\n    def get_user(user_id: UserId) -> UserOut:\n        \"\"\"\n        Returns a user.\n\n        :param object user_id: user id\n        :return object: registered user\n        :raise NotFoundError: user not found\n        \"\"\"\n\n        user = flask.current_app.users_db.get(user_id.hex)\n        if not user:\n            raise NotFoundError()\n\n        return UserOut(id=user_id, **user.model_dump())\n\n\n    @specs.annotate(\n        summary='Deletes a user',\n        tags=['users'],\n        errors=[NotFoundError],\n    )\n    @methods.add\n    @validator.validate\n    def delete_user(user_id: UserId) -> None:\n        \"\"\"\n        Deletes a user.\n\n        :param object user_id: user id\n        :raise NotFoundError: user not found\n        \"\"\"\n\n        user = flask.current_app.users_db.pop(user_id.hex, None)\n        if not user:\n            raise NotFoundError()\n\n\n    json_rpc = AuthenticatedJsonRPC(\n        '/api/v1',\n        json_encoder=JSONEncoder,\n        spec=specs.OpenAPI(\n            info=specs.Info(version=\"1.0.0\", title=\"User storage\"),\n            servers=[\n                specs.Server(\n                    url='http://127.0.0.1:8080',\n                ),\n            ],\n            security_schemes=dict(\n                basicAuth=specs.SecurityScheme(\n                    type=specs.SecuritySchemeType.HTTP,\n                    scheme='basic',\n                ),\n            ),\n            security=[\n                dict(basicAuth=[]),\n            ],\n            schema_extractor=extractors.pydantic.PydanticSchemaExtractor(),\n            ui=specs.SwaggerUI(),\n        ),\n    )\n    json_rpc.dispatcher.add_methods(methods)\n\n    app.users_db = {}\n\n    myapp = flask.Blueprint('myapp', __name__, url_prefix='/myapp')\n    json_rpc.init_app(myapp)\n\n    app.register_blueprint(myapp)\n\n    if __name__ == \"__main__\":\n        app.run(port=8080)\n\n\nSpecification is available on http://localhost:8080/myapp/api/v1/openapi.json\n\nWeb UI is running on http://localhost:8080/myapp/api/v1/ui/\n\nSwagger UI:\n~~~~~~~~~~~\n\n.. image:: docs/source/_static/swagger-ui-screenshot.png\n  :width: 1024\n  :alt: Open API full example\n\nRapiDoc:\n~~~~~~~~\n\n.. image:: docs/source/_static/rapidoc-screenshot.png\n  :width: 1024\n  :alt: Open API cli example\n\nRedoc:\n~~~~~~\n\n.. image:: docs/source/_static/redoc-screenshot.png\n  :width: 1024\n  :alt: Open API method example\n",
    "bugtrack_url": null,
    "license": "Unlicense",
    "summary": "Extensible JSON-RPC library",
    "version": "1.10.1",
    "project_urls": {
        "Documentation": "https://pjrpc.readthedocs.io",
        "Homepage": "https://github.com/dapper91/pjrpc",
        "Repository": "https://github.com/dapper91/pjrpc"
    },
    "split_keywords": [
        "json-rpc",
        " rpc",
        " jsonrpc-client",
        " jsonrpc-server",
        " requests",
        " aiohttp",
        " flask",
        " httpx",
        " aio-pika",
        " kombu",
        " openapi",
        " openrpc",
        " starlette",
        " django"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "05847368b4273f4ea4aa1eca05bcbaaa6b2b4c8d43936f732fd2bbe7c2f3bee5",
                "md5": "585352c0c709dae02a346013a8302a7c",
                "sha256": "862a45eddaf9607e1c2cf684b3782efaff547397030e0269833c55720a431592"
            },
            "downloads": -1,
            "filename": "pjrpc-1.10.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "585352c0c709dae02a346013a8302a7c",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4.0,>=3.9",
            "size": 73229,
            "upload_time": "2024-11-12T19:03:40",
            "upload_time_iso_8601": "2024-11-12T19:03:40.972084Z",
            "url": "https://files.pythonhosted.org/packages/05/84/7368b4273f4ea4aa1eca05bcbaaa6b2b4c8d43936f732fd2bbe7c2f3bee5/pjrpc-1.10.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "b238c1d68d1b2056bd30d1e7cd3436144717d9880121769b2398381033a9071d",
                "md5": "699d9f4c2881f15d6f33adf395e1975d",
                "sha256": "17408b9bb19832dd980746f688029074e559259d42e4a5a287641514795e2260"
            },
            "downloads": -1,
            "filename": "pjrpc-1.10.1.tar.gz",
            "has_sig": false,
            "md5_digest": "699d9f4c2881f15d6f33adf395e1975d",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4.0,>=3.9",
            "size": 56830,
            "upload_time": "2024-11-12T19:03:42",
            "upload_time_iso_8601": "2024-11-12T19:03:42.961274Z",
            "url": "https://files.pythonhosted.org/packages/b2/38/c1d68d1b2056bd30d1e7cd3436144717d9880121769b2398381033a9071d/pjrpc-1.10.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-11-12 19:03:42",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "dapper91",
    "github_project": "pjrpc",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "pjrpc"
}
        
Elapsed time: 0.58129s