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
|
@@ -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
|