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,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjoyment.Plugin.AllowedHosts - Host and Origin validation plugin
|
|
3
|
+
=================================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
# Pyjoyment
|
|
7
|
+
app.plugin('AllowedHosts')
|
|
8
|
+
|
|
9
|
+
# Pyjoyment.Lite
|
|
10
|
+
plugin('AllowedHosts')
|
|
11
|
+
|
|
12
|
+
# The hosts of a deployment, and the origins of its pages
|
|
13
|
+
app.plugin('AllowedHosts', hosts=['example.com', '.example.com'], origins=['https://app.example.com'])
|
|
14
|
+
|
|
15
|
+
# Only for some routes, under the helper "allowed_hosts"
|
|
16
|
+
app.plugin('AllowedHosts', hook=False)
|
|
17
|
+
mcp = app.routes.under('/mcp', lambda c: c.allowed_hosts())
|
|
18
|
+
mcp.post('/').to(cb=lambda c: c.render(json={'jsonrpc': '2.0'}))
|
|
19
|
+
|
|
20
|
+
:mod:`Pyjoyment.Plugin.AllowedHosts` rejects a request whose ``Host`` header
|
|
21
|
+
is not one of the hosts of the application, with ``421 Misdirected Request``,
|
|
22
|
+
and a request whose ``Origin`` header, which a browser sends with a request
|
|
23
|
+
of a page of another site, is not one of its origins, with
|
|
24
|
+
``403 Forbidden``. A request without ``Origin``, which a browser sends
|
|
25
|
+
without it only for a page of the same site, and a client that is not a
|
|
26
|
+
browser, passes.
|
|
27
|
+
|
|
28
|
+
It protects a server on ``localhost`` against DNS rebinding: a page of another
|
|
29
|
+
site whose host name the attacker makes an address of ``127.0.0.1`` requests
|
|
30
|
+
the server with its own host in ``Host``, which is not one of the hosts, and
|
|
31
|
+
with its own origin, which is not one of the origins, as for a server of
|
|
32
|
+
the Model Context Protocol, whose transport over HTTP requires it.
|
|
33
|
+
|
|
34
|
+
The host is the one of ``Host``, the host of
|
|
35
|
+
:meth:`Pyjo.URL.Pyjo_URL.to_abs` of the request, which a reverse proxy
|
|
36
|
+
passes on.
|
|
37
|
+
|
|
38
|
+
See :mod:`Pyjoyment.Plugins` for a list of plugins that are available by
|
|
39
|
+
default.
|
|
40
|
+
|
|
41
|
+
Options
|
|
42
|
+
-------
|
|
43
|
+
|
|
44
|
+
:mod:`Pyjoyment.Plugin.AllowedHosts` supports the following options.
|
|
45
|
+
|
|
46
|
+
``hook`` ::
|
|
47
|
+
|
|
48
|
+
app.plugin('AllowedHosts', hook=False)
|
|
49
|
+
|
|
50
|
+
Check every request of the application, in a hook of ``before_dispatch``,
|
|
51
|
+
defaults to ``True``. With ``False``, only the routes under the helper
|
|
52
|
+
``allowed_hosts`` are checked.
|
|
53
|
+
|
|
54
|
+
``hosts`` ::
|
|
55
|
+
|
|
56
|
+
app.plugin('AllowedHosts', hosts=['example.com', 'localhost:3000', '.example.org'])
|
|
57
|
+
app.plugin('AllowedHosts', hosts=[re.compile(r'^pr-\\d+\\.example\\.com$')])
|
|
58
|
+
app.plugin('AllowedHosts', hosts=lambda c, host: host.endswith('.internal'))
|
|
59
|
+
|
|
60
|
+
The hosts of the application, defaults to ``localhost``, ``127.0.0.1``
|
|
61
|
+
and ``[::1]``. A host without a port is of any port, one with a port of
|
|
62
|
+
that port only, one that starts with a dot of the domain and its
|
|
63
|
+
subdomains, a regular expression searches the host and its port, as
|
|
64
|
+
``Host`` has them, and a function takes the controller and the host and
|
|
65
|
+
its port, and returns true for one of the application. The names of the
|
|
66
|
+
hosts are compared without their case.
|
|
67
|
+
|
|
68
|
+
``origins`` ::
|
|
69
|
+
|
|
70
|
+
app.plugin('AllowedHosts', origins=['https://app.example.com', 'http://localhost:5173'])
|
|
71
|
+
app.plugin('AllowedHosts', origins=lambda c, origin: origin.startswith('https://'))
|
|
72
|
+
|
|
73
|
+
The origins of the pages that request the application, defaults to the
|
|
74
|
+
origins of the hosts of ``hosts``, of any scheme. An origin is a scheme, a
|
|
75
|
+
host and a port, as a host of ``hosts`` with its scheme, and a function
|
|
76
|
+
takes the controller and the origin, as ``Origin`` has it.
|
|
77
|
+
|
|
78
|
+
Helpers
|
|
79
|
+
-------
|
|
80
|
+
|
|
81
|
+
:mod:`Pyjoyment.Plugin.AllowedHosts` implements the following helpers.
|
|
82
|
+
|
|
83
|
+
``allowed_hosts`` ::
|
|
84
|
+
|
|
85
|
+
boolean = c.allowed_hosts()
|
|
86
|
+
boolean = c.allowed_hosts(hosts=['mcp.example.com'])
|
|
87
|
+
|
|
88
|
+
mcp = app.routes.under('/mcp', lambda c: c.allowed_hosts(hosts=['mcp.example.com']))
|
|
89
|
+
mcp.post('/').to(cb=lambda c: c.render(json={'jsonrpc': '2.0'}))
|
|
90
|
+
|
|
91
|
+
Check the request, with the options of the plugin, or with ``hosts`` and
|
|
92
|
+
``origins`` of its own, and render the response of a request that is not
|
|
93
|
+
allowed, so that it is false and ends the dispatch of :meth:`under
|
|
94
|
+
<Pyjoyment.Routes.Route.Pyjoyment_Routes_Route.under>`, which a condition
|
|
95
|
+
of a route would not do, as the next route would match the request.
|
|
96
|
+
|
|
97
|
+
See also
|
|
98
|
+
--------
|
|
99
|
+
|
|
100
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
101
|
+
|
|
102
|
+
It has no counterpart in Mojolicious.
|
|
103
|
+
|
|
104
|
+
Classes
|
|
105
|
+
-------
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
import Pyjo.URL
|
|
109
|
+
import Pyjoyment.Plugin
|
|
110
|
+
|
|
111
|
+
import re
|
|
112
|
+
|
|
113
|
+
from collections.abc import Callable, Iterable
|
|
114
|
+
from typing import TYPE_CHECKING, Any
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if TYPE_CHECKING:
|
|
118
|
+
import Pyjoyment
|
|
119
|
+
import Pyjoyment.Controller
|
|
120
|
+
|
|
121
|
+
# The hosts of the loopback interface, of a server on localhost
|
|
122
|
+
HOSTS = ('localhost', '127.0.0.1', '[::1]')
|
|
123
|
+
|
|
124
|
+
_PORTS = {'http': 80, 'https': 443, 'ws': 80, 'wss': 443}
|
|
125
|
+
|
|
126
|
+
type _Hosts = Iterable[str | re.Pattern[str]] | Callable[['Pyjoyment.Controller.object', str], bool]
|
|
127
|
+
type _Origins = Iterable[str] | Callable[['Pyjoyment.Controller.object', str], bool] | None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class Pyjoyment_Plugin_AllowedHosts(Pyjoyment.Plugin.object):
|
|
131
|
+
"""
|
|
132
|
+
:mod:`Pyjoyment.Plugin.AllowedHosts` inherits all methods from
|
|
133
|
+
:mod:`Pyjoyment.Plugin` and implements the following new ones.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def register(self, app: 'Pyjoyment.object', conf: dict[str, Any] | None = None) -> None:
|
|
137
|
+
"""::
|
|
138
|
+
|
|
139
|
+
plugin.register(Pyjoyment.new())
|
|
140
|
+
plugin.register(Pyjoyment.new(), {'hosts': ['example.com']})
|
|
141
|
+
|
|
142
|
+
Register the hook and the helper in :mod:`Pyjoyment` application.
|
|
143
|
+
"""
|
|
144
|
+
conf = conf or {}
|
|
145
|
+
hosts: _Hosts = conf.get('hosts', HOSTS)
|
|
146
|
+
origins: _Origins = conf.get('origins')
|
|
147
|
+
|
|
148
|
+
def allowed_hosts(c: 'Pyjoyment.Controller.object', **own: Any) -> bool:
|
|
149
|
+
return _allowed(c, own.get('hosts', hosts), own.get('origins', origins if 'hosts' not in own else None))
|
|
150
|
+
|
|
151
|
+
app.helper('allowed_hosts', allowed_hosts)
|
|
152
|
+
if conf.get('hook', True):
|
|
153
|
+
app.hook('before_dispatch', allowed_hosts)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _allowed(c: 'Pyjoyment.Controller.object', hosts: _Hosts, origins: _Origins) -> bool:
|
|
157
|
+
# The response of a request that is not allowed, which the hook and the helper render
|
|
158
|
+
url = c.req.url.to_abs()
|
|
159
|
+
host = url.host_port
|
|
160
|
+
if not host or not _host(c, hosts, url):
|
|
161
|
+
c.helpers.log().debug(f'Host "{host}" not allowed')
|
|
162
|
+
c.render(text='Host not allowed', status=421)
|
|
163
|
+
return False
|
|
164
|
+
origin = c.req.headers.header('Origin')
|
|
165
|
+
if origin is not None and not _origin(c, hosts, origins, origin):
|
|
166
|
+
c.helpers.log().debug(f'Origin "{origin}" not allowed')
|
|
167
|
+
c.render(text='Origin not allowed', status=403)
|
|
168
|
+
return False
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _host(c: 'Pyjoyment.Controller.object', hosts: _Hosts, url: Pyjo.URL.object) -> bool:
|
|
173
|
+
host = url.host_port or ''
|
|
174
|
+
if callable(hosts):
|
|
175
|
+
return hosts(c, host)
|
|
176
|
+
name, port = (url.host or '').lower(), url.port or _PORTS.get(url.scheme or '')
|
|
177
|
+
for allowed in hosts:
|
|
178
|
+
if isinstance(allowed, re.Pattern):
|
|
179
|
+
if allowed.search(host):
|
|
180
|
+
return True
|
|
181
|
+
continue
|
|
182
|
+
allowed = allowed.lower()
|
|
183
|
+
if allowed.startswith('.'):
|
|
184
|
+
if name == allowed[1:] or name.endswith(allowed):
|
|
185
|
+
return True
|
|
186
|
+
continue
|
|
187
|
+
expected = Pyjo.URL.new(f'http://{allowed}')
|
|
188
|
+
if expected.host == name and (expected.port is None or expected.port == port):
|
|
189
|
+
return True
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _origin(c: 'Pyjoyment.Controller.object', hosts: _Hosts, origins: _Origins, origin: str) -> bool:
|
|
194
|
+
if callable(origins):
|
|
195
|
+
return origins(c, origin)
|
|
196
|
+
if origins is not None:
|
|
197
|
+
return origin.lower() in (allowed.lower() for allowed in origins)
|
|
198
|
+
# The origins of the hosts, of a page of the application
|
|
199
|
+
url = Pyjo.URL.new(origin)
|
|
200
|
+
return bool(url.scheme and url.host) and _host(c, hosts, url)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
new = Pyjoyment_Plugin_AllowedHosts.new
|
|
204
|
+
object = Pyjoyment_Plugin_AllowedHosts
|
|
205
|
+
AllowedHosts = Pyjoyment_Plugin_AllowedHosts
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjoyment.Plugin.Config - Python-ish configuration plugin
|
|
3
|
+
=========================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
# myapp.conf (it's just Python ending with a dict)
|
|
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
|
+
'music_dir': app.home.child('music'),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Pyjoyment
|
|
19
|
+
config = app.plugin('Config')
|
|
20
|
+
print(config['foo'])
|
|
21
|
+
|
|
22
|
+
# Pyjoyment.Lite
|
|
23
|
+
config = plugin('Config')
|
|
24
|
+
print(config['foo'])
|
|
25
|
+
|
|
26
|
+
# foo.html.ep
|
|
27
|
+
%= config()['foo']
|
|
28
|
+
|
|
29
|
+
# The configuration is available application-wide
|
|
30
|
+
config = app.config()
|
|
31
|
+
print(config['foo'])
|
|
32
|
+
|
|
33
|
+
# Everything can be customized with options
|
|
34
|
+
config = plugin('Config', file='/etc/myapp.stuff')
|
|
35
|
+
|
|
36
|
+
:mod:`Pyjoyment.Plugin.Config` is a Python-ish configuration plugin.
|
|
37
|
+
|
|
38
|
+
The configuration file is Python code, and the value of its last expression,
|
|
39
|
+
a dict, is the configuration; the statements before it can import modules and
|
|
40
|
+
prepare values. The application object can be accessed via ``app``. A
|
|
41
|
+
default configuration filename in the application home directory will be
|
|
42
|
+
generated from the value of :attr:`Pyjoyment.Pyjoyment_App.moniker`
|
|
43
|
+
(``$moniker.conf``). You can extend the normal configuration file
|
|
44
|
+
``$moniker.conf`` with ``mode`` specific ones like ``$moniker.$mode.conf``,
|
|
45
|
+
which will be detected automatically.
|
|
46
|
+
|
|
47
|
+
These configuration values are currently reserved:
|
|
48
|
+
|
|
49
|
+
``config_override``
|
|
50
|
+
If this configuration value has been set in
|
|
51
|
+
:meth:`Pyjoyment.Pyjoyment_App.config` when this plugin is loaded, it will
|
|
52
|
+
not do anything besides loading deployment specific plugins.
|
|
53
|
+
|
|
54
|
+
``plugins`` ::
|
|
55
|
+
|
|
56
|
+
'plugins': [{'SetUserGroup': {'user': 'sri', 'group': 'staff'}}]
|
|
57
|
+
|
|
58
|
+
One or more deployment specific plugins that should be loaded right after
|
|
59
|
+
this plugin has been loaded.
|
|
60
|
+
|
|
61
|
+
The code of this plugin is a good example for learning to build new plugins,
|
|
62
|
+
you're welcome to fork it.
|
|
63
|
+
|
|
64
|
+
See :mod:`Pyjoyment.Plugins` for a list of plugins that are available by
|
|
65
|
+
default.
|
|
66
|
+
|
|
67
|
+
Options
|
|
68
|
+
-------
|
|
69
|
+
|
|
70
|
+
:mod:`Pyjoyment.Plugin.Config` supports the following options.
|
|
71
|
+
|
|
72
|
+
default ::
|
|
73
|
+
|
|
74
|
+
# Pyjoyment.Lite
|
|
75
|
+
plugin('Config', default={'foo': 'bar'})
|
|
76
|
+
|
|
77
|
+
Default configuration, making configuration files optional.
|
|
78
|
+
|
|
79
|
+
ext ::
|
|
80
|
+
|
|
81
|
+
# Pyjoyment.Lite
|
|
82
|
+
plugin('Config', ext='stuff')
|
|
83
|
+
|
|
84
|
+
File extension for generated configuration filenames, defaults to ``conf``.
|
|
85
|
+
|
|
86
|
+
file ::
|
|
87
|
+
|
|
88
|
+
# Pyjoyment.Lite
|
|
89
|
+
plugin('Config', file='myapp.conf')
|
|
90
|
+
plugin('Config', file='/etc/foo.stuff')
|
|
91
|
+
|
|
92
|
+
Path to configuration file, absolute or relative to the application home
|
|
93
|
+
directory, defaults to the value of the ``PYJO_CONFIG`` environment variable
|
|
94
|
+
or ``$moniker.conf`` in the application home directory.
|
|
95
|
+
|
|
96
|
+
See also
|
|
97
|
+
--------
|
|
98
|
+
|
|
99
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
100
|
+
|
|
101
|
+
Ported from `Mojolicious::Plugin::Config
|
|
102
|
+
<https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojolicious/Plugin/Config.pm>`_ of Mojolicious 9.49.
|
|
103
|
+
|
|
104
|
+
Classes
|
|
105
|
+
-------
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
import Pyjo.Exception
|
|
109
|
+
import Pyjo.File
|
|
110
|
+
import Pyjoyment.Plugin
|
|
111
|
+
|
|
112
|
+
from Pyjo.Util import decode
|
|
113
|
+
|
|
114
|
+
import ast
|
|
115
|
+
import os
|
|
116
|
+
import re
|
|
117
|
+
|
|
118
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if TYPE_CHECKING:
|
|
122
|
+
import Pyjoyment
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class Pyjoyment_Plugin_Config(Pyjoyment.Plugin.object):
|
|
126
|
+
"""
|
|
127
|
+
:mod:`Pyjoyment.Plugin.Config` inherits all methods from
|
|
128
|
+
:mod:`Pyjoyment.Plugin` and implements the following new ones.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def load(self, file: str, conf: dict[str, Any], app: 'Pyjoyment.object') -> dict[str, Any]:
|
|
132
|
+
"""::
|
|
133
|
+
|
|
134
|
+
plugin.load(file, conf, app)
|
|
135
|
+
|
|
136
|
+
Loads configuration file and passes the content to :meth:`parse`. ::
|
|
137
|
+
|
|
138
|
+
def load(self, file, conf, app):
|
|
139
|
+
...
|
|
140
|
+
return self.parse(content, file, conf, app)
|
|
141
|
+
"""
|
|
142
|
+
data = Pyjo.File.path(file).slurp()
|
|
143
|
+
assert isinstance(data, bytes)
|
|
144
|
+
if (content := decode('UTF-8', data)) is None:
|
|
145
|
+
raise Pyjo.Exception.new(f'Configuration file "{file}" has invalid encoding')
|
|
146
|
+
return self.parse(content, file, conf, app)
|
|
147
|
+
|
|
148
|
+
def parse(self, content: str, file: str, conf: dict[str, Any], app: 'Pyjoyment.object') -> dict[str, Any]:
|
|
149
|
+
"""::
|
|
150
|
+
|
|
151
|
+
plugin.parse(content, file, conf, app)
|
|
152
|
+
|
|
153
|
+
Parse configuration file. ::
|
|
154
|
+
|
|
155
|
+
def parse(self, content, file, conf, app):
|
|
156
|
+
...
|
|
157
|
+
return config
|
|
158
|
+
"""
|
|
159
|
+
# Run Python code in sandbox
|
|
160
|
+
try:
|
|
161
|
+
config = _run(content, file, app)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
raise Pyjo.Exception.new(f'Can\'t load configuration from file "{file}": {e}') from e
|
|
164
|
+
if not isinstance(config, dict):
|
|
165
|
+
raise Pyjo.Exception.new(f'Configuration file "{file}" did not return a dict')
|
|
166
|
+
|
|
167
|
+
return cast(dict[str, Any], config)
|
|
168
|
+
|
|
169
|
+
def register(self, app: 'Pyjoyment.object', conf: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
170
|
+
"""::
|
|
171
|
+
|
|
172
|
+
config = plugin.register(Pyjoyment.new())
|
|
173
|
+
config = plugin.register(Pyjoyment.new(), {'file': '/etc/app.conf'})
|
|
174
|
+
|
|
175
|
+
Register plugin in :mod:`Pyjoyment` application and merge
|
|
176
|
+
configuration.
|
|
177
|
+
"""
|
|
178
|
+
conf = conf or {}
|
|
179
|
+
|
|
180
|
+
# Override
|
|
181
|
+
if app.config().get('config_override'):
|
|
182
|
+
return _plugins(app, app.config())
|
|
183
|
+
|
|
184
|
+
# Config file
|
|
185
|
+
file = conf.get('file') or os.environ.get('PYJO_CONFIG')
|
|
186
|
+
file = str(file) if file else f'{app.moniker}.{conf.get("ext") or "conf"}'
|
|
187
|
+
|
|
188
|
+
# Mode specific config file
|
|
189
|
+
m = re.search(r'^(.*)\.([^.]+)$', file)
|
|
190
|
+
mode: str | None = f'{m[1]}.{app.mode}.{m[2]}' if m else ''
|
|
191
|
+
|
|
192
|
+
home = app.home
|
|
193
|
+
if not Pyjo.File.path(file).is_abs:
|
|
194
|
+
file = home.child(file).to_str()
|
|
195
|
+
if mode and not Pyjo.File.path(mode).is_abs:
|
|
196
|
+
mode = home.child(mode).to_str()
|
|
197
|
+
if not (mode and os.path.exists(mode)):
|
|
198
|
+
mode = None
|
|
199
|
+
|
|
200
|
+
# Read config file
|
|
201
|
+
config: dict[str, Any] = {}
|
|
202
|
+
if os.path.exists(file):
|
|
203
|
+
config = self.load(file, conf, app)
|
|
204
|
+
|
|
205
|
+
# Check for default and mode specific config file
|
|
206
|
+
elif not conf.get('default') and not mode:
|
|
207
|
+
raise Pyjo.Exception.new(f'Configuration file "{file}" missing, maybe you need to create it?\n')
|
|
208
|
+
|
|
209
|
+
# Merge everything
|
|
210
|
+
if mode:
|
|
211
|
+
config = {**config, **self.load(mode, conf, app)}
|
|
212
|
+
if conf.get('default'):
|
|
213
|
+
config = {**conf['default'], **config}
|
|
214
|
+
return _plugins(app, app.config(config).config())
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _plugins(app: 'Pyjoyment.object', config: dict[str, Any]) -> dict[str, Any]:
|
|
218
|
+
if plugins := config.get('plugins'):
|
|
219
|
+
if not isinstance(plugins, list):
|
|
220
|
+
raise Pyjo.Exception.new('Configuration value "plugins" is not a list')
|
|
221
|
+
for plugin in cast(list[Any], plugins):
|
|
222
|
+
if not isinstance(plugin, dict):
|
|
223
|
+
raise Pyjo.Exception.new('Configuration value "plugins" contains an entry that is not a dict')
|
|
224
|
+
name, values = next(iter(cast(dict[str, Any], plugin).items()))
|
|
225
|
+
app.plugin(name, values)
|
|
226
|
+
|
|
227
|
+
return app.config()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# The value of the last expression of the code, as Perl gives the value of the last statement, with the application
|
|
231
|
+
# as "app"
|
|
232
|
+
def _run(content: str, file: str, app: 'Pyjoyment.object') -> Any:
|
|
233
|
+
tree = ast.parse(content, filename=file)
|
|
234
|
+
last = tree.body[-1] if tree.body else None
|
|
235
|
+
if not isinstance(last, ast.Expr):
|
|
236
|
+
last = None
|
|
237
|
+
else:
|
|
238
|
+
tree.body.pop()
|
|
239
|
+
namespace: dict[str, Any] = {'__name__': f'{__name__}.Sandbox', 'app': app}
|
|
240
|
+
exec(compile(tree, file, 'exec'), namespace)
|
|
241
|
+
if last is None:
|
|
242
|
+
return None
|
|
243
|
+
return eval(compile(ast.Expression(last.value), file, 'eval'), namespace)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
new = Pyjoyment_Plugin_Config.new
|
|
247
|
+
object = Pyjoyment_Plugin_Config
|
|
248
|
+
Config = Pyjoyment_Plugin_Config
|