Pyjoyment 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjo/JSON/RPC.py
ADDED
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.JSON.RPC - JSON-RPC 2.0
|
|
3
|
+
============================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.JSON.RPC import RPC
|
|
7
|
+
|
|
8
|
+
rpc = RPC()
|
|
9
|
+
|
|
10
|
+
@rpc.method
|
|
11
|
+
def subtract(minuend: int, subtrahend: int) -> int:
|
|
12
|
+
\"\"\"Subtract two numbers.\"\"\"
|
|
13
|
+
return minuend - subtrahend
|
|
14
|
+
|
|
15
|
+
# {'jsonrpc': '2.0', 'result': 19, 'id': 1}
|
|
16
|
+
rpc.dispatch_p({'jsonrpc': '2.0', 'method': 'subtract', 'params': [42, 23], 'id': 1}).then(print).wait()
|
|
17
|
+
|
|
18
|
+
# Or import the module, as in Google Python Style Guide
|
|
19
|
+
|
|
20
|
+
import Pyjo.JSON.RPC
|
|
21
|
+
|
|
22
|
+
rpc = Pyjo.JSON.RPC.new()
|
|
23
|
+
rpc.method('subtract', lambda minuend, subtrahend: minuend - subtrahend)
|
|
24
|
+
|
|
25
|
+
# A route of an application of Pyjoyment.Lite
|
|
26
|
+
post('/rpc', rpc.to_action())
|
|
27
|
+
|
|
28
|
+
# A client, which sends its requests with a user agent
|
|
29
|
+
client = Pyjo.JSON.RPC.Client.new(lambda request: ua.post_p('/rpc', json=request).then(lambda tx: tx.res.json()))
|
|
30
|
+
client.call_p('subtract', 42, 23).then(print).wait()
|
|
31
|
+
|
|
32
|
+
:mod:`Pyjo.JSON.RPC` is an implementation of `JSON-RPC 2.0
|
|
33
|
+
<https://www.jsonrpc.org/specification>`_, the requests, the notifications,
|
|
34
|
+
the responses and the batches of a server, which calls the methods that it
|
|
35
|
+
has, and of a client. It knows the messages, not how they travel: a message
|
|
36
|
+
is a value as :mod:`Pyjo.JSON` decodes it, or the JSON of one, and a server
|
|
37
|
+
or a client carries it over HTTP, as :meth:`~Pyjo_JSON_RPC.to_action` does
|
|
38
|
+
for a route of :mod:`Pyjoyment`, or over a stream of messages in both
|
|
39
|
+
directions, as :meth:`~Pyjo_JSON_RPC.serve` does for the standard input and
|
|
40
|
+
output of :mod:`Pyjo.IOLoop.Stdio` or a WebSocket, where both peers are
|
|
41
|
+
servers and clients.
|
|
42
|
+
|
|
43
|
+
A method gets the ``params`` of a request, the members of an array as its
|
|
44
|
+
positional arguments and those of an object as its keyword arguments, and
|
|
45
|
+
returns the ``result`` of the response, as a value, a :mod:`Pyjo.Promise` of
|
|
46
|
+
it, or a coroutine, of an ``async def``. A method that raises :class:`Error`
|
|
47
|
+
gives that error, and one that raises another exception gives ``-32603``,
|
|
48
|
+
``Internal error``, without its message, which the event ``exception`` gets.
|
|
49
|
+
The ``params`` that the parameters of a method do not take give ``-32602``,
|
|
50
|
+
``Invalid params``, and so do those that its JSON Schema does not validate,
|
|
51
|
+
with the errors of :mod:`Pyjo.JSON.Schema` as its ``data``. ::
|
|
52
|
+
|
|
53
|
+
# {'jsonrpc': '2.0', 'error': {'code': -32602, 'message': 'Invalid params',
|
|
54
|
+
# 'data': '/subtrahend: Expected integer - got string.'}, 'id': 2}
|
|
55
|
+
@rpc.method('subtract', schema=True)
|
|
56
|
+
def subtract(minuend: int, subtrahend: int) -> int:
|
|
57
|
+
return minuend - subtrahend
|
|
58
|
+
|
|
59
|
+
rpc.dispatch_p({'jsonrpc': '2.0', 'method': 'subtract', 'params': [42, 'x'], 'id': 2}).then(print).wait()
|
|
60
|
+
|
|
61
|
+
A notification, a request without an ``id``, gets no response, also when it
|
|
62
|
+
fails, and neither does a batch of notifications. A batch is answered with the
|
|
63
|
+
responses of its requests in one array, once the last of them is there.
|
|
64
|
+
|
|
65
|
+
See also
|
|
66
|
+
--------
|
|
67
|
+
|
|
68
|
+
:mod:`Pyjo.JSON`, :mod:`Pyjo.JSON.Schema`, https://mojolicious.org.
|
|
69
|
+
|
|
70
|
+
It has no counterpart in Mojolicious, and is written from the specification of
|
|
71
|
+
JSON-RPC 2.0.
|
|
72
|
+
|
|
73
|
+
Classes
|
|
74
|
+
-------
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
import Pyjo.Base
|
|
78
|
+
import Pyjo.EventEmitter
|
|
79
|
+
import Pyjo.Exception
|
|
80
|
+
import Pyjo.JSON
|
|
81
|
+
import Pyjo.JSON.Schema
|
|
82
|
+
import Pyjo.Promise
|
|
83
|
+
|
|
84
|
+
import builtins
|
|
85
|
+
import inspect
|
|
86
|
+
|
|
87
|
+
from collections.abc import Callable
|
|
88
|
+
from typing import TYPE_CHECKING, Any, Self, cast, overload
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if TYPE_CHECKING:
|
|
92
|
+
import Pyjoyment.Controller
|
|
93
|
+
|
|
94
|
+
type Anything = builtins.object
|
|
95
|
+
|
|
96
|
+
PARSE_ERROR = -32700
|
|
97
|
+
"""The code of the error of a message that is not JSON."""
|
|
98
|
+
INVALID_REQUEST = -32600
|
|
99
|
+
"""The code of the error of a message that is not a request."""
|
|
100
|
+
METHOD_NOT_FOUND = -32601
|
|
101
|
+
"""The code of the error of a method that the server does not have."""
|
|
102
|
+
INVALID_PARAMS = -32602
|
|
103
|
+
"""The code of the error of parameters that the method does not take."""
|
|
104
|
+
INTERNAL_ERROR = -32603
|
|
105
|
+
"""The code of the error of a method that fails."""
|
|
106
|
+
|
|
107
|
+
# The messages of the errors of the specification
|
|
108
|
+
MESSAGES = {
|
|
109
|
+
PARSE_ERROR: 'Parse error',
|
|
110
|
+
INVALID_REQUEST: 'Invalid Request',
|
|
111
|
+
METHOD_NOT_FOUND: 'Method not found',
|
|
112
|
+
INVALID_PARAMS: 'Invalid params',
|
|
113
|
+
INTERNAL_ERROR: 'Internal error',
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_MISSING = builtins.object()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class Error(Pyjo.Exception.object):
|
|
120
|
+
"""
|
|
121
|
+
An error of JSON-RPC, which a method raises for the response of its
|
|
122
|
+
request, and which rejects the promise of a call of :class:`Client`. ::
|
|
123
|
+
|
|
124
|
+
raise Pyjo.JSON.RPC.Error.new(-32000, 'Not enough credit', {'balance': 3})
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, code: int = INTERNAL_ERROR, message: str | None = None, data: Any = _MISSING) -> None:
|
|
128
|
+
"""::
|
|
129
|
+
|
|
130
|
+
e = Pyjo.JSON.RPC.Error.new(-32000, 'Not enough credit')
|
|
131
|
+
e = Pyjo.JSON.RPC.Error.new(-32000, 'Not enough credit', {'balance': 3})
|
|
132
|
+
e = Pyjo.JSON.RPC.Error.new(Pyjo.JSON.RPC.INVALID_PARAMS)
|
|
133
|
+
|
|
134
|
+
Construct a new :class:`Error` object, with the message of the
|
|
135
|
+
specification for one of its codes by default.
|
|
136
|
+
"""
|
|
137
|
+
super().__init__(message if message is not None else MESSAGES.get(code, 'Server error'))
|
|
138
|
+
|
|
139
|
+
self.code = code
|
|
140
|
+
"""::
|
|
141
|
+
|
|
142
|
+
code = e.code
|
|
143
|
+
|
|
144
|
+
The code of the error, an integer.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
self.data = None if data is _MISSING else data
|
|
148
|
+
"""::
|
|
149
|
+
|
|
150
|
+
data = e.data
|
|
151
|
+
|
|
152
|
+
What the server tells about the error, ``None`` if it tells nothing.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
self._data = data is not _MISSING
|
|
156
|
+
|
|
157
|
+
def __json__(self) -> dict[str, Any]:
|
|
158
|
+
"""::
|
|
159
|
+
|
|
160
|
+
error = e.__json__()
|
|
161
|
+
|
|
162
|
+
The error object of a response, as :mod:`Pyjo.JSON` encodes it, with
|
|
163
|
+
``code``, ``message`` and ``data``, where it has one.
|
|
164
|
+
"""
|
|
165
|
+
error: dict[str, Any] = {'code': self.code, 'message': self.message}
|
|
166
|
+
if self._data:
|
|
167
|
+
error['data'] = self.data
|
|
168
|
+
return error
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def from_json(cls, error: Any) -> Self:
|
|
172
|
+
"""::
|
|
173
|
+
|
|
174
|
+
e = Pyjo.JSON.RPC.Error.from_json({'code': -32601, 'message': 'Method not found'})
|
|
175
|
+
|
|
176
|
+
The :class:`Error` of the error object of a response.
|
|
177
|
+
"""
|
|
178
|
+
if not isinstance(error, dict):
|
|
179
|
+
return cls(INTERNAL_ERROR, 'Invalid error', error)
|
|
180
|
+
error_ = cast(dict[str, Any], error)
|
|
181
|
+
code, message = error_.get('code'), error_.get('message')
|
|
182
|
+
return cls(
|
|
183
|
+
code if isinstance(code, int) and not isinstance(code, bool) else INTERNAL_ERROR,
|
|
184
|
+
message if isinstance(message, str) else None,
|
|
185
|
+
error_.get('data', _MISSING),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class _Method:
|
|
190
|
+
"""A method of the server, with its signature and its schema."""
|
|
191
|
+
|
|
192
|
+
def __init__(self, cb: Callable[..., Any], context: bool, schema: Pyjo.JSON.Schema.object | None) -> None:
|
|
193
|
+
self.cb = cb
|
|
194
|
+
self.context = context
|
|
195
|
+
self.schema = schema
|
|
196
|
+
self.signature = inspect.signature(cb)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class Pyjo_JSON_RPC(Pyjo.EventEmitter.object):
|
|
200
|
+
"""
|
|
201
|
+
:mod:`Pyjo.JSON.RPC` inherits all events and methods from
|
|
202
|
+
:mod:`Pyjo.EventEmitter` and implements the following new ones.
|
|
203
|
+
|
|
204
|
+
Events
|
|
205
|
+
------
|
|
206
|
+
|
|
207
|
+
``exception`` ::
|
|
208
|
+
|
|
209
|
+
@rpc.on('exception')
|
|
210
|
+
def exception(rpc, e, context):
|
|
211
|
+
...
|
|
212
|
+
|
|
213
|
+
Emitted when a method raises an exception that is not an :class:`Error`,
|
|
214
|
+
which the response does not tell, with the context of the request. ::
|
|
215
|
+
|
|
216
|
+
rpc.on('exception', lambda rpc, e, context: print(f'Failed: {e}'))
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
def __init__(self) -> None:
|
|
220
|
+
"""::
|
|
221
|
+
|
|
222
|
+
rpc = Pyjo.JSON.RPC.new()
|
|
223
|
+
|
|
224
|
+
Construct a new :mod:`Pyjo.JSON.RPC` object.
|
|
225
|
+
"""
|
|
226
|
+
super().__init__()
|
|
227
|
+
self._methods: dict[str, _Method] = {}
|
|
228
|
+
|
|
229
|
+
def dispatch_p(self, message: Any, context: Any = None) -> Pyjo.Promise.object:
|
|
230
|
+
"""::
|
|
231
|
+
|
|
232
|
+
promise = rpc.dispatch_p({'jsonrpc': '2.0', 'method': 'subtract', 'params': [42, 23], 'id': 1})
|
|
233
|
+
promise = rpc.dispatch_p(b'{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}')
|
|
234
|
+
promise = rpc.dispatch_p(request, context=c)
|
|
235
|
+
|
|
236
|
+
Dispatch a request, a notification or a batch, as :mod:`Pyjo.JSON`
|
|
237
|
+
decodes it, or the JSON of one, as :class:`bytes` or :class:`str`, to
|
|
238
|
+
the methods, and return a :mod:`Pyjo.Promise` of the response, or of
|
|
239
|
+
``None`` for a notification or a batch of notifications. The promise
|
|
240
|
+
is not rejected: an error is a response. ``context`` is what a method
|
|
241
|
+
that asks for it gets, as the controller of :meth:`to_action`. ::
|
|
242
|
+
|
|
243
|
+
# {'jsonrpc': '2.0', 'error': {'code': -32601, 'message': 'Method not found'}, 'id': 1}
|
|
244
|
+
rpc.dispatch_p({'jsonrpc': '2.0', 'method': 'foobar', 'id': 1}).then(print).wait()
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
def exception(e: Exception) -> None:
|
|
248
|
+
self.emit('exception', e, context)
|
|
249
|
+
|
|
250
|
+
return self._dispatch_p(message, context, exception)
|
|
251
|
+
|
|
252
|
+
@overload
|
|
253
|
+
def method[F: Callable[..., Any]](self, name: F, cb: None = None) -> F: ...
|
|
254
|
+
|
|
255
|
+
@overload
|
|
256
|
+
def method[F: Callable[..., Any]](
|
|
257
|
+
self,
|
|
258
|
+
name: str | None = None,
|
|
259
|
+
cb: None = None,
|
|
260
|
+
*,
|
|
261
|
+
context: bool = False,
|
|
262
|
+
schema: bool | dict[str, Any] | Pyjo.JSON.Schema.object | None = None,
|
|
263
|
+
) -> Callable[[F], F]: ...
|
|
264
|
+
|
|
265
|
+
@overload
|
|
266
|
+
def method[F: Callable[..., Any]](
|
|
267
|
+
self,
|
|
268
|
+
name: str,
|
|
269
|
+
cb: F,
|
|
270
|
+
*,
|
|
271
|
+
context: bool = False,
|
|
272
|
+
schema: bool | dict[str, Any] | Pyjo.JSON.Schema.object | None = None,
|
|
273
|
+
) -> F: ...
|
|
274
|
+
|
|
275
|
+
def method(
|
|
276
|
+
self,
|
|
277
|
+
name: str | Callable[..., Any] | None = None,
|
|
278
|
+
cb: Callable[..., Any] | None = None,
|
|
279
|
+
*,
|
|
280
|
+
context: bool = False,
|
|
281
|
+
schema: bool | dict[str, Any] | Pyjo.JSON.Schema.object | None = None,
|
|
282
|
+
) -> Any:
|
|
283
|
+
"""::
|
|
284
|
+
|
|
285
|
+
cb = rpc.method('subtract', lambda minuend, subtrahend: minuend - subtrahend)
|
|
286
|
+
cb = rpc.method('whoami', lambda c: c.req.headers.user_agent, context=True)
|
|
287
|
+
cb = rpc.method('subtract', subtract, schema=True)
|
|
288
|
+
cb = rpc.method('subtract', subtract, schema={'type': 'object'})
|
|
289
|
+
|
|
290
|
+
Add a method, and return its callback. Without a callback, it is a
|
|
291
|
+
decorator, with the name of the method or of the function. ::
|
|
292
|
+
|
|
293
|
+
@rpc.method
|
|
294
|
+
def subtract(minuend: int, subtrahend: int) -> int:
|
|
295
|
+
return minuend - subtrahend
|
|
296
|
+
|
|
297
|
+
@rpc.method('math.add', schema=True)
|
|
298
|
+
def add(a: int, b: int) -> int:
|
|
299
|
+
return a + b
|
|
300
|
+
|
|
301
|
+
With ``context``, the method gets the context of the request before
|
|
302
|
+
its parameters, as the controller of :meth:`to_action`. ``schema``
|
|
303
|
+
validates the parameters with a :mod:`Pyjo.JSON.Schema` object or a
|
|
304
|
+
schema of its own, or, with ``True``, with
|
|
305
|
+
:meth:`~Pyjo.JSON.Schema.Pyjo_JSON_Schema.parameters` of the function,
|
|
306
|
+
the schema of its type hints. Positional parameters are validated as
|
|
307
|
+
the members of an object that the names of the parameters of the
|
|
308
|
+
function have.
|
|
309
|
+
"""
|
|
310
|
+
if callable(name):
|
|
311
|
+
return self.method(name.__name__, name, context=context, schema=schema)
|
|
312
|
+
if cb is None:
|
|
313
|
+
|
|
314
|
+
def decorator(cb: Callable[..., Any]) -> Callable[..., Any]:
|
|
315
|
+
return self.method(name if name is not None else cb.__name__, cb, context=context, schema=schema)
|
|
316
|
+
|
|
317
|
+
return decorator
|
|
318
|
+
|
|
319
|
+
if schema is True:
|
|
320
|
+
compiled = Pyjo.JSON.Schema.parameters(cb, skip=1 if context else 0)
|
|
321
|
+
elif isinstance(schema, Pyjo.JSON.Schema.object):
|
|
322
|
+
compiled = schema
|
|
323
|
+
elif schema:
|
|
324
|
+
compiled = Pyjo.JSON.Schema.new(schema)
|
|
325
|
+
else:
|
|
326
|
+
compiled = None
|
|
327
|
+
self._methods[name or cb.__name__] = _Method(cb, context, compiled)
|
|
328
|
+
return cb
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def methods(self) -> list[str]:
|
|
332
|
+
"""::
|
|
333
|
+
|
|
334
|
+
names = rpc.methods
|
|
335
|
+
|
|
336
|
+
The names of the methods, in the order that they were added.
|
|
337
|
+
"""
|
|
338
|
+
return list(self._methods)
|
|
339
|
+
|
|
340
|
+
def serve(
|
|
341
|
+
self, peer: Any, context: Any = None, *, dispatch_p: Callable[[Any, 'Client'], Any] | None = None
|
|
342
|
+
) -> 'Client':
|
|
343
|
+
"""::
|
|
344
|
+
|
|
345
|
+
client = rpc.serve(messages)
|
|
346
|
+
client = rpc.serve(messages, context=app)
|
|
347
|
+
client = rpc.serve(messages, dispatch_p=lambda message, client: rpc.dispatch_p(message, {'client': client}))
|
|
348
|
+
|
|
349
|
+
Serve the methods to a peer on a stream of messages in both directions,
|
|
350
|
+
as a :mod:`Pyjo.IOLoop.Messages` object, a
|
|
351
|
+
:mod:`Pyjo.Transaction.WebSocket` object or the controller of a
|
|
352
|
+
WebSocket, anything with the event ``message`` and the method
|
|
353
|
+
``send``, and return the :class:`Client` of the peer, whose requests
|
|
354
|
+
go on the same stream, so that both peers are servers and clients.
|
|
355
|
+
|
|
356
|
+
A message that is a response to a call of the client settles it, and a
|
|
357
|
+
response to no call is dropped, as an error of it would go back and
|
|
358
|
+
forth between two peers. Any other message is dispatched, with
|
|
359
|
+
``context``, the client by default, so that a method that asks for it
|
|
360
|
+
calls the peer back, and its response is sent once it is there: the
|
|
361
|
+
requests are served at the same time, and their responses may go out
|
|
362
|
+
in another order.
|
|
363
|
+
|
|
364
|
+
The event ``end`` of the peer, the end of its input, rejects the calls
|
|
365
|
+
of the client, which get no response anymore, and closes the peer
|
|
366
|
+
gracefully once the requests in progress have their responses, and the
|
|
367
|
+
events ``close`` and ``finish`` reject them too.
|
|
368
|
+
|
|
369
|
+
``dispatch_p`` is a function that dispatches each message in the place
|
|
370
|
+
of :meth:`dispatch_p` with ``context``, which the function chooses
|
|
371
|
+
itself, so it is an error to give both. It gets the message, as
|
|
372
|
+
:mod:`Pyjo.JSON` decodes it, which is not a response, and the client,
|
|
373
|
+
and returns the response, a :mod:`Pyjo.Promise` of it, or a coroutine,
|
|
374
|
+
or ``None`` for a notification, so that a protocol on JSON-RPC gives
|
|
375
|
+
each request a context of its own or rules of its own. A function that
|
|
376
|
+
raises, or whose promise is rejected, gives ``-32603``, ``Internal
|
|
377
|
+
error``, to the requests of the message that have an ``id``, and the
|
|
378
|
+
event ``exception``, with the client as the context. ::
|
|
379
|
+
|
|
380
|
+
# Standard input and output, whose method asks the client
|
|
381
|
+
@rpc.method(context=True)
|
|
382
|
+
def greet(client, name):
|
|
383
|
+
return client.call_p('confirm', f'Greet {name}?').then(lambda yes: f'Hello, {name}!' if yes else 'Bye')
|
|
384
|
+
|
|
385
|
+
messages = Pyjo.IOLoop.Messages.new(Pyjo.IOLoop.Stdio.new())
|
|
386
|
+
rpc.serve(messages)
|
|
387
|
+
messages.start()
|
|
388
|
+
|
|
389
|
+
# A WebSocket of Pyjoyment.Lite
|
|
390
|
+
websocket('/rpc', lambda c: rpc.serve(c))
|
|
391
|
+
|
|
392
|
+
# A context of its own for each request, and no batches
|
|
393
|
+
def dispatch_p(message, client):
|
|
394
|
+
if isinstance(message, list):
|
|
395
|
+
return {'jsonrpc': '2.0', 'error': {'code': -32600, 'message': 'No batches'}, 'id': None}
|
|
396
|
+
return rpc.dispatch_p(message, {'client': client})
|
|
397
|
+
|
|
398
|
+
rpc.serve(messages, dispatch_p=dispatch_p)
|
|
399
|
+
"""
|
|
400
|
+
if context is not None and dispatch_p is not None:
|
|
401
|
+
raise Pyjo.Exception.new('The context of serve is the one of its dispatch_p, not both')
|
|
402
|
+
client = Client()
|
|
403
|
+
if context is None:
|
|
404
|
+
context = client
|
|
405
|
+
pending = 0
|
|
406
|
+
ended = False
|
|
407
|
+
|
|
408
|
+
def send(message: Any) -> None:
|
|
409
|
+
peer.send(Pyjo.JSON.to_json(message))
|
|
410
|
+
|
|
411
|
+
def exception(e: Exception) -> None:
|
|
412
|
+
self.emit('exception', e, context)
|
|
413
|
+
|
|
414
|
+
def respond(response: Any = None) -> None:
|
|
415
|
+
nonlocal pending
|
|
416
|
+
pending -= 1
|
|
417
|
+
if response is not None:
|
|
418
|
+
send(response)
|
|
419
|
+
if ended and not pending:
|
|
420
|
+
peer.close_gracefully()
|
|
421
|
+
|
|
422
|
+
def dispatch(message: Any, client: Client) -> Any:
|
|
423
|
+
if isinstance(message, list):
|
|
424
|
+
return self._dispatch_p(message, context, exception)
|
|
425
|
+
return self._request_p(message, context, exception)
|
|
426
|
+
|
|
427
|
+
def message(peer: Any, message: Any) -> None:
|
|
428
|
+
nonlocal pending
|
|
429
|
+
try:
|
|
430
|
+
decoded = Pyjo.JSON.decode_json(message)
|
|
431
|
+
except ValueError:
|
|
432
|
+
send(_response(None, error=Error(PARSE_ERROR)))
|
|
433
|
+
return
|
|
434
|
+
if not isinstance(decoded, str) and client.receive(decoded) or _is_response(decoded):
|
|
435
|
+
return
|
|
436
|
+
pending += 1
|
|
437
|
+
|
|
438
|
+
# A function that fails answers the requests with an id, which wait for their responses
|
|
439
|
+
def failed(e: Any = None) -> None:
|
|
440
|
+
exception(e if isinstance(e, Exception) else Pyjo.Exception.new(e))
|
|
441
|
+
respond(_internal_errors(decoded))
|
|
442
|
+
|
|
443
|
+
try:
|
|
444
|
+
response = (dispatch_p or dispatch)(decoded, client)
|
|
445
|
+
if inspect.iscoroutine(response):
|
|
446
|
+
response = Pyjo.Promise.new(response)
|
|
447
|
+
except Exception as e:
|
|
448
|
+
response = Pyjo.Promise.reject(e)
|
|
449
|
+
if not isinstance(response, Pyjo.Promise.object):
|
|
450
|
+
response = Pyjo.Promise.resolve(response)
|
|
451
|
+
response.then(respond, failed)
|
|
452
|
+
|
|
453
|
+
def end(peer: Any) -> None:
|
|
454
|
+
nonlocal ended
|
|
455
|
+
ended = True
|
|
456
|
+
client.fail(Pyjo.Exception.new('End of input'))
|
|
457
|
+
if not pending:
|
|
458
|
+
peer.close_gracefully()
|
|
459
|
+
|
|
460
|
+
def close(peer: Any, *args: Any) -> None:
|
|
461
|
+
client.fail(Pyjo.Exception.new('Connection closed'))
|
|
462
|
+
|
|
463
|
+
client.send = send
|
|
464
|
+
peer.on('message', message)
|
|
465
|
+
peer.on('end', end)
|
|
466
|
+
peer.on('close', close)
|
|
467
|
+
peer.on('finish', close)
|
|
468
|
+
return client
|
|
469
|
+
|
|
470
|
+
def to_action(self) -> Callable[['Pyjoyment.Controller.object'], Pyjo.Promise.object]:
|
|
471
|
+
"""::
|
|
472
|
+
|
|
473
|
+
action = rpc.to_action()
|
|
474
|
+
|
|
475
|
+
The action of a route of :mod:`Pyjoyment`, which dispatches the body
|
|
476
|
+
of the request, with the controller as the context, and renders the
|
|
477
|
+
response in JSON, ``202 Accepted`` without a body for a notification
|
|
478
|
+
or a batch of notifications, and ``400 Bad Request`` for a body that is
|
|
479
|
+
not JSON or not a request. A method that fails is logged. ::
|
|
480
|
+
|
|
481
|
+
# Pyjoyment.Lite
|
|
482
|
+
post('/rpc', rpc.to_action())
|
|
483
|
+
|
|
484
|
+
# Pyjoyment
|
|
485
|
+
app.routes.post('/rpc').to(cb=rpc.to_action())
|
|
486
|
+
"""
|
|
487
|
+
|
|
488
|
+
def action(c: 'Pyjoyment.Controller.object') -> Pyjo.Promise.object:
|
|
489
|
+
def exception(e: Exception) -> None:
|
|
490
|
+
c.helpers.log().error(Pyjo.Exception.new(e).inspect())
|
|
491
|
+
self.emit('exception', e, c)
|
|
492
|
+
|
|
493
|
+
def respond(response: Any = None) -> None:
|
|
494
|
+
if response is None:
|
|
495
|
+
c.rendered(202)
|
|
496
|
+
return
|
|
497
|
+
error = _member(response, 'error') if _member(response, 'id') is None else None
|
|
498
|
+
status = 400 if _member(error, 'code') in (PARSE_ERROR, INVALID_REQUEST) else 200
|
|
499
|
+
c.render(json=response, status=status)
|
|
500
|
+
|
|
501
|
+
c.render_later()
|
|
502
|
+
return self._dispatch_p(c.req.body, c, exception).then(respond)
|
|
503
|
+
|
|
504
|
+
return action
|
|
505
|
+
|
|
506
|
+
def _call(self, request: dict[str, Any], context: Any) -> Any:
|
|
507
|
+
# The result of a method, or the error of its request
|
|
508
|
+
if (method := self._methods.get(request['method'])) is None:
|
|
509
|
+
raise Error(METHOD_NOT_FOUND)
|
|
510
|
+
params: list[Any] | dict[str, Any] = request.get('params', [])
|
|
511
|
+
args: list[Any] = params if isinstance(params, list) else []
|
|
512
|
+
kwargs: dict[str, Any] = params if isinstance(params, dict) else {}
|
|
513
|
+
try:
|
|
514
|
+
bound = method.signature.bind(*([context] if method.context else []), *args, **kwargs)
|
|
515
|
+
except TypeError as e:
|
|
516
|
+
raise Error(INVALID_PARAMS, data=str(e)) from None
|
|
517
|
+
|
|
518
|
+
if (schema := method.schema) is not None:
|
|
519
|
+
arguments: dict[str, Any] = {}
|
|
520
|
+
for i, (name, value) in enumerate(bound.arguments.items()):
|
|
521
|
+
if method.context and not i:
|
|
522
|
+
continue
|
|
523
|
+
if method.signature.parameters[name].kind is inspect.Parameter.VAR_KEYWORD:
|
|
524
|
+
arguments.update(value)
|
|
525
|
+
else:
|
|
526
|
+
arguments[name] = value
|
|
527
|
+
if not (result := schema.validate(arguments)):
|
|
528
|
+
raise Error(INVALID_PARAMS, data=str(result))
|
|
529
|
+
|
|
530
|
+
value = method.cb(*bound.args, **bound.kwargs)
|
|
531
|
+
if inspect.iscoroutine(value):
|
|
532
|
+
value = Pyjo.Promise.new(value)
|
|
533
|
+
return value
|
|
534
|
+
|
|
535
|
+
def _dispatch_p(self, message: Any, context: Any, exception: Callable[[Exception], None]) -> Pyjo.Promise.object:
|
|
536
|
+
if isinstance(message, bytes | bytearray | str):
|
|
537
|
+
try:
|
|
538
|
+
message = Pyjo.JSON.decode_json(message)
|
|
539
|
+
except ValueError:
|
|
540
|
+
return Pyjo.Promise.resolve(_response(None, error=Error(PARSE_ERROR)))
|
|
541
|
+
|
|
542
|
+
# A batch, whose responses come together, without the notifications
|
|
543
|
+
if isinstance(message, list):
|
|
544
|
+
if not message:
|
|
545
|
+
return Pyjo.Promise.resolve(_response(None, error=Error(INVALID_REQUEST)))
|
|
546
|
+
promises = [self._request_p(request, context, exception) for request in cast(list[Any], message)]
|
|
547
|
+
return Pyjo.Promise.all(*promises).then(
|
|
548
|
+
lambda *results: [r[0] for r in results if r and r[0] is not None] or None
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
return self._request_p(message, context, exception)
|
|
552
|
+
|
|
553
|
+
def _request_p(self, request: Any, context: Any, exception: Callable[[Exception], None]) -> Pyjo.Promise.object:
|
|
554
|
+
if not _is_request(request):
|
|
555
|
+
id = _member(request, 'id')
|
|
556
|
+
return Pyjo.Promise.resolve(_response(id if _is_id(id) else None, error=Error(INVALID_REQUEST)))
|
|
557
|
+
notification = 'id' not in request
|
|
558
|
+
id = request.get('id')
|
|
559
|
+
|
|
560
|
+
def result(value: Any = None) -> Any:
|
|
561
|
+
return None if notification else _response(id, result=value)
|
|
562
|
+
|
|
563
|
+
def error(e: Any = None) -> Any:
|
|
564
|
+
if not isinstance(e, Error):
|
|
565
|
+
exception(e if isinstance(e, Exception) else Pyjo.Exception.new(e))
|
|
566
|
+
e = Error(INTERNAL_ERROR)
|
|
567
|
+
return None if notification else _response(id, error=e)
|
|
568
|
+
|
|
569
|
+
try:
|
|
570
|
+
value = self._call(request, context)
|
|
571
|
+
except Exception as e:
|
|
572
|
+
return Pyjo.Promise.resolve(error(e))
|
|
573
|
+
if isinstance(value, Pyjo.Promise.object):
|
|
574
|
+
return value.then(result, error)
|
|
575
|
+
return Pyjo.Promise.resolve(result(value))
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
class Client(Pyjo.Base.object):
|
|
579
|
+
"""
|
|
580
|
+
A client of JSON-RPC, which sends its requests with a function of the
|
|
581
|
+
transport, and settles the promise of a call with the response that
|
|
582
|
+
comes back, from what the function returns or from :meth:`receive`. ::
|
|
583
|
+
|
|
584
|
+
# HTTP, whose response comes back from the request
|
|
585
|
+
client = Pyjo.JSON.RPC.Client.new(lambda request: ua.post_p(url, json=request).then(lambda tx: tx.res.json()))
|
|
586
|
+
|
|
587
|
+
# A WebSocket, whose responses come back on their own
|
|
588
|
+
client = Pyjo.JSON.RPC.Client.new(lambda request: ws.send({'json': request}))
|
|
589
|
+
ws.on('json', lambda ws, message: client.receive(message))
|
|
590
|
+
"""
|
|
591
|
+
|
|
592
|
+
def __init__(self, send: Callable[[dict[str, Any]], Any] | None = None) -> None:
|
|
593
|
+
"""::
|
|
594
|
+
|
|
595
|
+
client = Pyjo.JSON.RPC.Client.new()
|
|
596
|
+
client = Pyjo.JSON.RPC.Client.new(send)
|
|
597
|
+
|
|
598
|
+
Construct a new :class:`Client` object.
|
|
599
|
+
"""
|
|
600
|
+
self.send = send
|
|
601
|
+
"""::
|
|
602
|
+
|
|
603
|
+
send = client.send
|
|
604
|
+
client.send = lambda request: ...
|
|
605
|
+
|
|
606
|
+
The function of the transport, which gets a request or a notification,
|
|
607
|
+
and returns the response, as :meth:`receive` takes it, or a
|
|
608
|
+
:mod:`Pyjo.Promise` of it, or anything else, as ``None``, when the
|
|
609
|
+
response comes back to :meth:`receive`.
|
|
610
|
+
"""
|
|
611
|
+
|
|
612
|
+
self._id = 0
|
|
613
|
+
self._pending: dict[Any, Pyjo.Promise.object] = {}
|
|
614
|
+
|
|
615
|
+
def call_p(self, method: str, *args: Any, **kwargs: Any) -> Pyjo.Promise.object:
|
|
616
|
+
"""::
|
|
617
|
+
|
|
618
|
+
promise = client.call_p('subtract', 42, 23)
|
|
619
|
+
promise = client.call_p('subtract', minuend=42, subtrahend=23)
|
|
620
|
+
|
|
621
|
+
Call a method, with its positional or its keyword arguments as the
|
|
622
|
+
``params``, and return a :mod:`Pyjo.Promise` of the ``result``, which
|
|
623
|
+
an error of the response rejects with an :class:`Error`. ::
|
|
624
|
+
|
|
625
|
+
client.call_p('subtract', 42, 23).then(print).catch(lambda e: print(f'{e.code}: {e.message}')).wait()
|
|
626
|
+
"""
|
|
627
|
+
self._id += 1
|
|
628
|
+
id = self._id
|
|
629
|
+
promise = self._pending[id] = Pyjo.Promise.new()
|
|
630
|
+
try:
|
|
631
|
+
response = self._send(_request(method, args, kwargs, id))
|
|
632
|
+
except Exception as e:
|
|
633
|
+
self._pending.pop(id, None)
|
|
634
|
+
return promise.reject(e)
|
|
635
|
+
|
|
636
|
+
def received(response: Any = None) -> None:
|
|
637
|
+
if isinstance(response, dict | list | bytes | bytearray | str):
|
|
638
|
+
self.receive(response)
|
|
639
|
+
|
|
640
|
+
def failed(e: Any = None) -> None:
|
|
641
|
+
if (pending := self._pending.pop(id, None)) is not None:
|
|
642
|
+
pending.reject(e)
|
|
643
|
+
|
|
644
|
+
if isinstance(response, Pyjo.Promise.object):
|
|
645
|
+
response.then(received, failed)
|
|
646
|
+
else:
|
|
647
|
+
received(response)
|
|
648
|
+
return promise
|
|
649
|
+
|
|
650
|
+
def fail(self, e: Any) -> None:
|
|
651
|
+
"""::
|
|
652
|
+
|
|
653
|
+
client.fail(Pyjo.Exception.new('Connection closed'))
|
|
654
|
+
|
|
655
|
+
Reject the calls that wait for their responses, as the transport does
|
|
656
|
+
when no response can come anymore, as at its end.
|
|
657
|
+
"""
|
|
658
|
+
pending, self._pending = self._pending, {}
|
|
659
|
+
for promise in pending.values():
|
|
660
|
+
promise.reject(e)
|
|
661
|
+
|
|
662
|
+
def notify(self, method: str, *args: Any, **kwargs: Any) -> Any:
|
|
663
|
+
"""::
|
|
664
|
+
|
|
665
|
+
client.notify('update', 1, 2, 3)
|
|
666
|
+
client.notify('update', values=[1, 2, 3])
|
|
667
|
+
|
|
668
|
+
Send a notification, a request without a response, and return what
|
|
669
|
+
:attr:`send` returns.
|
|
670
|
+
"""
|
|
671
|
+
return self._send(_request(method, args, kwargs))
|
|
672
|
+
|
|
673
|
+
def receive(self, message: Any) -> bool:
|
|
674
|
+
"""::
|
|
675
|
+
|
|
676
|
+
boolean = client.receive({'jsonrpc': '2.0', 'result': 19, 'id': 1})
|
|
677
|
+
boolean = client.receive(b'{"jsonrpc": "2.0", "result": 19, "id": 1}')
|
|
678
|
+
|
|
679
|
+
Settle the calls of a response or of a batch of them, as :mod:`Pyjo.JSON`
|
|
680
|
+
decodes it, or the JSON of one, and return whether it was a response to
|
|
681
|
+
a call, so that another message, as a request of the server on the same
|
|
682
|
+
stream, goes elsewhere.
|
|
683
|
+
"""
|
|
684
|
+
if isinstance(message, bytes | bytearray | str):
|
|
685
|
+
try:
|
|
686
|
+
message = Pyjo.JSON.decode_json(message)
|
|
687
|
+
except ValueError:
|
|
688
|
+
return False
|
|
689
|
+
responses = cast(list[Any], message) if isinstance(message, list) else [message]
|
|
690
|
+
received = False
|
|
691
|
+
for response in responses:
|
|
692
|
+
if not isinstance(response, dict) or 'method' in response:
|
|
693
|
+
continue
|
|
694
|
+
response_ = cast(dict[str, Any], response)
|
|
695
|
+
id = response_.get('id')
|
|
696
|
+
if not _is_id(id) or (promise := self._pending.pop(id, None)) is None:
|
|
697
|
+
continue
|
|
698
|
+
received = True
|
|
699
|
+
if 'error' in response_:
|
|
700
|
+
promise.reject(Error.from_json(response_['error']))
|
|
701
|
+
else:
|
|
702
|
+
promise.resolve(response_.get('result'))
|
|
703
|
+
return received
|
|
704
|
+
|
|
705
|
+
def _send(self, request: dict[str, Any]) -> Any:
|
|
706
|
+
if self.send is None:
|
|
707
|
+
raise Pyjo.Exception.new('No function to send the request')
|
|
708
|
+
return self.send(request)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def _internal_errors(message: Anything) -> Any:
|
|
712
|
+
# The responses of -32603 of the requests of a message with an id, in an array for a batch
|
|
713
|
+
requests: list[Any] = cast(list[Any], message) if isinstance(message, list) else [message]
|
|
714
|
+
responses = [
|
|
715
|
+
_response(request['id'], error=Error(INTERNAL_ERROR))
|
|
716
|
+
for request in requests
|
|
717
|
+
if _is_request(request) and 'id' in request
|
|
718
|
+
]
|
|
719
|
+
if isinstance(message, list):
|
|
720
|
+
return responses or None
|
|
721
|
+
return responses[0] if responses else None
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def _is_id(id: Anything) -> bool:
|
|
725
|
+
return id is None or isinstance(id, str) or (isinstance(id, int | float) and not isinstance(id, bool))
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _is_request(request: Anything) -> bool:
|
|
729
|
+
if not isinstance(request, dict):
|
|
730
|
+
return False
|
|
731
|
+
request_ = cast(dict[str, Any], request)
|
|
732
|
+
return (
|
|
733
|
+
request_.get('jsonrpc') == '2.0'
|
|
734
|
+
and isinstance(request_.get('method'), str)
|
|
735
|
+
and isinstance(request_.get('params', []), list | dict)
|
|
736
|
+
and _is_id(request_.get('id'))
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _is_response(message: Anything) -> bool:
|
|
741
|
+
# A response, or a batch of them, which a server does not answer
|
|
742
|
+
if isinstance(message, list):
|
|
743
|
+
responses = cast(list[Any], message)
|
|
744
|
+
return bool(responses) and all(_is_response(response) for response in responses)
|
|
745
|
+
return isinstance(message, dict) and 'method' not in message and ('result' in message or 'error' in message)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _member(message: Anything, name: str) -> Any:
|
|
749
|
+
# A member of an object of JSON, or None for another value
|
|
750
|
+
return cast(dict[str, Any], message).get(name) if isinstance(message, dict) else None
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def _request(method: str, args: tuple[Any, ...], kwargs: dict[str, Any], id: Any = _MISSING) -> dict[str, Any]:
|
|
754
|
+
if args and kwargs:
|
|
755
|
+
raise Pyjo.Exception.new('The params of JSON-RPC are positional or named, not both')
|
|
756
|
+
request: dict[str, Any] = {'jsonrpc': '2.0', 'method': method}
|
|
757
|
+
if args or kwargs:
|
|
758
|
+
request['params'] = list(args) if args else kwargs
|
|
759
|
+
if id is not _MISSING:
|
|
760
|
+
request['id'] = id
|
|
761
|
+
return request
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _response(id: Any, result: Any = None, error: Error | None = None) -> dict[str, Any]:
|
|
765
|
+
if error is not None:
|
|
766
|
+
return {'jsonrpc': '2.0', 'error': error.__json__(), 'id': id}
|
|
767
|
+
return {'jsonrpc': '2.0', 'result': result, 'id': id}
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
new = Pyjo_JSON_RPC.new
|
|
771
|
+
object = Pyjo_JSON_RPC
|
|
772
|
+
RPC = Pyjo_JSON_RPC
|