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.
Files changed (200) hide show
  1. Pyjo/App/ASGI.py +735 -0
  2. Pyjo/App/CGI.py +773 -0
  3. Pyjo/App/FastCGI.py +631 -0
  4. Pyjo/App/Lambda.py +987 -0
  5. Pyjo/App/WSGI.py +427 -0
  6. Pyjo/App/__init__.py +409 -0
  7. Pyjo/Asset/File.py +377 -0
  8. Pyjo/Asset/Memory.py +235 -0
  9. Pyjo/Asset/__init__.py +200 -0
  10. Pyjo/Base.py +215 -0
  11. Pyjo/Cache.py +119 -0
  12. Pyjo/Collection.py +570 -0
  13. Pyjo/Content/MultiPart.py +325 -0
  14. Pyjo/Content/Single.py +193 -0
  15. Pyjo/Content/__init__.py +806 -0
  16. Pyjo/Cookie/Request.py +90 -0
  17. Pyjo/Cookie/Response.py +294 -0
  18. Pyjo/Cookie/__init__.py +143 -0
  19. Pyjo/DOM/CSS.py +1019 -0
  20. Pyjo/DOM/HTML.py +550 -0
  21. Pyjo/DOM/__init__.py +1329 -0
  22. Pyjo/Date.py +294 -0
  23. Pyjo/EventEmitter.py +522 -0
  24. Pyjo/Exception.py +494 -0
  25. Pyjo/File.py +959 -0
  26. Pyjo/GetOpt.py +605 -0
  27. Pyjo/Headers.py +760 -0
  28. Pyjo/HelloWorld.py +77 -0
  29. Pyjo/Home.py +109 -0
  30. Pyjo/IOLoop/ChildProcess.py +591 -0
  31. Pyjo/IOLoop/Client.py +736 -0
  32. Pyjo/IOLoop/Datagram.py +982 -0
  33. Pyjo/IOLoop/Messages.py +664 -0
  34. Pyjo/IOLoop/Server.py +458 -0
  35. Pyjo/IOLoop/Stdio.py +446 -0
  36. Pyjo/IOLoop/Stream.py +644 -0
  37. Pyjo/IOLoop/Subprocess.py +419 -0
  38. Pyjo/IOLoop/TLS.py +360 -0
  39. Pyjo/IOLoop/__init__.py +1036 -0
  40. Pyjo/IOLoop/certs/README.md +41 -0
  41. Pyjo/IOLoop/certs/ca.crt +20 -0
  42. Pyjo/IOLoop/certs/server.crt +21 -0
  43. Pyjo/IOLoop/certs/server.key +28 -0
  44. Pyjo/JSON/Pointer.py +135 -0
  45. Pyjo/JSON/RPC.py +772 -0
  46. Pyjo/JSON/Schema.py +2506 -0
  47. Pyjo/JSON/__init__.py +573 -0
  48. Pyjo/Loader.py +342 -0
  49. Pyjo/Log.py +723 -0
  50. Pyjo/Message/Request.py +725 -0
  51. Pyjo/Message/Response.py +476 -0
  52. Pyjo/Message/__init__.py +830 -0
  53. Pyjo/Parameters.py +472 -0
  54. Pyjo/Path.py +419 -0
  55. Pyjo/Promise.py +894 -0
  56. Pyjo/Prove.py +1305 -0
  57. Pyjo/Reactor/Asyncio.py +854 -0
  58. Pyjo/Reactor/Poll.py +439 -0
  59. Pyjo/Reactor/__init__.py +525 -0
  60. Pyjo/SSE.py +119 -0
  61. Pyjo/Server/ASGI.py +486 -0
  62. Pyjo/Server/CGI.py +259 -0
  63. Pyjo/Server/Daemon.py +1007 -0
  64. Pyjo/Server/FastCGI.py +517 -0
  65. Pyjo/Server/Hot/__init__.py +687 -0
  66. Pyjo/Server/Hot/__main__.py +5 -0
  67. Pyjo/Server/Lambda.py +641 -0
  68. Pyjo/Server/Prefork.py +917 -0
  69. Pyjo/Server/Prethread.py +394 -0
  70. Pyjo/Server/Reload/Backend/Poll.py +114 -0
  71. Pyjo/Server/Reload/Backend/__init__.py +126 -0
  72. Pyjo/Server/Reload/__init__.py +482 -0
  73. Pyjo/Server/Reload/__main__.py +5 -0
  74. Pyjo/Server/Stdio.py +228 -0
  75. Pyjo/Server/WSGI.py +349 -0
  76. Pyjo/Server/__init__.py +396 -0
  77. Pyjo/String/Bytes.py +532 -0
  78. Pyjo/String/HTML.py +210 -0
  79. Pyjo/String/Mixin.py +333 -0
  80. Pyjo/String/Template.py +470 -0
  81. Pyjo/String/Unicode.py +553 -0
  82. Pyjo/String/__init__.py +0 -0
  83. Pyjo/TOML.py +232 -0
  84. Pyjo/Template.py +1290 -0
  85. Pyjo/Test/Pyjo.py +1454 -0
  86. Pyjo/Test/__init__.py +1233 -0
  87. Pyjo/Test/pytest_plugin.py +305 -0
  88. Pyjo/Transaction/HTTP.py +374 -0
  89. Pyjo/Transaction/WebSocket.py +709 -0
  90. Pyjo/Transaction/__init__.py +429 -0
  91. Pyjo/URL/Template.py +459 -0
  92. Pyjo/URL/__init__.py +651 -0
  93. Pyjo/Upload.py +139 -0
  94. Pyjo/UserAgent/CookieJar.py +391 -0
  95. Pyjo/UserAgent/Proxy.py +138 -0
  96. Pyjo/UserAgent/Server.py +239 -0
  97. Pyjo/UserAgent/Transactor.py +766 -0
  98. Pyjo/UserAgent/__init__.py +1243 -0
  99. Pyjo/Util.py +3567 -0
  100. Pyjo/WebSocket.py +332 -0
  101. Pyjo/YAML.py +1167 -0
  102. Pyjo/__init__.py +441 -0
  103. Pyjo/__main__.py +35 -0
  104. Pyjo/py.typed +0 -0
  105. Pyjoyment/Command/Author/__init__.py +20 -0
  106. Pyjoyment/Command/Author/generate/__init__.py +106 -0
  107. Pyjoyment/Command/Author/generate/app.py +299 -0
  108. Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
  109. Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
  110. Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
  111. Pyjoyment/Command/Author/generate/lite_app.py +113 -0
  112. Pyjoyment/Command/Author/generate/plugin.py +267 -0
  113. Pyjoyment/Command/Author/generate/pyproject.py +123 -0
  114. Pyjoyment/Command/Author/inflate.py +101 -0
  115. Pyjoyment/Command/__init__.py +332 -0
  116. Pyjoyment/Command/asgi.py +92 -0
  117. Pyjoyment/Command/cgi.py +90 -0
  118. Pyjoyment/Command/daemon.py +160 -0
  119. Pyjoyment/Command/eval.py +128 -0
  120. Pyjoyment/Command/fastcgi.py +99 -0
  121. Pyjoyment/Command/get.py +305 -0
  122. Pyjoyment/Command/lambda.py +117 -0
  123. Pyjoyment/Command/prefork.py +181 -0
  124. Pyjoyment/Command/prethread.py +126 -0
  125. Pyjoyment/Command/routes.py +133 -0
  126. Pyjoyment/Command/stdio.py +92 -0
  127. Pyjoyment/Command/version.py +141 -0
  128. Pyjoyment/Command/wsgi.py +93 -0
  129. Pyjoyment/Commands.py +546 -0
  130. Pyjoyment/Controller.py +1307 -0
  131. Pyjoyment/HelloWorld.py +56 -0
  132. Pyjoyment/Lite.py +494 -0
  133. Pyjoyment/Plugin/AllowedHosts.py +205 -0
  134. Pyjoyment/Plugin/Config.py +248 -0
  135. Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
  136. Pyjoyment/Plugin/EPLRenderer.py +134 -0
  137. Pyjoyment/Plugin/EPRenderer.py +212 -0
  138. Pyjoyment/Plugin/HeaderCondition.py +98 -0
  139. Pyjoyment/Plugin/Health.py +325 -0
  140. Pyjoyment/Plugin/JSONConfig.py +172 -0
  141. Pyjoyment/Plugin/Mount.py +223 -0
  142. Pyjoyment/Plugin/PyRenderer.py +209 -0
  143. Pyjoyment/Plugin/TOMLConfig.py +141 -0
  144. Pyjoyment/Plugin/TagHelpers.py +959 -0
  145. Pyjoyment/Plugin/YAMLConfig.py +178 -0
  146. Pyjoyment/Plugin/__init__.py +80 -0
  147. Pyjoyment/Plugins.py +215 -0
  148. Pyjoyment/Renderer.py +673 -0
  149. Pyjoyment/Routes/Match.py +251 -0
  150. Pyjoyment/Routes/Pattern.py +443 -0
  151. Pyjoyment/Routes/Route.py +827 -0
  152. Pyjoyment/Routes/__init__.py +419 -0
  153. Pyjoyment/Sessions.py +255 -0
  154. Pyjoyment/Static.py +421 -0
  155. Pyjoyment/Types.py +238 -0
  156. Pyjoyment/Validator/Validation.py +395 -0
  157. Pyjoyment/Validator/__init__.py +304 -0
  158. Pyjoyment/__init__.py +1083 -0
  159. Pyjoyment/__main__.py +5 -0
  160. Pyjoyment/py.typed +0 -0
  161. Pyjoyment/resources/public/favicon.ico +0 -0
  162. Pyjoyment/resources/public/favicon.svg +345 -0
  163. Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
  164. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
  165. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
  166. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
  167. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
  168. Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
  169. Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
  170. Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
  171. Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
  172. Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
  173. Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
  174. Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
  175. Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
  176. Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
  177. Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
  178. Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
  179. Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
  180. Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
  181. Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
  182. Pyjoyment/resources/public/pyjo/logo.png +0 -0
  183. Pyjoyment/resources/public/pyjo/logo.svg +346 -0
  184. Pyjoyment/resources/public/pyjo/notfound.png +0 -0
  185. Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
  186. Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
  187. Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
  188. Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
  189. Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
  190. Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
  191. Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
  192. Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
  193. Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
  194. Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
  195. Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
  196. pyjoyment-0.1.0.dist-info/METADATA +376 -0
  197. pyjoyment-0.1.0.dist-info/RECORD +200 -0
  198. pyjoyment-0.1.0.dist-info/WHEEL +4 -0
  199. pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
  200. 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