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
@@ -0,0 +1,325 @@
1
+ """
2
+ Pyjoyment.Plugin.Health - Liveness and readiness plugin
3
+ =======================================================
4
+ ::
5
+
6
+ # Pyjoyment
7
+ app.plugin('Health')
8
+
9
+ # Pyjoyment.Lite
10
+ plugin('Health')
11
+
12
+ # The checks of readiness, of a function, a coroutine function or one that returns a promise
13
+ app.plugin('Health', checks={'db': lambda c: db.ping(), 'queue': queue_ready_p}, timeout=2)
14
+
15
+ # A check that another plugin adds
16
+ app.health.check('cache', lambda c: cache.connected)
17
+
18
+ :mod:`Pyjoyment.Plugin.Health` answers the probes of containers and
19
+ orchestrators, as the liveness, readiness and startup probes of Kubernetes,
20
+ the probes of Cloud Run, the health checks of load balancers and the
21
+ ``HEALTHCHECK`` of Docker, with the endpoints of Kubernetes itself.
22
+
23
+ ``GET /livez``
24
+ Liveness: ``200 OK`` with ``ok`` while the server answers, as a server
25
+ whose event loop is blocked does not answer. It has no checks of the
26
+ application, as an orchestrator restarts a container whose liveness
27
+ fails, and a database that is down does not get up with it.
28
+
29
+ ``GET /healthz``
30
+ The same as ``/livez``, under the name of the probes of Docker and of
31
+ load balancers.
32
+
33
+ ``GET /readyz``
34
+ Readiness: ``200 OK`` with ``ok`` once the checks of the application
35
+ pass, and ``503 Service Unavailable`` with the list of the checks while
36
+ one of them fails, so that an orchestrator or a load balancer sends the
37
+ requests to another instance. The check ``shutdown`` fails from the
38
+ moment that the server stops gracefully, also during its
39
+ :attr:`~Pyjo.Server.Daemon.Pyjo_Server_Daemon.shutdown_delay`, as
40
+ :attr:`Pyjo.IOLoop.Pyjo_IOLoop.is_stopping` of the event loop of the
41
+ thread, :data:`Pyjo.IOLoop.singleton`, which is the one of every server of
42
+ Pyjoyment, and its workers, tells: a client whose connection is kept alive
43
+ gets ``503`` with its last response.
44
+
45
+ The checks run at the same time, each with a timeout, and a check passes
46
+ unless it raises an exception, returns ``False``, or returns a promise, a
47
+ coroutine or another awaitable that is rejected, is fulfilled with ``False``,
48
+ or does not settle within its timeout. The reason of a failure goes to the
49
+ log, as the ``warn`` message ``Health check "db" failed: ...``, not to the
50
+ response, which withholds it.
51
+
52
+ A query of ``verbose`` lists the checks on success too, ``exclude`` leaves a
53
+ check out, as many times as there are checks to leave out, and a check of its
54
+ own is under the name of the endpoint, as ``/readyz/db``, as Kubernetes has
55
+ them. ::
56
+
57
+ $ curl 'http://127.0.0.1:3000/readyz?verbose'
58
+ [+]shutdown ok
59
+ [+]db ok
60
+ readyz check passed
61
+
62
+ $ curl 'http://127.0.0.1:3000/readyz?exclude=db'
63
+ ok
64
+
65
+ $ curl -i 'http://127.0.0.1:3000/readyz/db'
66
+ HTTP/1.1 503 Service Unavailable
67
+ ...
68
+ [-]db failed: reason withheld
69
+ readyz check failed
70
+
71
+ The endpoints are answered in a hook of ``around_dispatch``, before the hooks
72
+ of ``before_dispatch``, the static files and the routes, so that a probe is
73
+ cheap, is not logged but on the level ``trace``, and is answered also where a
74
+ hook rejects it, as :mod:`Pyjoyment.Plugin.AllowedHosts` would reject the
75
+ ``Host`` of the address of a pod, which a probe of Kubernetes sends. A hook of
76
+ ``around_dispatch`` that the application registers before the plugin wraps
77
+ the probes too.
78
+
79
+ See :mod:`Pyjoyment.Plugins` for a list of plugins that are available by
80
+ default.
81
+
82
+ Options
83
+ -------
84
+
85
+ :mod:`Pyjoyment.Plugin.Health` supports the following options.
86
+
87
+ ``checks`` ::
88
+
89
+ app.plugin('Health', checks={'db': lambda c: db.ping()})
90
+
91
+ The checks of readiness, the names and the functions that take the
92
+ controller of the probe, defaults to none.
93
+
94
+ ``healthz`` ::
95
+
96
+ app.plugin('Health', healthz='/health')
97
+ app.plugin('Health', healthz=None)
98
+
99
+ The path of the alias of liveness, defaults to ``/healthz``, ``None``
100
+ leaves it out.
101
+
102
+ ``livez`` ::
103
+
104
+ app.plugin('Health', livez='/-/live')
105
+
106
+ The path of liveness, defaults to ``/livez``, ``None`` leaves it out.
107
+
108
+ ``readyz`` ::
109
+
110
+ app.plugin('Health', readyz='/-/ready')
111
+
112
+ The path of readiness, defaults to ``/readyz``, ``None`` leaves it out.
113
+
114
+ ``timeout`` ::
115
+
116
+ app.plugin('Health', timeout=0.5)
117
+
118
+ The time in seconds that a check may take, defaults to ``1``, the
119
+ timeout of a probe of Kubernetes.
120
+
121
+ Helpers
122
+ -------
123
+
124
+ :mod:`Pyjoyment.Plugin.Health` implements the following helpers.
125
+
126
+ ``health.check`` ::
127
+
128
+ app.health.check('db', lambda c: db.ping())
129
+ app.health.check('search', search_ready_p, timeout=5)
130
+
131
+ Add a check of readiness, with a timeout of its own, or replace the
132
+ check of the name, as a plugin that connects to a service does.
133
+
134
+ ``health.path`` ::
135
+
136
+ path = c.health.path('livez')
137
+
138
+ The path of an endpoint, ``livez``, ``readyz`` or ``healthz``, or
139
+ ``None`` for one that is left out, as
140
+ :mod:`Pyjoyment.Command.Author.generate.dockerfile` takes it for the
141
+ ``HEALTHCHECK`` of the image.
142
+
143
+ See also
144
+ --------
145
+
146
+ :mod:`Pyjoyment`, https://mojolicious.org.
147
+
148
+ It has no counterpart in Mojolicious.
149
+
150
+ Classes
151
+ -------
152
+ """
153
+
154
+ import Pyjo.IOLoop
155
+ import Pyjo.Promise
156
+ import Pyjoyment.Plugin
157
+
158
+ import builtins
159
+ import concurrent.futures
160
+
161
+ from collections.abc import Awaitable, Callable
162
+ from typing import TYPE_CHECKING, Any
163
+
164
+
165
+ if TYPE_CHECKING:
166
+ import Pyjoyment
167
+ import Pyjoyment.Controller
168
+
169
+ type _Check = Callable[['Pyjoyment.Controller.object'], Any]
170
+
171
+ # The check of readiness of the server itself, which comes before the ones of the application
172
+ SHUTDOWN = 'shutdown'
173
+
174
+ # The result of a check that the query leaves out
175
+ EXCLUDED = builtins.object()
176
+
177
+
178
+ class Pyjoyment_Plugin_Health(Pyjoyment.Plugin.object):
179
+ """
180
+ :mod:`Pyjoyment.Plugin.Health` inherits all methods from
181
+ :mod:`Pyjoyment.Plugin` and implements the following new ones.
182
+ """
183
+
184
+ def register(self, app: 'Pyjoyment.object', conf: dict[str, Any] | None = None) -> None:
185
+ """::
186
+
187
+ plugin.register(Pyjoyment.new())
188
+ plugin.register(Pyjoyment.new(), {'checks': {'db': lambda c: True}})
189
+
190
+ Register the hook and the helpers in :mod:`Pyjoyment` application.
191
+ """
192
+ conf = conf or {}
193
+ default: float = conf.get('timeout', 1)
194
+ given: dict[str, _Check] = conf.get('checks') or {}
195
+ checks = {name: (cb, default) for name, cb in given.items()}
196
+ paths: dict[str, str | None] = {
197
+ endpoint: conf.get(endpoint, f'/{endpoint}') for endpoint in ('livez', 'readyz', 'healthz')
198
+ }
199
+
200
+ def check(c: 'Pyjoyment.Controller.object', name: str, cb: _Check, timeout: float | None = None) -> None:
201
+ checks[name] = (cb, timeout if timeout is not None else default)
202
+
203
+ def path(c: 'Pyjoyment.Controller.object', endpoint: str) -> str | None:
204
+ return paths.get(endpoint)
205
+
206
+ app.helper('health.check', check)
207
+ app.helper('health.path', path)
208
+
209
+ # The endpoints and the checks of their own, as Kubernetes has them
210
+ live = {
211
+ p: (endpoint, name)
212
+ for endpoint in ('livez', 'healthz')
213
+ if (prefix := paths[endpoint])
214
+ for p, name in ((prefix, None), (f'{prefix}/ping', 'ping'))
215
+ }
216
+ ready = paths['readyz']
217
+
218
+ def around_dispatch(next: Callable[[], Any], c: 'Pyjoyment.Controller.object') -> Any:
219
+ req = c.req
220
+ if req.method not in ('GET', 'HEAD'):
221
+ return next()
222
+ route = req.url.path.to_route()
223
+ if (found := live.get(route)) is not None:
224
+ return _live(c, *found)
225
+ if ready and (route == ready or route.startswith(f'{ready}/')):
226
+ name = route[len(ready) + 1 :] or None
227
+ if name is None or name == SHUTDOWN or name in checks:
228
+ return _ready(c, name, checks)
229
+ return next()
230
+
231
+ app.hook('around_dispatch', around_dispatch)
232
+
233
+
234
+ def _live(c: 'Pyjoyment.Controller.object', endpoint: str, name: str | None) -> None:
235
+ _render(c, endpoint, name, {'ping': None})
236
+
237
+
238
+ def _ready(c: 'Pyjoyment.Controller.object', name: str | None, checks: dict[str, tuple[_Check, float]]) -> None:
239
+ # The checks of the probe in their order, which the query leaves out, each with the reason of its failure
240
+ excluded = set(c.req.url.query.every_param('exclude')) if name is None else set[str]()
241
+ results: dict[str, Any] = {n: None for n in ([SHUTDOWN, *checks] if name is None else [name])}
242
+ promises: list[Pyjo.Promise.object] = []
243
+
244
+ def failure(n: str) -> Callable[..., None]:
245
+ def failed(*reason: Any) -> None:
246
+ results[n] = reason[0] if reason and reason[0] is not None else 'rejected'
247
+
248
+ return failed
249
+
250
+ for n in results:
251
+ if n in excluded:
252
+ results[n] = EXCLUDED
253
+ elif n == SHUTDOWN:
254
+ results[n] = 'stopping gracefully' if Pyjo.IOLoop.singleton.is_stopping else None
255
+ else:
256
+ cb, timeout = checks[n]
257
+ promises.append(_check_p(c, cb, timeout).catch(failure(n)))
258
+
259
+ if not promises:
260
+ return _render(c, 'readyz', name, results)
261
+
262
+ # The response once every check has settled
263
+ def render(*args: Any) -> None:
264
+ _render(c, 'readyz', name, results)
265
+
266
+ def exception(e: Any) -> None:
267
+ c.helpers.reply.exception(e)
268
+
269
+ c.render_later()
270
+ Pyjo.Promise.all_settled(*promises).then(render).catch(exception)
271
+
272
+
273
+ def _check_p(c: 'Pyjoyment.Controller.object', cb: _Check, timeout: float) -> Pyjo.Promise.object:
274
+ # A promise of its own, as a timeout attached to the promise of the check would reject a promise that the
275
+ # application may share, as the one of its warm-up
276
+ result = Pyjo.Promise.new()
277
+ try:
278
+ value = cb(c)
279
+ if isinstance(value, Awaitable | concurrent.futures.Future) and not isinstance(value, Pyjo.Promise.object):
280
+ value = Pyjo.Promise.new(value)
281
+ except Exception as e:
282
+ return result.reject(e)
283
+ if not isinstance(value, Pyjo.Promise.object):
284
+ return result.reject('returned False') if value is False else result.resolve()
285
+
286
+ loop = result.ioloop
287
+ timer = loop.timer(timeout, lambda loop: result.reject(f'timeout of {timeout:g} seconds'))
288
+
289
+ def settled(*values: Any) -> None:
290
+ loop.remove(timer)
291
+ result.reject('returned False') if values and values[0] is False else result.resolve()
292
+
293
+ def rejected(*reason: Any) -> None:
294
+ loop.remove(timer)
295
+ result.reject(*reason)
296
+
297
+ value.then(settled, rejected)
298
+ return result
299
+
300
+
301
+ def _render(c: 'Pyjoyment.Controller.object', endpoint: str, name: str | None, results: dict[str, Any]) -> None:
302
+ # The failures with their reason in the log only, and the list of the checks on a failure or with "verbose"
303
+ lines: list[str] = []
304
+ failed = False
305
+ for n, reason in results.items():
306
+ if reason is EXCLUDED:
307
+ lines.append(f'[+]{n} excluded: ok')
308
+ elif reason is None:
309
+ lines.append(f'[+]{n} ok')
310
+ else:
311
+ failed = True
312
+ lines.append(f'[-]{n} failed: reason withheld')
313
+ if n != SHUTDOWN:
314
+ c.helpers.log().warn(f'Health check "{n}" failed: {reason}')
315
+
316
+ if not failed and (name is not None or c.req.url.query.param('verbose') is None):
317
+ c.render(text='ok', format='txt')
318
+ else:
319
+ lines.append(f'{endpoint} check {"failed" if failed else "passed"}')
320
+ c.render(text='\n'.join(lines) + '\n', format='txt', status=503 if failed else 200)
321
+
322
+
323
+ new = Pyjoyment_Plugin_Health.new
324
+ object = Pyjoyment_Plugin_Health
325
+ Health = Pyjoyment_Plugin_Health
@@ -0,0 +1,172 @@
1
+ """
2
+ Pyjoyment.Plugin.JSONConfig - JSON configuration plugin
3
+ =======================================================
4
+ ::
5
+
6
+ # myapp.json (it's just JSON with embedded Python)
7
+ {
8
+ %# Just a value
9
+ "foo": "bar",
10
+
11
+ %# Nested data structures are fine too
12
+ "baz": ["♥"],
13
+
14
+ %# You have full access to the application
15
+ % import Pyjo.JSON
16
+ "music_dir": <%= Pyjo.JSON.to_json(app.home.child('music').to_str()) %>
17
+ }
18
+
19
+ # Pyjoyment
20
+ config = app.plugin('JSONConfig')
21
+ print(config['foo'])
22
+
23
+ # Pyjoyment.Lite
24
+ config = plugin('JSONConfig')
25
+ print(config['foo'])
26
+
27
+ # foo.html.ep
28
+ %= config()['foo']
29
+
30
+ # The configuration is available application-wide
31
+ config = app.config()
32
+ print(config['foo'])
33
+
34
+ # Everything can be customized with options
35
+ config = plugin('JSONConfig', file='/etc/myapp.conf')
36
+
37
+ :mod:`Pyjoyment.Plugin.JSONConfig` is a JSON configuration plugin that
38
+ preprocesses its input with :mod:`Pyjo.Template`.
39
+
40
+ The application object can be accessed via ``app``. A default configuration
41
+ filename in the application home directory will be generated from the value
42
+ of :attr:`Pyjoyment.Pyjoyment_App.moniker` (``$moniker.json``). You can extend
43
+ the normal configuration file ``$moniker.json`` with ``mode`` specific ones
44
+ like ``$moniker.$mode.json``, which will be detected automatically.
45
+
46
+ A value of the application goes into the JSON through
47
+ :func:`Pyjo.JSON.to_json`, which escapes what a string of JSON cannot hold
48
+ as it is, as the backslashes of a path of Windows.
49
+
50
+ These configuration values are currently reserved:
51
+
52
+ ``config_override``
53
+ If this configuration value has been set in
54
+ :meth:`Pyjoyment.Pyjoyment_App.config` when this plugin is loaded, it will
55
+ not do anything besides loading deployment specific plugins.
56
+
57
+ ``plugins`` ::
58
+
59
+ "plugins": [{"SetUserGroup": {"user": "sri", "group": "staff"}}]
60
+
61
+ One or more deployment specific plugins that should be loaded right after
62
+ this plugin has been loaded.
63
+
64
+ The code of this plugin is a good example for learning to build new plugins,
65
+ you're welcome to fork it.
66
+
67
+ See :mod:`Pyjoyment.Plugins` for a list of plugins that are available by
68
+ default.
69
+
70
+ Options
71
+ -------
72
+
73
+ :mod:`Pyjoyment.Plugin.JSONConfig` inherits all options from
74
+ :mod:`Pyjoyment.Plugin.Config` and supports the following new ones.
75
+
76
+ template ::
77
+
78
+ # Pyjoyment.Lite
79
+ plugin('JSONConfig', template={'line_start': '.'})
80
+
81
+ Attribute values passed to :mod:`Pyjo.Template` object used to preprocess
82
+ configuration files.
83
+
84
+ See also
85
+ --------
86
+
87
+ :mod:`Pyjoyment`, https://mojolicious.org.
88
+
89
+ Ported from `Mojolicious::Plugin::JSONConfig
90
+ <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojolicious/Plugin/JSONConfig.pm>`_ of Mojolicious 9.49.
91
+
92
+ Classes
93
+ -------
94
+ """
95
+
96
+ import Pyjo.Exception
97
+ import Pyjo.Template
98
+ import Pyjoyment.Plugin.Config
99
+
100
+ from Pyjo.JSON import from_json
101
+
102
+ from typing import TYPE_CHECKING, Any, cast
103
+
104
+
105
+ if TYPE_CHECKING:
106
+ import Pyjoyment
107
+
108
+
109
+ class Pyjoyment_Plugin_JSONConfig(Pyjoyment.Plugin.Config.object):
110
+ """
111
+ :mod:`Pyjoyment.Plugin.JSONConfig` inherits all methods from
112
+ :mod:`Pyjoyment.Plugin.Config` and implements the following new ones.
113
+ """
114
+
115
+ def parse(self, content: str, file: str, conf: dict[str, Any], app: 'Pyjoyment.object') -> dict[str, Any]:
116
+ """::
117
+
118
+ plugin.parse(content, file, conf, app)
119
+
120
+ Process content with :meth:`render` and parse it with
121
+ :mod:`Pyjo.JSON`. ::
122
+
123
+ def parse(self, content, file, conf, app):
124
+ ...
125
+ content = self.render(content, file, conf, app)
126
+ ...
127
+ return config
128
+ """
129
+ try:
130
+ config = from_json(self.render(content, file, conf, app))
131
+ except Exception as e:
132
+ raise Pyjo.Exception.new(f'Can\'t load configuration from file "{file}": {e}') from e
133
+ if not isinstance(config, dict):
134
+ raise Pyjo.Exception.new(f'Configuration file "{file}" did not return a JSON object')
135
+
136
+ return cast(dict[str, Any], config)
137
+
138
+ def register(self, app: 'Pyjoyment.object', conf: dict[str, Any] | None = None) -> dict[str, Any]:
139
+ """::
140
+
141
+ config = plugin.register(Pyjoyment.new())
142
+ config = plugin.register(Pyjoyment.new(), {'file': '/etc/foo.conf'})
143
+
144
+ Register plugin in :mod:`Pyjoyment` application and merge
145
+ configuration.
146
+ """
147
+ return super().register(app, {'ext': 'json', **(conf or {})})
148
+
149
+ def render(self, content: str, file: str, conf: dict[str, Any], app: 'Pyjoyment.object') -> str:
150
+ """::
151
+
152
+ plugin.render(content, file, conf, app)
153
+
154
+ Process configuration file with :mod:`Pyjo.Template`. ::
155
+
156
+ def render(self, content, file, conf, app):
157
+ ...
158
+ return content
159
+ """
160
+ # Application instance
161
+ mt = Pyjo.Template.new(**(conf.get('template') or {}))
162
+ mt.name = file
163
+ mt.prepend = 'app = args[0]' + (f'\n{mt.prepend}' if mt.prepend else '')
164
+ output = mt.render(content, app)
165
+ if isinstance(output, Exception):
166
+ raise output
167
+ return str(output)
168
+
169
+
170
+ new = Pyjoyment_Plugin_JSONConfig.new
171
+ object = Pyjoyment_Plugin_JSONConfig
172
+ JSONConfig = Pyjoyment_Plugin_JSONConfig