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