jsonrpcx


Namejsonrpcx JSON
Version 4.1.0 PyPI version JSON
download
home_pagehttps://codeberg.org/_laphilipa/jsonrpcx
SummaryA battle tested Python JSON-RPC2.0 library supporting client and server code in sync and async fashion.
upload_time2023-05-07 08:05:32
maintainer
docs_urlNone
author
requires_python
licenseISC
keywords json jsonrpc rpc
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Overview

A JSON-RPC 2.0 implementation.

Features:
- **Experimental support for request canceling with `rpc.cancel(id)` in async servers**
- **Experimental support for response streaming in the ASGI server**
- **Experimental support for MQTT Transport**
- Supports calling JSON-RCP 2.0 servers
- Build a JSON-RPC server using ASGI, WSGI and CGI
- Optionally supports async client and server code
- Use the JSON-RPC 2.0 with any transport layer by using building blocks
- Battle tested code because it was alreay used ~10 years in varioius closed source projects
- Unittests are written for new code and whenever a bug is fixed for older parts
- Abstracts handling of datetime.datetime() over the RPC
- Because this is a serious library a GPG signature is uploaded to PIP so the integrity of updates can be easily verified.
- Plays nice with OpenRPC (https://open-rpc.org/)

Drawbacks:
- Depnends on libraries to support async and correctly convert Pythons datatime.datetime() to JSON and to ensure timezone information is not swallowed
- This library is commited not to brake existing code so it can get bloated over time

Features under construction:
_Note: For features under construction it is open if they will arrive as a separate package or merged in to this one_
- Experimental code for OpenRPC is already included and will get more major over time. Several closed source applications already rely on it.
- A JSON-RPC 2.0 over MQTT is under consideration

# Install

`pip install jsonrpcx`

A GPG signature is available at PIP that can be checked as well.

# Build clients

## Async

_Note: `acall` does not support a session parameter because it could lead to hard to debug code.
If for example you have two paralel requests one is logging you in to the server and sets a cookie and the other is calling a method where it already needs to be logged in
Then it can happen that sometimes it works while other times the login happens after the first request to a resource that requires a login thus withuot the login.
In the last case the server would return some kind of error message and the develper with a headage.
If you want to use async requests please only do so with stateless RPC servers or use the sync version instead._
Note: There currenlty is code to support sessions for `acall` but that will be removed in a future version unless a way to circumvent the problem described above is found.

```python
from jsonrpcx import acall
import asyncio

async def main():
    result1 = await acall("https://yourjsonrpc.online", method="add", params=[1, 2])
    print(result1)

    result2 = await acall("https://yourjsonrpc.online", method="add", params={a: 1, b: 2})
    print(result2)

if __name__ == "__main__":
    asyncio.run(main())

```

If you don't want to write the URL over and over again you can do this trick
```python
from jsonrpcx import acall
import asyncio
from functools import partial

async def main():
    rpc = partial(acall "https://yourjsonrpc.online")
    result = await rpc("add", [2, 2])
    print(result)

if __name__ == "__main__":
    asyncio.run(main())
```


## Sync

### Call RPC via string

```python
from jsonrpcx import call
import https

result1 = call("https://yourjsonrpc.online", method="add", params=[1, 2])
print(result1)

result2 = call("https://yourjsonrpc.online", method="add", params={a: 1, b: 2})
print(result2)

# Generally you should keep JSON-RPC 2.0 servers state less that means not rely on sessions for stuff like authentication
# Sessions can be used here but should be threaded as an implementation detail and only used if absolutely necessary
# because it will only work as long as httpx is used under the hood
# If you do need a session you can do the following
session = httpx.Client()
result3 = call("https://yourjsonrpc.online", method="add", params=[1, 2], session=session)
print(result3)
```

If you don't want to write the URL over and over again you can do this trick
```python
from jsonrpcx import call
from functools import partial

rpc = partial(call, "https://yourjsonrpc.online")
result = rpc("add", [2, 2])
print(result)
```

### Call RPC via proxy object

For legacy reasons `ServiceProxy` exists and will automatically translate a method call on the ServiceProxy object to a method call on the RPC.
_Because this looks like normal Python code but static code analysis or auto complete can not support the developer here this way of calling an RPC has not made it to the newer async version._

```python
import jsonrpcx

# The service proxy keeps track of HTTP sessions
rpc = jsonrpcx.ServiceProxy("https://yourjsonrpc.online")
rpc.add(1, 2)
rpc.add(a=1, b=2)

# As a limitation of the JSON-RPC 2.0 protocol this is not possible and would usually trhow an Exception but is dependend on the implementation detail of the server
rpc.add(1, b=2)
```

# Build Servers

## ASGI

```python
import logging
from typing import *
from jsonrpcx import *

logging.basicConfig(level=logging.DEBUG)

class Service(ASGIServer):
    async def ping(self) -> str:
        return "pong"

    async def add(self, val1: float, val2: float) -> float:
        return val1 + val2

    async def echo(self, var: str) -> str:
        return var

    async def testReturnNone(self):
        return None

# You only need this if you want to overwrite the HTTP headers sent to the client
class Delegate(ASGIServerDelegate):
    def HTMLHeaders(self) -> List[str]:
        return [("Content-Type", "application/json")]

async def app(scope, receive, send):
    rpcServer = Service(delegate=Delegate())
    return await rpcServer.parseRequest(scope, receive, send)
```

## WSGI
_Note: WSGI does not support async. This is a limitation of WSGI itself not of this library._

```python
from typing import *
from jsonrpcx import *

class Service(WSGIServer):
    def ping(self) -> str:
        return "pong"

    def add(self, val1: int, val2: int) -> int:
        return val1 + val2

    def echo(self, var):
        return var

    def testReturnNone(self):
        return None

# You only need this if you want to overwrite the HTTP headers sent to the client
class Delegate(WSGIServerDelegate):
    def HTMLHeaders(self) -> List[str]:
        return [("Content-Type", "application/json")]

def app(environment, start_response):
    wsgiServer = Service(delegate=Delegate())
    return wsgiServer.parseRequest(environment, start_response)
```

## CGI
CGI is the perfect choice for hosting providers that do not support WSGI or ASGI.

Recomended configuration using Apache .htaccess as an example a similar configuration should work for other webservers.

```htaccess
    # If the webserver retuns some error your JSON-RPC 2.0 clients won't break even if they don't understand HTTP error codes which some don't
    ErrorDocument 500 '{"jsonrpc": "2.0", "error": {"code": -32603, "message": "Internal error issued by webserver"}, "id": null}'

    # Security precaution
    Options -Indexes

    DirectoryIndex index.py

    # Don't give hackes access to configuration secrets
    <Files ~ "\.pyc$">
        Order allow,deny
        Deny from all
    </Files>

    # Requied to make .py scripts executable
    Options +ExecCGI
    AddHandler cgi-script .py

    # Only if you intend to use the RPC from a browser.
    # Please check if you can use more strict CORS rules
    Header set Access-Control-Allow-Origin "*"
    Header set Access-Control-Allow-Headers "Accept,Content-Type,Cookie,Origin,Referer,User-Agent"
    Header set Access-Control-Allow-Methods "*"
    Header set Access-Control-Allow-Credentials true
```

### Async
```python
from jsonrpcx import *
import asyncio

class Service(AsyncCGIServer):
    # Will work becasue its marked with the `async` keyword`
    async def ping(self) -> str:
        return "pong"
    
    # This will not work because it is missing the `async` keyword
    def ping2(self) -> str:
        return "pong"


# You only need this if you want to overwrite the HTTP headers sent to the client
class Delegate(CGIServerDelegate):
    def HTTPHeader(self):
        return "Content-Type: application/json"

if __name__ == "__main__":
    service = Service(delegate=Delegate())
    asyncio.run(service.processRequests())
```

### Sync

```python
from jsonrpcx import *

class Service(CGIServer):
    def ping(self) -> str:
        return "pong"

# You only need this if you want to overwrite the HTTP headers sent to the client
class Delegate(CGIServerDelegate):
    def HTTPHeader(self):
        return "Content-Type: application/json"

if __name__ == "__main__":
    Service(delegate=Delegate())
```

# OpenRPC documentation
_Note: If you want to use this with web based tooling you need to configure CORS headers in a way to allow that._

This library already provieds experimental support for https://open-rpc.org/ which means you can auto generate documenation.
This library uses Pythons typing support to figure out what input types a function has.

If you point a tool that can generate documentation form the OpenRPC spec to your JSON-RPC 2.0 server it will already be able to generate documentation for it.
The method to generate the OpenRPC json document is `rpc.discover` if you want to overwrite its behaviour then you can overrde the method `def rpcDiscover(self):` or `async rpcDiscover(self):` depending on if you choose a sync or async JSON-RPC 2.0 server.

The `info` and `server` parts of the documentation return a example information by default. If you want to add your own information you can do it like so:

```python
from typing import *
from jsonrpcx import *

# Make sure to inherit from the correct class here depending on how you deploy the server
class Delegate(CGIServerDelegate):
    def experimentalOpenRPCInfo(self):
        return {
            "version": "1.0.0",
            "title": "Example Title",
            "termsOfService": "https://example.eu/tos",
            "contact": {
                "name": "Support Contact Name",
                "email": "support@example.eu",
                "url": "https://example.eu"
            },
            "description": "This API uses JSON-RPC 2.0. For simplicity it does not support batch method calls."
            }

    def experimentalOpenRPCServers(self) -> Optional[List[Dict]]:
        return [{
                "name": "Example endpoint",
                "url": "http://localhost:8000"
            }]
```

# Experimental support for request canceling

There is currently no definition for how request canceling should be implemented in JSON-RPC 2.0 but Microsoft has something like that in LSP and this works similar to that.

WARNING: WHEN ENABLING EXPERIMENTAL REQUEST CANCELING ANYONE CAN CANCEL REQUESTS EVEN REQUESTS FROM OTHER CLIENTS AS LONG AS THEY CORRECTL GUESS THE MESSAGEID.

The server supports a method `rpc.cancel(id)` where id is the messageId the client sent to the server.

```python
from jsonrpcx import *
import asyncio

someVar = []

async def cancel_me():
    print('cancel_me(): before sleep')

    try:
        # Wait for 1 hour
        someVar.append("example")
        await asyncio.sleep(3600)
    except asyncio.CancelledError:
        print('cancel_me(): cancel sleep')
        raise
    finally:
        print('cancel_me(): after sleep')


class Service(ASGIServer, ExperimentalCancelRequestMixin):
    async def getLongRunningProcess2(self, bla: str):   
        return await cancel_me()


class Delegate(ASGIServerDelegate):
    # You have these options to overwrite default behaviour (method signatures are subject to change):

    async def experimentalWaitOnCancelRequest(self) -> None:
        super.experimentalWaitOnCancelRequest()

    async def experimentalAddRunningRequest(self, messageId: Any, task: asyncio.Task) -> None:
        super.experimentalAddRunningRequest(messageId, task)

    async def experimentalRemoveRunningRequest(self, messageId: Any) -> None:
        super.experimentalRemoveRunningRequest(messageID)

```

# Experimental support for response streaming

There is currently no definition for how request canceling should be implemented in JSON-RPC 2.0.

On the Server:
```python
from jsonrpcx import *

class Service(ASGIServer, ExperimentalCancelRequestMixin):
    async def streamedResponse(self):
        # Any dict or list (json serializable) that is returned with the `yield` keyword will be streamed to the client
        for i in range(10):
            yield {"res": f"test {i=}"}
            await asyncio.sleep(5)

```

On the client:
```python
import httpx
import asyncio

async def main():
    client = httpx.AsyncClient()
    async with client.stream('POST', 'http://127.0.0.1:8000', timeout=5000, json={
        "jsonrpc": "2.0",
        "method": "streamedResponse",
        "params": [],
        "id": 1
    }) as response:
        async for chunk in response.aiter_bytes():
            print(chunk)
            
            
if __name__ == '__main__':
    asyncio.run(main())
```

# MQTT Transport

WARNING: MQTT Transport is currently EXPERIMENTAL and its interface is subject to change.
NOTE: To use MQTT Transport you need to install the package `asyncio_mqtt`.

Server:
```
from jsonrpcx import *

class API(MqttJSONRPCServer):
    async def ping(self):
        return "pong"
    
    async def add(self, a, b):
        return a + b


async def main():
    server = API("localhost", "user", "password", "mqttChannel")
    await server.processRequests()


if __name__ == "__main__":
    asyncio.run(main())
```

Client:
```
from jsonrpcx import *

async def main():
    res = await mqttRPCCall("add", [5, 5], mqttHost="host", mqttUser="user", mqttPassword="password", mqttPort=1883, mqttChannel="mqttChannel")
    print(res)

if __name__ == "__main__":
    asyncio.run(main())
```


# Setup development

Run `pipenv install`

# Testing
Run `pipenv run pytest`

# Running the Example server
```uvicorn exampleserver:app --reload```

# Publish

- `python3 setup.py bdist_wheel`
- `twine upload dist/* --skip-existing`
# Improvement ideas

- Instead of throwing CircularReference detected when what is returned from a function can not be serialized by json send a JSON serialization exception.
- Add middle wear support
- Add authentication middlewear
- Add stream support for websockets
- Improve secutiry problem with rpc.cancel (currently anyone can cancel any request as long as they know the messageId)
    - Force the client to use cryptographic secure `messageId`.
    - Let the server provide a crypographic secure `cancelToken` or JWT so only the client that made the request can canel that without the need to generate cryptographic secure messageId on the client which some clients might not be capable of.
    - Keep a dict which client requested which messageId and check against that before canceling a request somehow.
    - Let the middlewear take care of this.
    - Expose all required data into the delegate and let the developer handle this.

            

Raw data

            {
    "_id": null,
    "home_page": "https://codeberg.org/_laphilipa/jsonrpcx",
    "name": "jsonrpcx",
    "maintainer": "",
    "docs_url": null,
    "requires_python": "",
    "maintainer_email": "",
    "keywords": "JSON,jsonrpc,rpc",
    "author": "",
    "author_email": "",
    "download_url": "",
    "platform": null,
    "description": "# Overview\n\nA JSON-RPC 2.0 implementation.\n\nFeatures:\n- **Experimental support for request canceling with `rpc.cancel(id)` in async servers**\n- **Experimental support for response streaming in the ASGI server**\n- **Experimental support for MQTT Transport**\n- Supports calling JSON-RCP 2.0 servers\n- Build a JSON-RPC server using ASGI, WSGI and CGI\n- Optionally supports async client and server code\n- Use the JSON-RPC 2.0 with any transport layer by using building blocks\n- Battle tested code because it was alreay used ~10 years in varioius closed source projects\n- Unittests are written for new code and whenever a bug is fixed for older parts\n- Abstracts handling of datetime.datetime() over the RPC\n- Because this is a serious library a GPG signature is uploaded to PIP so the integrity of updates can be easily verified.\n- Plays nice with OpenRPC (https://open-rpc.org/)\n\nDrawbacks:\n- Depnends on libraries to support async and correctly convert Pythons datatime.datetime() to JSON and to ensure timezone information is not swallowed\n- This library is commited not to brake existing code so it can get bloated over time\n\nFeatures under construction:\n_Note: For features under construction it is open if they will arrive as a separate package or merged in to this one_\n- Experimental code for OpenRPC is already included and will get more major over time. Several closed source applications already rely on it.\n- A JSON-RPC 2.0 over MQTT is under consideration\n\n# Install\n\n`pip install jsonrpcx`\n\nA GPG signature is available at PIP that can be checked as well.\n\n# Build clients\n\n## Async\n\n_Note: `acall` does not support a session parameter because it could lead to hard to debug code.\nIf for example you have two paralel requests one is logging you in to the server and sets a cookie and the other is calling a method where it already needs to be logged in\nThen it can happen that sometimes it works while other times the login happens after the first request to a resource that requires a login thus withuot the login.\nIn the last case the server would return some kind of error message and the develper with a headage.\nIf you want to use async requests please only do so with stateless RPC servers or use the sync version instead._\nNote: There currenlty is code to support sessions for `acall` but that will be removed in a future version unless a way to circumvent the problem described above is found.\n\n```python\nfrom jsonrpcx import acall\nimport asyncio\n\nasync def main():\n    result1 = await acall(\"https://yourjsonrpc.online\", method=\"add\", params=[1, 2])\n    print(result1)\n\n    result2 = await acall(\"https://yourjsonrpc.online\", method=\"add\", params={a: 1, b: 2})\n    print(result2)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n\n```\n\nIf you don't want to write the URL over and over again you can do this trick\n```python\nfrom jsonrpcx import acall\nimport asyncio\nfrom functools import partial\n\nasync def main():\n    rpc = partial(acall \"https://yourjsonrpc.online\")\n    result = await rpc(\"add\", [2, 2])\n    print(result)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n\n## Sync\n\n### Call RPC via string\n\n```python\nfrom jsonrpcx import call\nimport https\n\nresult1 = call(\"https://yourjsonrpc.online\", method=\"add\", params=[1, 2])\nprint(result1)\n\nresult2 = call(\"https://yourjsonrpc.online\", method=\"add\", params={a: 1, b: 2})\nprint(result2)\n\n# Generally you should keep JSON-RPC 2.0 servers state less that means not rely on sessions for stuff like authentication\n# Sessions can be used here but should be threaded as an implementation detail and only used if absolutely necessary\n# because it will only work as long as httpx is used under the hood\n# If you do need a session you can do the following\nsession = httpx.Client()\nresult3 = call(\"https://yourjsonrpc.online\", method=\"add\", params=[1, 2], session=session)\nprint(result3)\n```\n\nIf you don't want to write the URL over and over again you can do this trick\n```python\nfrom jsonrpcx import call\nfrom functools import partial\n\nrpc = partial(call, \"https://yourjsonrpc.online\")\nresult = rpc(\"add\", [2, 2])\nprint(result)\n```\n\n### Call RPC via proxy object\n\nFor legacy reasons `ServiceProxy` exists and will automatically translate a method call on the ServiceProxy object to a method call on the RPC.\n_Because this looks like normal Python code but static code analysis or auto complete can not support the developer here this way of calling an RPC has not made it to the newer async version._\n\n```python\nimport jsonrpcx\n\n# The service proxy keeps track of HTTP sessions\nrpc = jsonrpcx.ServiceProxy(\"https://yourjsonrpc.online\")\nrpc.add(1, 2)\nrpc.add(a=1, b=2)\n\n# As a limitation of the JSON-RPC 2.0 protocol this is not possible and would usually trhow an Exception but is dependend on the implementation detail of the server\nrpc.add(1, b=2)\n```\n\n# Build Servers\n\n## ASGI\n\n```python\nimport logging\nfrom typing import *\nfrom jsonrpcx import *\n\nlogging.basicConfig(level=logging.DEBUG)\n\nclass Service(ASGIServer):\n    async def ping(self) -> str:\n        return \"pong\"\n\n    async def add(self, val1: float, val2: float) -> float:\n        return val1 + val2\n\n    async def echo(self, var: str) -> str:\n        return var\n\n    async def testReturnNone(self):\n        return None\n\n# You only need this if you want to overwrite the HTTP headers sent to the client\nclass Delegate(ASGIServerDelegate):\n    def HTMLHeaders(self) -> List[str]:\n        return [(\"Content-Type\", \"application/json\")]\n\nasync def app(scope, receive, send):\n    rpcServer = Service(delegate=Delegate())\n    return await rpcServer.parseRequest(scope, receive, send)\n```\n\n## WSGI\n_Note: WSGI does not support async. This is a limitation of WSGI itself not of this library._\n\n```python\nfrom typing import *\nfrom jsonrpcx import *\n\nclass Service(WSGIServer):\n    def ping(self) -> str:\n        return \"pong\"\n\n    def add(self, val1: int, val2: int) -> int:\n        return val1 + val2\n\n    def echo(self, var):\n        return var\n\n    def testReturnNone(self):\n        return None\n\n# You only need this if you want to overwrite the HTTP headers sent to the client\nclass Delegate(WSGIServerDelegate):\n    def HTMLHeaders(self) -> List[str]:\n        return [(\"Content-Type\", \"application/json\")]\n\ndef app(environment, start_response):\n    wsgiServer = Service(delegate=Delegate())\n    return wsgiServer.parseRequest(environment, start_response)\n```\n\n## CGI\nCGI is the perfect choice for hosting providers that do not support WSGI or ASGI.\n\nRecomended configuration using Apache .htaccess as an example a similar configuration should work for other webservers.\n\n```htaccess\n    # If the webserver retuns some error your JSON-RPC 2.0 clients won't break even if they don't understand HTTP error codes which some don't\n    ErrorDocument 500 '{\"jsonrpc\": \"2.0\", \"error\": {\"code\": -32603, \"message\": \"Internal error issued by webserver\"}, \"id\": null}'\n\n    # Security precaution\n    Options -Indexes\n\n    DirectoryIndex index.py\n\n    # Don't give hackes access to configuration secrets\n    <Files ~ \"\\.pyc$\">\n        Order allow,deny\n        Deny from all\n    </Files>\n\n    # Requied to make .py scripts executable\n    Options +ExecCGI\n    AddHandler cgi-script .py\n\n    # Only if you intend to use the RPC from a browser.\n    # Please check if you can use more strict CORS rules\n    Header set Access-Control-Allow-Origin \"*\"\n    Header set Access-Control-Allow-Headers \"Accept,Content-Type,Cookie,Origin,Referer,User-Agent\"\n    Header set Access-Control-Allow-Methods \"*\"\n    Header set Access-Control-Allow-Credentials true\n```\n\n### Async\n```python\nfrom jsonrpcx import *\nimport asyncio\n\nclass Service(AsyncCGIServer):\n    # Will work becasue its marked with the `async` keyword`\n    async def ping(self) -> str:\n        return \"pong\"\n    \n    # This will not work because it is missing the `async` keyword\n    def ping2(self) -> str:\n        return \"pong\"\n\n\n# You only need this if you want to overwrite the HTTP headers sent to the client\nclass Delegate(CGIServerDelegate):\n    def HTTPHeader(self):\n        return \"Content-Type: application/json\"\n\nif __name__ == \"__main__\":\n    service = Service(delegate=Delegate())\n    asyncio.run(service.processRequests())\n```\n\n### Sync\n\n```python\nfrom jsonrpcx import *\n\nclass Service(CGIServer):\n    def ping(self) -> str:\n        return \"pong\"\n\n# You only need this if you want to overwrite the HTTP headers sent to the client\nclass Delegate(CGIServerDelegate):\n    def HTTPHeader(self):\n        return \"Content-Type: application/json\"\n\nif __name__ == \"__main__\":\n    Service(delegate=Delegate())\n```\n\n# OpenRPC documentation\n_Note: If you want to use this with web based tooling you need to configure CORS headers in a way to allow that._\n\nThis library already provieds experimental support for https://open-rpc.org/ which means you can auto generate documenation.\nThis library uses Pythons typing support to figure out what input types a function has.\n\nIf you point a tool that can generate documentation form the OpenRPC spec to your JSON-RPC 2.0 server it will already be able to generate documentation for it.\nThe method to generate the OpenRPC json document is `rpc.discover` if you want to overwrite its behaviour then you can overrde the method `def rpcDiscover(self):` or `async rpcDiscover(self):` depending on if you choose a sync or async JSON-RPC 2.0 server.\n\nThe `info` and `server` parts of the documentation return a example information by default. If you want to add your own information you can do it like so:\n\n```python\nfrom typing import *\nfrom jsonrpcx import *\n\n# Make sure to inherit from the correct class here depending on how you deploy the server\nclass Delegate(CGIServerDelegate):\n    def experimentalOpenRPCInfo(self):\n        return {\n            \"version\": \"1.0.0\",\n            \"title\": \"Example Title\",\n            \"termsOfService\": \"https://example.eu/tos\",\n            \"contact\": {\n                \"name\": \"Support Contact Name\",\n                \"email\": \"support@example.eu\",\n                \"url\": \"https://example.eu\"\n            },\n            \"description\": \"This API uses JSON-RPC 2.0. For simplicity it does not support batch method calls.\"\n            }\n\n    def experimentalOpenRPCServers(self) -> Optional[List[Dict]]:\n        return [{\n                \"name\": \"Example endpoint\",\n                \"url\": \"http://localhost:8000\"\n            }]\n```\n\n# Experimental support for request canceling\n\nThere is currently no definition for how request canceling should be implemented in JSON-RPC 2.0 but Microsoft has something like that in LSP and this works similar to that.\n\nWARNING: WHEN ENABLING EXPERIMENTAL REQUEST CANCELING ANYONE CAN CANCEL REQUESTS EVEN REQUESTS FROM OTHER CLIENTS AS LONG AS THEY CORRECTL GUESS THE MESSAGEID.\n\nThe server supports a method `rpc.cancel(id)` where id is the messageId the client sent to the server.\n\n```python\nfrom jsonrpcx import *\nimport asyncio\n\nsomeVar = []\n\nasync def cancel_me():\n    print('cancel_me(): before sleep')\n\n    try:\n        # Wait for 1 hour\n        someVar.append(\"example\")\n        await asyncio.sleep(3600)\n    except asyncio.CancelledError:\n        print('cancel_me(): cancel sleep')\n        raise\n    finally:\n        print('cancel_me(): after sleep')\n\n\nclass Service(ASGIServer, ExperimentalCancelRequestMixin):\n    async def getLongRunningProcess2(self, bla: str):   \n        return await cancel_me()\n\n\nclass Delegate(ASGIServerDelegate):\n    # You have these options to overwrite default behaviour (method signatures are subject to change):\n\n    async def experimentalWaitOnCancelRequest(self) -> None:\n        super.experimentalWaitOnCancelRequest()\n\n    async def experimentalAddRunningRequest(self, messageId: Any, task: asyncio.Task) -> None:\n        super.experimentalAddRunningRequest(messageId, task)\n\n    async def experimentalRemoveRunningRequest(self, messageId: Any) -> None:\n        super.experimentalRemoveRunningRequest(messageID)\n\n```\n\n# Experimental support for response streaming\n\nThere is currently no definition for how request canceling should be implemented in JSON-RPC 2.0.\n\nOn the Server:\n```python\nfrom jsonrpcx import *\n\nclass Service(ASGIServer, ExperimentalCancelRequestMixin):\n    async def streamedResponse(self):\n        # Any dict or list (json serializable) that is returned with the `yield` keyword will be streamed to the client\n        for i in range(10):\n            yield {\"res\": f\"test {i=}\"}\n            await asyncio.sleep(5)\n\n```\n\nOn the client:\n```python\nimport httpx\nimport asyncio\n\nasync def main():\n    client = httpx.AsyncClient()\n    async with client.stream('POST', 'http://127.0.0.1:8000', timeout=5000, json={\n        \"jsonrpc\": \"2.0\",\n        \"method\": \"streamedResponse\",\n        \"params\": [],\n        \"id\": 1\n    }) as response:\n        async for chunk in response.aiter_bytes():\n            print(chunk)\n            \n            \nif __name__ == '__main__':\n    asyncio.run(main())\n```\n\n# MQTT Transport\n\nWARNING: MQTT Transport is currently EXPERIMENTAL and its interface is subject to change.\nNOTE: To use MQTT Transport you need to install the package `asyncio_mqtt`.\n\nServer:\n```\nfrom jsonrpcx import *\n\nclass API(MqttJSONRPCServer):\n    async def ping(self):\n        return \"pong\"\n    \n    async def add(self, a, b):\n        return a + b\n\n\nasync def main():\n    server = API(\"localhost\", \"user\", \"password\", \"mqttChannel\")\n    await server.processRequests()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nClient:\n```\nfrom jsonrpcx import *\n\nasync def main():\n    res = await mqttRPCCall(\"add\", [5, 5], mqttHost=\"host\", mqttUser=\"user\", mqttPassword=\"password\", mqttPort=1883, mqttChannel=\"mqttChannel\")\n    print(res)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n\n# Setup development\n\nRun `pipenv install`\n\n# Testing\nRun `pipenv run pytest`\n\n# Running the Example server\n```uvicorn exampleserver:app --reload```\n\n# Publish\n\n- `python3 setup.py bdist_wheel`\n- `twine upload dist/* --skip-existing`\n# Improvement ideas\n\n- Instead of throwing CircularReference detected when what is returned from a function can not be serialized by json send a JSON serialization exception.\n- Add middle wear support\n- Add authentication middlewear\n- Add stream support for websockets\n- Improve secutiry problem with rpc.cancel (currently anyone can cancel any request as long as they know the messageId)\n    - Force the client to use cryptographic secure `messageId`.\n    - Let the server provide a crypographic secure `cancelToken` or JWT so only the client that made the request can canel that without the need to generate cryptographic secure messageId on the client which some clients might not be capable of.\n    - Keep a dict which client requested which messageId and check against that before canceling a request somehow.\n    - Let the middlewear take care of this.\n    - Expose all required data into the delegate and let the developer handle this.\n",
    "bugtrack_url": null,
    "license": "ISC",
    "summary": "A battle tested Python JSON-RPC2.0 library supporting client and server code in sync and async fashion.",
    "version": "4.1.0",
    "project_urls": {
        "Homepage": "https://codeberg.org/_laphilipa/jsonrpcx"
    },
    "split_keywords": [
        "json",
        "jsonrpc",
        "rpc"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "7a03c78f65615086582d82ae6ddb0eec4b498f317414e627f8e9060f3432ecc6",
                "md5": "2ef441d99637ab478710720c8b742513",
                "sha256": "94afb796154009f68a9e14bbc7256f245e668d642995b980217c372852608569"
            },
            "downloads": -1,
            "filename": "jsonrpcx-4.1.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "2ef441d99637ab478710720c8b742513",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": null,
            "size": 16014,
            "upload_time": "2023-05-07T08:05:32",
            "upload_time_iso_8601": "2023-05-07T08:05:32.551344Z",
            "url": "https://files.pythonhosted.org/packages/7a/03/c78f65615086582d82ae6ddb0eec4b498f317414e627f8e9060f3432ecc6/jsonrpcx-4.1.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-05-07 08:05:32",
    "github": false,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": true,
    "codeberg_user": "_laphilipa",
    "codeberg_project": "jsonrpcx",
    "lcname": "jsonrpcx"
}
        
Elapsed time: 0.15999s