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
Pyjoyment/Renderer.py ADDED
@@ -0,0 +1,673 @@
1
+ """
2
+ Pyjoyment.Renderer - Generate dynamic content
3
+ =============================================
4
+ ::
5
+
6
+ from Pyjoyment.Renderer import Renderer
7
+
8
+ renderer = Renderer()
9
+ renderer.classes.append('MyApp.Controller.Foo')
10
+ renderer.paths.append('/home/sri/templates')
11
+
12
+ # Or import the module, as in Google Python Style Guide
13
+
14
+ import Pyjoyment.Renderer
15
+
16
+ renderer = Pyjoyment.Renderer.new()
17
+ renderer.classes.append('MyApp.Controller.Foo')
18
+ renderer.paths.append('/home/sri/templates')
19
+
20
+ :mod:`Pyjoyment.Renderer` is the standard :mod:`Pyjoyment` renderer.
21
+
22
+ See also
23
+ --------
24
+
25
+ :mod:`Pyjoyment`, https://mojolicious.org.
26
+
27
+ Ported from `Mojolicious::Renderer <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojolicious/Renderer.pm>`_
28
+ of Mojolicious 9.49.
29
+ """
30
+
31
+ import Pyjo.Base
32
+ import Pyjo.Cache
33
+ import Pyjo.Exception
34
+ import Pyjo.File
35
+ import Pyjo.Loader
36
+
37
+ from Pyjo.JSON import encode_json
38
+ from Pyjo.String.Template import is_template
39
+ from Pyjo.TOML import dump as dump_toml
40
+ from Pyjo.Util import _suggest, decamelize, deprecated, encode, gzip, md5_sum # pyright: ignore[reportPrivateUsage]
41
+ from Pyjo.YAML import dump
42
+
43
+ import io
44
+ import os
45
+ import re
46
+ import sys
47
+ import weakref
48
+
49
+ from collections.abc import Callable
50
+ from typing import TYPE_CHECKING, Any, Self, cast, overload
51
+
52
+
53
+ if TYPE_CHECKING:
54
+ import Pyjoyment.Controller
55
+
56
+
57
+ # A handler gets the renderer, the controller, the file object to write the output to and the options
58
+ type Handler = Callable[[Pyjoyment_Renderer, Any, io.StringIO, dict[str, Any]], Any]
59
+
60
+ # Bundled templates
61
+ _TEMPLATES = Pyjo.File.path(__file__).sibling('resources', 'templates')
62
+
63
+
64
+ class Pyjoyment_Renderer(Pyjo.Base.object):
65
+ """
66
+ :mod:`Pyjoyment.Renderer` inherits all methods from :mod:`Pyjo.Base` and
67
+ implements the following new ones.
68
+ """
69
+
70
+ def __init__(self) -> None:
71
+ """::
72
+
73
+ renderer = Pyjoyment.Renderer.new()
74
+
75
+ Construct a new :mod:`Pyjoyment.Renderer` object.
76
+ """
77
+ self.cache: Pyjo.Cache.object[Any, Any] = Pyjo.Cache.new()
78
+ """::
79
+
80
+ cache = renderer.cache
81
+ renderer.cache = Pyjo.Cache.new()
82
+
83
+ Renderer cache, defaults to a :mod:`Pyjo.Cache` object.
84
+ """
85
+
86
+ self.classes = ['__main__']
87
+ """::
88
+
89
+ classes = renderer.classes
90
+ renderer.classes = ['__main__']
91
+
92
+ Classes to use for finding templates in ``DATA`` sections with
93
+ :mod:`Pyjo.Loader`, first one has the highest precedence, defaults to
94
+ ``__main__``. Only files with exactly two extensions will be used, like
95
+ ``index.html.ep``. Note that for templates to be detected, these
96
+ classes need to have already been loaded and added before
97
+ :meth:`warmup` is called, which usually happens automatically during
98
+ application startup. ::
99
+
100
+ # Add another class with templates in DATA section
101
+ renderer.classes.append('Pyjoyment.Plugin.Fun')
102
+
103
+ # Add another class with templates in DATA section and higher precedence
104
+ renderer.classes.insert(0, 'Pyjoyment.Plugin.MoreFun')
105
+ """
106
+
107
+ self.compress = True
108
+ """::
109
+
110
+ boolean = renderer.compress
111
+ renderer.compress = boolean
112
+
113
+ Try to negotiate compression for dynamically generated response content
114
+ and ``gzip`` compress it automatically, defaults to true.
115
+ """
116
+
117
+ self.default_format = 'html'
118
+ """::
119
+
120
+ default = renderer.default_format
121
+ renderer.default_format = 'html'
122
+
123
+ The default format to render if ``format`` is not set in the stash,
124
+ defaults to ``html``. Note that changing the default away from ``html``
125
+ is not recommended, as it has the potential to break, for example,
126
+ plugins with bundled templates.
127
+ """
128
+
129
+ self.default_handler: str | None = None
130
+ """::
131
+
132
+ default = renderer.default_handler
133
+ renderer.default_handler = 'ep'
134
+
135
+ The default template handler to use for rendering in cases where
136
+ auto-detection doesn't work, like for ``inline`` templates. An
137
+ ``inline`` template that is a template string of
138
+ :mod:`Pyjo.String.Template` is rendered by the handler ``py`` of
139
+ :mod:`Pyjoyment.Plugin.PyRenderer`.
140
+ """
141
+
142
+ self.encoding: str | None = 'UTF-8'
143
+ """::
144
+
145
+ encoding = renderer.encoding
146
+ renderer.encoding = 'koi8-r'
147
+
148
+ Will encode generated content if set, defaults to ``UTF-8``. Note that
149
+ many renderers also use this value to determine if template files
150
+ should be decoded before processing.
151
+ """
152
+
153
+ self.handlers: dict[str, Handler] = {}
154
+ """::
155
+
156
+ handlers = renderer.handlers
157
+ renderer.handlers = {'epl': lambda renderer, c, output, options: ...}
158
+
159
+ Registered handlers.
160
+ """
161
+
162
+ self.helpers: dict[str, Callable[..., Any]] = {}
163
+ """::
164
+
165
+ helpers = renderer.helpers
166
+ renderer.helpers = {'url_for': lambda c, *args: ...}
167
+
168
+ Registered helpers.
169
+ """
170
+
171
+ self.min_compress_size = 860
172
+ """::
173
+
174
+ size = renderer.min_compress_size
175
+ renderer.min_compress_size = 1024
176
+
177
+ Minimum output size in bytes required for compression to be used if
178
+ enabled, defaults to ``860``.
179
+ """
180
+
181
+ self.paths: list[str] = []
182
+ """::
183
+
184
+ paths = renderer.paths
185
+ renderer.paths = ['/home/sri/templates']
186
+
187
+ Directories to look for templates in, first one has the highest
188
+ precedence. ::
189
+
190
+ # Add another "templates" directory
191
+ renderer.paths.append('/home/sri/templates')
192
+
193
+ # Add another "templates" directory with higher precedence
194
+ renderer.paths.insert(0, '/home/sri/themes/blue/templates')
195
+ """
196
+
197
+ # Not in Mojolicious: the templates of functions, for the handler "py"
198
+ self.templates: dict[str, Callable[..., Any]] = {}
199
+ """::
200
+
201
+ templates = renderer.templates
202
+ renderer.templates = {'index.html': lambda c: t('<p>Hello!</p>')}
203
+
204
+ Registered templates of functions, by the name of the template with
205
+ its format, as ``index.html``, which the handler ``py`` of
206
+ :mod:`Pyjoyment.Plugin.PyRenderer` renders, before the templates of
207
+ :attr:`paths` and :attr:`classes`.
208
+ """
209
+
210
+ self._index: dict[str, str] = {}
211
+ self._namespaces: list[type[Pyjoyment_Renderer_Helpers]] = []
212
+ self._proxy: dict[str, type[Pyjoyment_Renderer_Helpers]] = {}
213
+ self._templates: dict[str, list[str]] | None = None
214
+
215
+ # The methods of the proxies go with the renderer, as Mojo removes them in DESTROY
216
+ weakref.finalize(self, _teardown, self._namespaces)
217
+
218
+ def accepts(self, c: 'Pyjoyment.Controller.object', *formats: str) -> list[str] | str | None:
219
+ """::
220
+
221
+ all = renderer.accepts(Pyjoyment.Controller.new())
222
+ best = renderer.accepts(Pyjoyment.Controller.new(), 'html', 'json')
223
+
224
+ Select best possible representation for :mod:`Pyjoyment.Controller`
225
+ object from ``format`` ``GET``/``POST`` parameter, ``format`` stash
226
+ value, or ``Accept`` request header, defaults to returning the first
227
+ extension if no preference could be detected.
228
+ """
229
+ # DEPRECATED!
230
+ req = c.req
231
+ param = req.param('format')
232
+ if param is not None:
233
+ deprecated('The ?format=* parameter is deprecated in favor of ?_format=* for content negotiation')
234
+
235
+ # List representations
236
+ fmt = (param if param is not None else req.param('_format')) or c.stash().get('format')
237
+ exts = [fmt] if fmt else []
238
+ exts.extend(c.app.types.detect(req.headers.accept))
239
+ if not formats:
240
+ return exts
241
+
242
+ # Find best representation
243
+ for ext in exts:
244
+ if ext in formats:
245
+ return ext
246
+ return None if exts else formats[0]
247
+
248
+ def add_handler(self, name: str, cb: Handler) -> Self:
249
+ """::
250
+
251
+ renderer = renderer.add_handler('epl', lambda renderer, c, output, options: ...)
252
+
253
+ Register a handler. ::
254
+
255
+ def foo(renderer, c, output, options):
256
+ ...
257
+ output.write('Hello World!')
258
+
259
+ renderer.add_handler('foo', foo)
260
+ """
261
+ self.handlers[name] = cb
262
+ return self
263
+
264
+ def add_helper(self, name: str, cb: Callable[..., Any]) -> Self:
265
+ """::
266
+
267
+ renderer = renderer.add_helper('url_for', lambda c, *args: ...)
268
+
269
+ Register a helper. ::
270
+
271
+ def foo(c, *args):
272
+ ...
273
+
274
+ renderer.add_helper('foo', foo)
275
+ """
276
+ self.helpers[name] = cb
277
+ self._proxy.clear()
278
+ return self
279
+
280
+ @overload
281
+ def add_template[F: Callable[..., Any]](self, name: str, cb: None = None) -> Callable[[F], F]: ...
282
+
283
+ @overload
284
+ def add_template(self, name: str, cb: Callable[..., Any]) -> Self: ...
285
+
286
+ # Not in Mojolicious: the templates of functions, for the handler "py"
287
+ def add_template(self, name: str, cb: Callable[..., Any] | None = None) -> Any:
288
+ """::
289
+
290
+ renderer = renderer.add_template('index.html', lambda c: t('<p>Hello!</p>'))
291
+
292
+ @renderer.add_template('index.html')
293
+ def index(c): ...
294
+
295
+ Register a template of a function, which gets the controller and
296
+ returns a template string, see :mod:`Pyjoyment.Plugin.PyRenderer`. ::
297
+
298
+ def foo(c, name):
299
+ return t('<p>Hello {name}!</p>')
300
+
301
+ renderer.add_template('foo.html', foo)
302
+ """
303
+ if cb is None:
304
+
305
+ def decorator(cb: Callable[..., Any]) -> Callable[..., Any]:
306
+ self.add_template(name, cb)
307
+ return cb
308
+
309
+ return decorator
310
+ self.templates[name] = cb
311
+ if self._templates is not None:
312
+ self.warmup()
313
+ return self
314
+
315
+ def get_data_template(self, options: dict[str, Any]) -> str | bytes | None:
316
+ """::
317
+
318
+ template = renderer.get_data_template({'template': 'foo/bar', 'format': 'html', 'handler': 'epl'})
319
+
320
+ Return a ``DATA`` section template from :attr:`classes` for an options
321
+ dict with ``template``, ``format``, ``variant`` and ``handler`` values,
322
+ or ``None`` if no template could be found, usually used by handlers.
323
+ """
324
+ if not (template := self.template_name(options)):
325
+ return None
326
+ return cast('str | bytes | None', Pyjo.Loader.data_section(self._index.get(template), template))
327
+
328
+ def get_helper(self, name: str) -> 'Callable[..., Any] | type[Pyjoyment_Renderer_Helpers] | None':
329
+ """::
330
+
331
+ helper = renderer.get_helper('url_for')
332
+
333
+ Get a helper by full name, generate a helper dynamically for a prefix,
334
+ or return ``None`` if no helper or prefix could be found. Generated
335
+ helpers return a proxy object containing the current controller object
336
+ and on which nested helpers can be called.
337
+ """
338
+ if h := self._proxy.get(name) or self.helpers.get(name):
339
+ return h
340
+
341
+ # The proxy of a prefix is a class with the nested helpers, as Mojo makes it, a helper as a method and a prefix
342
+ # as a property
343
+ found = False
344
+ attrs: dict[str, Any] = {}
345
+ regex = re.compile(rf'^({re.escape(name)}\.([^.]+))' if name else r'^(([^.]+))')
346
+ for key in list(self.helpers):
347
+ if not (m := regex.match(key)):
348
+ continue
349
+ found = True
350
+ sub = self.get_helper(m[1])
351
+ attrs[m[2]] = _attribute(sub)
352
+
353
+ if not found:
354
+ return None
355
+ md5 = md5_sum(f'{name}:{id(self)}'.encode())
356
+ cls = type(f'Pyjoyment_Renderer_Helpers_{md5}', (Pyjoyment_Renderer_Helpers,), attrs)
357
+ self._namespaces.append(cls)
358
+ self._proxy[name] = cls
359
+ return cls
360
+
361
+ def render(self, c: 'Pyjoyment.Controller.object') -> tuple[str | bytes, str | None] | None:
362
+ """::
363
+
364
+ output, format = renderer.render(Pyjoyment.Controller.new())
365
+
366
+ Render output through one of the renderers. See
367
+ :meth:`Pyjoyment.Controller.Pyjoyment_Controller.render` for a more
368
+ user-friendly interface.
369
+ """
370
+ stash = c.stash()
371
+ options: dict[str, Any] = {
372
+ 'encoding': self.encoding,
373
+ 'handler': stash.get('handler'),
374
+ 'template': stash.pop('template', None),
375
+ 'variant': stash.get('variant'),
376
+ }
377
+ inline = options['inline'] = stash.pop('inline', None)
378
+ if inline is not None and options['handler'] is None:
379
+ # Not in Mojolicious: an inline template string, which the handler "py" renders
380
+ options['handler'] = 'py' if is_template(inline) else self.default_handler
381
+ options['format'] = stash.get('format') or self.default_format
382
+
383
+ # Data
384
+ if stash.get('data') is not None:
385
+ return stash.pop('data'), options['format']
386
+
387
+ # Text, which is a string as Perl stringifies a number or an object
388
+ if stash.get('text') is not None:
389
+ text = stash.pop('text')
390
+ return _maybe(options.get('encoding'), text if isinstance(text, str) else str(text)), options['format']
391
+
392
+ # JSON
393
+ if 'json' in stash:
394
+ return encode_json(stash.pop('json')), 'json'
395
+
396
+ # Not in Mojolicious: YAML, in the style of the documents that tools write, as index.yaml of a chart repository
397
+ # of Helm: the items of a sequence at the column of its key, no anchors, and the quotes of YAML 1.1
398
+ if 'yaml' in stash:
399
+ return encode('UTF-8', dump(stash.pop('yaml'), indent_sequences=False, anchors=False)), 'yaml'
400
+
401
+ # Not in Mojolicious: TOML, of a table
402
+ if 'toml' in stash:
403
+ return encode('UTF-8', dump_toml(stash.pop('toml'))), 'toml'
404
+
405
+ # Template or templateless handler
406
+ if options['template'] is None:
407
+ options['template'] = self.template_for(c)
408
+ if (output := self._render_template(c, options)) is None:
409
+ return None
410
+
411
+ # Inheritance
412
+ content = stash.setdefault('mojo.content', {})
413
+ saved = _MISSING
414
+ if local := bool(stash.get('extends') or stash.get('layout')):
415
+ saved = content.get('content', _MISSING)
416
+ content['content'] = output if _visible(output) else None
417
+ try:
418
+ while (next := _next(stash)) and inline is None:
419
+ options['handler'], options['template'] = stash.get('handler'), next
420
+ options['format'] = stash.get('format') or self.default_format
421
+ if (tmp := self._render_template(c, options)) is not None:
422
+ output = tmp
423
+ if content.get('content') is None and _visible(output):
424
+ content['content'] = output
425
+
426
+ if stash.get('mojo.string'):
427
+ return output, None
428
+ return _maybe(options.get('encoding'), output), options['format']
429
+
430
+ finally:
431
+ if local and saved is _MISSING:
432
+ content.pop('content', None)
433
+ elif local:
434
+ content['content'] = saved
435
+
436
+ def respond(
437
+ self, c: 'Pyjoyment.Controller.object', output: str | bytes, format: str | None, status: int | str | None = None
438
+ ) -> bool:
439
+ """::
440
+
441
+ boolean = renderer.respond(Pyjoyment.Controller.new(), output, format)
442
+ boolean = renderer.respond(Pyjoyment.Controller.new(), output, format, status)
443
+
444
+ Finalize dynamically generated response content and :attr:`compress`
445
+ it if possible.
446
+ """
447
+ stash = c.stash()
448
+ respond = stash.get('mojo.respond')
449
+ stash['mojo.respond'] = (respond or 0) + 1
450
+ if respond:
451
+ raise Pyjo.Exception.new('A response has already been rendered')
452
+
453
+ # Gzip compression
454
+ res = c.res
455
+ if self.compress and len(output) >= self.min_compress_size:
456
+ headers = res.headers
457
+ headers.append('Vary', 'Accept-Encoding')
458
+ accept = re.search(r'gzip', c.req.headers.accept_encoding or '', re.IGNORECASE)
459
+ if accept and not headers.content_encoding:
460
+ headers.content_encoding = 'gzip'
461
+ output = gzip(output if isinstance(output, bytes) else output.encode())
462
+
463
+ res.body = output if isinstance(output, bytes) else output.encode()
464
+ c.app.types.content_type(c, ext=format)
465
+ return bool(c.rendered(status))
466
+
467
+ def template_for(self, c: 'Pyjoyment.Controller.object') -> str | None:
468
+ """::
469
+
470
+ name = renderer.template_for(Pyjoyment.Controller.new())
471
+
472
+ Return default template name for :mod:`Pyjoyment.Controller` object, or
473
+ ``None`` if no name could be generated.
474
+ """
475
+ # Normal default template
476
+ stash = c.stash()
477
+ controller, action = stash.get('controller'), stash.get('action')
478
+ if controller and action:
479
+ return '/'.join([*decamelize(controller.replace('.', '::')).split('-'), action])
480
+
481
+ # Try the route name if we don't have controller and action
482
+ if (route := c.match.endpoint) is None:
483
+ return None
484
+ return route.name
485
+
486
+ def template_handler(self, options: dict[str, Any]) -> str | None:
487
+ """::
488
+
489
+ handler = renderer.template_handler({'template': 'foo/bar', 'format': 'html'})
490
+
491
+ Return handler for an options dict with ``template``, ``format`` and
492
+ ``variant`` values, or ``None`` if no handler could be found.
493
+ """
494
+ if not (file := self.template_name(options)):
495
+ return None
496
+ if not (handlers := (self._templates or {}).get(file)):
497
+ return self.default_handler
498
+ return handlers[0]
499
+
500
+ def template_name(self, options: dict[str, Any]) -> str | None:
501
+ """::
502
+
503
+ template = renderer.template_name({'template': 'foo/bar', 'format': 'html', 'handler': 'epl'})
504
+
505
+ Return a template name for an options dict with ``template``,
506
+ ``format``, ``variant`` and ``handler`` values, or ``None`` if no
507
+ template could be found, usually used by handlers.
508
+ """
509
+ if (template := options.get('template')) is None:
510
+ return None
511
+ if not (format := options.get('format')):
512
+ return None
513
+ template = f'{template}.{format}'
514
+
515
+ if self._templates is None:
516
+ self.warmup()
517
+ templates = self._templates or {}
518
+
519
+ # Variants
520
+ handler = options.get('handler')
521
+ if (variant := options.get('variant')) is not None:
522
+ variant = f'{template}+{variant}'
523
+ handlers = templates.get(variant, [])
524
+ if (handlers and handler is None) or handler in handlers:
525
+ template = variant
526
+
527
+ return f'{template}.{handler}' if handler is not None else template
528
+
529
+ def template_path(self, options: dict[str, Any]) -> str | None:
530
+ """::
531
+
532
+ path = renderer.template_path({'template': 'foo/bar', 'format': 'html', 'handler': 'epl'})
533
+
534
+ Return the full template path for an options dict with ``template``,
535
+ ``format``, ``variant`` and ``handler`` values, or ``None`` if the file
536
+ does not exist in :attr:`paths`, usually used by handlers.
537
+ """
538
+ if not (name := self.template_name(options)):
539
+ return None
540
+ parts = name.split('/')
541
+ for path in [*self.paths, _TEMPLATES]:
542
+ if os.access(file := Pyjo.File.path(path, *parts).to_str(), os.R_OK):
543
+ return file
544
+ return None
545
+
546
+ def warmup(self) -> None:
547
+ """::
548
+
549
+ renderer.warmup()
550
+
551
+ Prepare templates from :attr:`classes` and :attr:`templates` for
552
+ future use.
553
+ """
554
+ index: dict[str, str] = {}
555
+ templates: dict[str, list[str]] = {}
556
+ self._index, self._templates = index, templates
557
+
558
+ # Handlers for templates
559
+ for path in [*self.paths, _TEMPLATES]:
560
+ for file in Pyjo.File.path(path).list_tree():
561
+ name = '/'.join(file.to_rel(path).to_list())
562
+ if m := re.search(r'\.(\w+)$', name):
563
+ templates.setdefault(name[: m.start()], []).append(m[1])
564
+
565
+ # Handlers and classes for DATA templates
566
+ for cls in reversed(self.classes):
567
+ keys = sorted(cast(dict[str, Any], Pyjo.Loader.data_section(cls) or {}))
568
+ for key in keys:
569
+ index[key] = cls
570
+ for key in reversed(keys):
571
+ if m := re.search(r'\.(\w+)$', key):
572
+ templates.setdefault(key[: m.start()], []).insert(0, m[1])
573
+
574
+ # Not in Mojolicious: the handler of the templates of functions
575
+ for key in self.templates:
576
+ templates.setdefault(key, []).insert(0, 'py')
577
+
578
+ def _render_template(self, c: 'Pyjoyment.Controller.object', options: dict[str, Any]) -> str | bytes | None:
579
+ if not options.get('handler'):
580
+ options['handler'] = self.template_handler(options)
581
+ if not (handler := options['handler']):
582
+ return None
583
+ if (renderer := self.handlers.get(handler)) is None:
584
+ c.helpers.log().error(f'No handler for "{handler}" found')
585
+ return None
586
+
587
+ output = _Output()
588
+ renderer(self, c, output, options)
589
+ return output.value() if output.defined else None
590
+
591
+
592
+ class Pyjoyment_Renderer_Helpers:
593
+ """
594
+ The proxy object of :meth:`Pyjoyment_Renderer.get_helper`, which contains
595
+ the current controller object and on which nested helpers can be called.
596
+ """
597
+
598
+ def __init__(self, c: Any) -> None:
599
+ self._c = c
600
+
601
+ def __getattr__(self, name: str) -> Any:
602
+ package = f'{type(self).__module__}.{type(self).__qualname__}'
603
+
604
+ # The location of the caller, which croak adds to the message
605
+ caller = sys._getframe(1) # pyright: ignore[reportPrivateUsage]
606
+ at = f'at {caller.f_code.co_filename} line {caller.f_lineno}'
607
+
608
+ # Not in Mojolicious: the nested helper that the name most likely meant
609
+ suggestion = None if name.startswith('_') else _suggest(name, dir(type(self)))
610
+ mean = f' Did you mean "{suggestion}"?' if suggestion is not None else ''
611
+ raise AttributeError(
612
+ f'Can\'t locate object method "{name}" via package "{package}" {at}.{mean}\n', name=name, obj=self
613
+ )
614
+
615
+
616
+ # The output of a handler, which tells whether the handler wrote any, as Mojo checks that it is defined, and which
617
+ # holds bytes instead of characters for a handler that writes them, as one that removes the encoding of the options
618
+ class _Output(io.StringIO):
619
+ defined = False
620
+ data: bytearray | None = None
621
+
622
+ def write(self, s: str | bytes, /) -> int:
623
+ self.defined = True
624
+ if isinstance(s, bytes):
625
+ self.data = (self.data or bytearray()) + s
626
+ return len(s)
627
+ return super().write(s)
628
+
629
+ def value(self) -> str | bytes:
630
+ return bytes(self.data) if self.data is not None else self.getvalue()
631
+
632
+
633
+ _MISSING: Any = Pyjo.Base.object()
634
+
635
+
636
+ # A nested helper is a method of the proxy, a nested prefix a property that makes its proxy
637
+ def _attribute(sub: Any) -> Any:
638
+ if isinstance(sub, type):
639
+ proxy = cast(type[Pyjoyment_Renderer_Helpers], sub)
640
+ return property(lambda helpers: proxy(helpers._c)) # pyright: ignore[reportPrivateUsage]
641
+
642
+ def method(helpers: Pyjoyment_Renderer_Helpers, *args: Any, **kwargs: Any) -> Any:
643
+ return sub(helpers._c, *args, **kwargs) # pyright: ignore[reportPrivateUsage]
644
+
645
+ return method
646
+
647
+
648
+ def _maybe(encoding: str | None, output: str | bytes) -> str | bytes:
649
+ return encode(encoding, output) if encoding and isinstance(output, str) else output
650
+
651
+
652
+ # Content that is not only whitespace, of characters or of the bytes of a handler
653
+ def _visible(output: str | bytes) -> bool:
654
+ return bool(re.search(rb'\S', output) if isinstance(output, bytes) else re.search(r'\S', output))
655
+
656
+
657
+ def _next(stash: dict[str, Any]) -> str | None:
658
+ if stash.get('extends'):
659
+ return stash.pop('extends')
660
+ if not (layout := stash.pop('layout', None)):
661
+ return None
662
+ return f'layouts/{layout}'
663
+
664
+
665
+ def _teardown(namespaces: list[type[Pyjoyment_Renderer_Helpers]]) -> None:
666
+ for cls in namespaces:
667
+ for name in [name for name in vars(cls) if not name.startswith('__')]:
668
+ delattr(cls, name)
669
+
670
+
671
+ new = Pyjoyment_Renderer.new
672
+ object = Pyjoyment_Renderer
673
+ Renderer = Pyjoyment_Renderer