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,1202 @@
1
+ """
2
+ Pyjoyment.Plugin.DefaultHelpers - Default helpers plugin
3
+ ========================================================
4
+ ::
5
+
6
+ # Pyjoyment
7
+ app.plugin('DefaultHelpers')
8
+
9
+ :mod:`Pyjoyment.Plugin.DefaultHelpers` is a collection of helpers for
10
+ :mod:`Pyjoyment`.
11
+
12
+ This is a core plugin, that means it is always enabled and its code a good
13
+ example for learning to build new plugins, you're welcome to fork it.
14
+
15
+ See :mod:`Pyjoyment.Plugins` for a list of plugins that are available by
16
+ default.
17
+
18
+ Helpers
19
+ -------
20
+
21
+ :mod:`Pyjoyment.Plugin.DefaultHelpers` implements the following helpers.
22
+
23
+ accepts ::
24
+
25
+ formats = c.accepts()
26
+ format = c.accepts('html', 'json', 'txt')
27
+
28
+ Select best possible representation for resource from ``format``
29
+ ``GET``/``POST`` parameter, ``format`` stash value or ``Accept`` request header
30
+ with :meth:`Pyjoyment.Renderer.Pyjoyment_Renderer.accepts`, defaults to
31
+ returning the first extension if no preference could be detected. ::
32
+
33
+ # Check if JSON is acceptable
34
+ if c.accepts('json'):
35
+ c.render(json={'hello': 'world'})
36
+
37
+ # Check if JSON was specifically requested
38
+ if c.accepts('', 'json'):
39
+ c.render(json={'hello': 'world'})
40
+
41
+ # Unsupported representation
42
+ if not (format := c.accepts('html', 'json')):
43
+ c.render(data=b'', status=204)
44
+
45
+ # Detected representations to select from
46
+ formats = c.accepts()
47
+
48
+ app ::
49
+
50
+ %= app().secrets[0]
51
+
52
+ Alias for :attr:`Pyjoyment.Controller.Pyjoyment_Controller.app`.
53
+
54
+ b ::
55
+
56
+ %= b(b'Joel is a slug').slugify()
57
+
58
+ Turn bytes into a :mod:`Pyjo.String.Bytes` object.
59
+
60
+ c ::
61
+
62
+ %= c.c('a', 'b', 'c').shuffle().join()
63
+
64
+ Turn list into a :mod:`Pyjo.Collection` object. In templates, the variable
65
+ ``c`` is the controller, which hides the helper.
66
+
67
+ config ::
68
+
69
+ %= config('something')
70
+
71
+ Alias for :meth:`Pyjo.App.Pyjo_App.config`.
72
+
73
+ content ::
74
+
75
+ %= content('foo', begin
76
+ test
77
+ % end)
78
+ %= content('bar', 'Hello World!')
79
+ %= content('foo')
80
+ %= content('bar')
81
+ %= content()
82
+
83
+ Store partial rendered content in a named buffer and retrieve it later,
84
+ defaults to retrieving the named buffer ``content``, which is used by the
85
+ renderer for the ``layout`` and ``extends`` features. New content will be
86
+ ignored if the named buffer is already in use.
87
+
88
+ content_for ::
89
+
90
+ % content_for('foo', begin
91
+ test
92
+ % end)
93
+ %= content_for('foo')
94
+
95
+ Same as ``content``, but appends content to named buffers if they are already
96
+ in use. ::
97
+
98
+ % content_for('message', begin
99
+ Hello
100
+ % end)
101
+ % content_for('message', begin
102
+ world!
103
+ % end)
104
+ %= content('message')
105
+
106
+ content_with ::
107
+
108
+ % content_with('foo', begin
109
+ test
110
+ % end)
111
+ %= content_with('foo')
112
+
113
+ Same as ``content``, but replaces content of named buffers if they are already
114
+ in use. ::
115
+
116
+ % content('message', begin
117
+ world!
118
+ % end)
119
+ % content_with('message', begin
120
+ Hello <%= content('message') %>
121
+ % end)
122
+ %= content('message')
123
+
124
+ csrf_token ::
125
+
126
+ %= csrf_token()
127
+
128
+ Get CSRF token from ``session``, and generate one if none exists. The token is
129
+ masked with a fresh random value on every call to protect against BREACH
130
+ attacks.
131
+
132
+ current_route ::
133
+
134
+ % if current_route('login'):
135
+ Welcome to Pyjoyment!
136
+ % end
137
+ %= current_route()
138
+
139
+ Check or get name of current route.
140
+
141
+ dumper ::
142
+
143
+ %= dumper({'some': 'data'})
144
+
145
+ Dump a Python data structure with :func:`Pyjo.Util.dumper`, very useful for
146
+ debugging.
147
+
148
+ exception_format ::
149
+
150
+ format = c.exception_format()
151
+ c = c.exception_format('txt')
152
+
153
+ Format for HTTP exceptions (``html``, ``json``, or ``txt``), defaults to the
154
+ value of :attr:`Pyjoyment.Pyjoyment_App.exception_format`.
155
+
156
+ extends ::
157
+
158
+ % extends('blue')
159
+ % extends('blue', title='Blue!')
160
+
161
+ Set ``extends`` stash value, all additional keyword arguments get merged into
162
+ the ``stash``.
163
+
164
+ flash ::
165
+
166
+ foo = c.flash('foo')
167
+ c = c.flash({'foo': 'bar'})
168
+ c = c.flash(foo='bar')
169
+ %= flash('foo')
170
+
171
+ Data storage persistent only for the next request, stored in the ``session``. ::
172
+
173
+ # Show message after redirect
174
+ c.flash(message='User created successfully!')
175
+ c.redirect_to('show_user', id=23)
176
+
177
+ inactivity_timeout ::
178
+
179
+ c = c.inactivity_timeout(3600)
180
+
181
+ Use :func:`Pyjo.IOLoop.stream` to find the current connection and increase
182
+ timeout if possible. ::
183
+
184
+ # Longer version
185
+ Pyjo.IOLoop.stream(c.tx.connection).timeout = 3600
186
+
187
+ include ::
188
+
189
+ %= include('menubar')
190
+ %= include('menubar', format='txt')
191
+
192
+ Alias for :meth:`Pyjoyment.Controller.Pyjoyment_Controller.render_to_string`.
193
+
194
+ is_fresh ::
195
+
196
+ boolean = c.is_fresh()
197
+ boolean = c.is_fresh(etag='abc')
198
+ boolean = c.is_fresh(etag='W/"def"')
199
+ boolean = c.is_fresh(last_modified=epoch)
200
+
201
+ Check freshness of request by comparing the ``If-None-Match`` and
202
+ ``If-Modified-Since`` request headers to the ``ETag`` and ``Last-Modified``
203
+ response headers with :meth:`Pyjoyment.Static.Pyjoyment_Static.is_fresh`. ::
204
+
205
+ # Add ETag/Last-Modified headers and check freshness before rendering
206
+ if c.is_fresh(etag='abc', last_modified=1424985708):
207
+ c.rendered(304)
208
+ else:
209
+ c.render(text='I ♥ Pyjoyment!')
210
+
211
+ layout ::
212
+
213
+ % layout('green')
214
+ % layout('green', title='Green!')
215
+
216
+ Set ``layout`` stash value, all additional keyword arguments get merged into
217
+ the ``stash``.
218
+
219
+ log ::
220
+
221
+ log = c.log()
222
+
223
+ Alternative to :attr:`Pyjoyment.Pyjoyment_App.log` that includes
224
+ :attr:`Pyjo.Message.Request.Pyjo_Message_Request.request_id` with every log
225
+ message, as the field ``request_id`` of a message of JSON, see
226
+ :class:`Pyjo.Log.Field`. ::
227
+
228
+ # Log message with context
229
+ c.log().debug('This is a log message with request id')
230
+
231
+ # Pass logger with context to model
232
+ log = c.log()
233
+ c.some_model().create({'foo': foo}, log)
234
+
235
+ param ::
236
+
237
+ %= param('foo')
238
+
239
+ Alias for :meth:`Pyjoyment.Controller.Pyjoyment_Controller.param`.
240
+
241
+ proxy.get_p ::
242
+
243
+ promise = c.proxy.get_p('http://example.com', {'Accept': '*/*'})
244
+
245
+ Perform non-blocking ``GET`` request and forward response as efficiently as
246
+ possible, takes the same arguments as
247
+ :attr:`Pyjo.UserAgent.Pyjo_UserAgent.get` and returns a :mod:`Pyjo.Promise`
248
+ object. ::
249
+
250
+ # Forward with exception handling
251
+ @c.proxy.get_p('http://mojolicious.org').catch
252
+ def catch(err):
253
+ c.log().debug(f'Proxy error: {err}')
254
+ c.render(text='Something went wrong!', status=400)
255
+
256
+ proxy.post_p ::
257
+
258
+ promise = c.proxy.post_p('http://example.com', {'Accept': '*/*'})
259
+
260
+ Perform non-blocking ``POST`` request and forward response as efficiently as
261
+ possible, takes the same arguments as
262
+ :attr:`Pyjo.UserAgent.Pyjo_UserAgent.post` and returns a :mod:`Pyjo.Promise`
263
+ object. ::
264
+
265
+ # Forward with exception handling
266
+ @c.proxy.post_p('example.com', form={'test': 'pass'}).catch
267
+ def catch(err):
268
+ c.log().debug(f'Proxy error: {err}')
269
+ c.render(text='Something went wrong!', status=400)
270
+
271
+ proxy.start_p ::
272
+
273
+ promise = c.proxy.start_p(Pyjo.Transaction.HTTP.new())
274
+
275
+ Perform non-blocking request for a custom :mod:`Pyjo.Transaction.HTTP` object
276
+ and forward response as efficiently as possible, returns a :mod:`Pyjo.Promise`
277
+ object. ::
278
+
279
+ # Forward with exception handling
280
+ tx = c.ua().build_tx('GET', 'http://mojolicious.org')
281
+
282
+ @c.proxy.start_p(tx).catch
283
+ def catch(err):
284
+ c.log().debug(f'Proxy error: {err}')
285
+ c.render(text='Something went wrong!', status=400)
286
+
287
+ # Forward with custom request and response headers
288
+ headers = c.req.headers.clone().dehop()
289
+ headers.header('X-Proxy', 'Pyjo')
290
+ tx = c.ua().build_tx('GET', 'http://example.com', headers.to_dict())
291
+ c.proxy.start_p(tx)
292
+ tx.res.content.once('body', lambda content: c.res.headers.header('X-Proxy', 'Pyjo'))
293
+
294
+ redirect_to ::
295
+
296
+ c = c.redirect_to('named', foo='bar')
297
+ c = c.redirect_to('named', {'foo': 'bar'})
298
+ c = c.redirect_to('/index.html')
299
+ c = c.redirect_to('http://example.com/index.html')
300
+
301
+ Prepare a ``302`` (if the status code is not already ``3xx``) redirect response
302
+ with ``Location`` header, takes the same arguments as ``url_for``. ::
303
+
304
+ # Moved Permanently
305
+ c.res.code = 301
306
+ c.redirect_to('some_route')
307
+
308
+ # Temporary Redirect
309
+ c.res.code = 307
310
+ c.redirect_to('some_route')
311
+
312
+ reply.asset ::
313
+
314
+ c.reply.asset(Pyjo.Asset.File.new())
315
+
316
+ Reply with a :mod:`Pyjo.Asset.File` or :mod:`Pyjo.Asset.Memory` object using
317
+ :meth:`Pyjoyment.Static.Pyjoyment_Static.serve_asset`, and perform content
318
+ negotiation with ``Range``, ``If-Modified-Since`` and ``If-None-Match``
319
+ headers. ::
320
+
321
+ # Serve asset with custom modification time
322
+ asset = Pyjo.Asset.Memory.new()
323
+ asset.add_chunk(b'Hello World!').mtime = 784111777
324
+ c.res.headers.content_type = 'text/plain'
325
+ c.reply.asset(asset)
326
+
327
+ # Serve static file if it exists
328
+ if asset := c.app.static.file('images/logo.png'):
329
+ c.res.headers.content_type = 'image/png'
330
+ c.reply.asset(asset)
331
+
332
+ reply.exception ::
333
+
334
+ c = c.reply.exception('Oops!')
335
+ c = c.reply.exception(Pyjo.Exception.new())
336
+
337
+ Render an exception response in the appropriate format by delegating to more
338
+ specific exception helpers.
339
+
340
+ reply.file ::
341
+
342
+ c.reply.file('/etc/passwd')
343
+
344
+ Reply with a static file from an absolute path anywhere on the file system
345
+ using :attr:`Pyjoyment.Pyjoyment_App.static`. ::
346
+
347
+ # Longer version
348
+ c.reply.asset(Pyjo.Asset.File.new(path='/etc/passwd'))
349
+
350
+ # Serve file from an absolute path with a custom content type
351
+ c.res.headers.content_type = 'application/myapp'
352
+ c.reply.file('/home/sri/foo.txt')
353
+
354
+ # Serve file from a secret application directory
355
+ c.reply.file(c.app.home.child('secret', 'file.txt'))
356
+
357
+ reply.html_exception ::
358
+
359
+ c = c.reply.html_exception('Oops!')
360
+ c = c.reply.html_exception(Pyjo.Exception.new())
361
+
362
+ Render the exception template ``exception.$mode.$format.*`` or
363
+ ``exception.$format.*`` and set the response status code to ``500``. Also sets
364
+ the stash values ``exception`` to a :mod:`Pyjo.Exception` object and
365
+ ``snapshot`` to a copy of the ``stash`` for use in the templates.
366
+
367
+ reply.html_not_found ::
368
+
369
+ c = c.reply.html_not_found()
370
+
371
+ Render the not found template ``not_found.$mode.$format.*`` or
372
+ ``not_found.$format.*`` and set the response status code to ``404``. Also sets
373
+ the stash value ``snapshot`` to a copy of the ``stash`` for use in the
374
+ templates.
375
+
376
+ reply.json_exception ::
377
+
378
+ c = c.reply.json_exception('Oops!')
379
+ c = c.reply.json_exception(Pyjo.Exception.new())
380
+
381
+ Render a JSON response and set the response status to ``500``.
382
+
383
+ reply.json_not_found ::
384
+
385
+ c = c.reply.json_not_found()
386
+
387
+ Render a JSON response and set the response status to ``404``.
388
+
389
+ reply.not_found ::
390
+
391
+ c = c.reply.not_found()
392
+
393
+ Render a not found response in the appropriate format by delegating to more
394
+ specific exception helpers.
395
+
396
+ reply.static ::
397
+
398
+ c.reply.static('images/logo.png')
399
+ c.reply.static('../lib/MyApp.py')
400
+
401
+ Reply with a static file using :attr:`Pyjoyment.Pyjoyment_App.static`, usually
402
+ from the ``public`` directories or ``DATA`` sections of your application. Note
403
+ that this helper uses a relative path, but does not protect from traversing to
404
+ parent directories. ::
405
+
406
+ # Serve file from a relative path with a custom content type
407
+ c.res.headers.content_type = 'application/myapp'
408
+ c.reply.static('foo.txt')
409
+
410
+ reply.txt_exception ::
411
+
412
+ c = c.reply.txt_exception('Oops!')
413
+ c = c.reply.txt_exception(Pyjo.Exception.new())
414
+
415
+ Render a plain text response and set the response status to ``500``.
416
+
417
+ reply.txt_not_found ::
418
+
419
+ c = c.reply.txt_not_found()
420
+
421
+ Render a plain text response and set the response status to ``404``.
422
+
423
+ respond_to ::
424
+
425
+ c = c.respond_to(json={'json': {'message': 'Welcome!'}}, html={'template': 'welcome'}, any=lambda c: ...)
426
+
427
+ Automatically select best possible representation for resource from ``format``
428
+ ``GET``/``POST`` parameter, ``format`` stash value or ``Accept`` request
429
+ header, defaults to
430
+ :attr:`Pyjoyment.Renderer.Pyjoyment_Renderer.default_format` or rendering an
431
+ empty ``204`` response. Each representation can be handled with a callback or
432
+ a dict containing arguments to be passed to
433
+ :meth:`Pyjoyment.Controller.Pyjoyment_Controller.render`. ::
434
+
435
+ # Everything else than "json" and "xml" gets a 204 response
436
+ c.respond_to(
437
+ json=lambda c: c.render(json={'just': 'works'}),
438
+ xml={'text': '<just>works</just>'},
439
+ any={'data': b'', 'status': 204},
440
+ )
441
+
442
+ For more advanced negotiation logic you can also use ``accepts``.
443
+
444
+ session ::
445
+
446
+ %= session('foo')
447
+
448
+ Alias for :meth:`Pyjoyment.Controller.Pyjoyment_Controller.session`.
449
+
450
+ stash ::
451
+
452
+ %= stash('foo')
453
+ % stash(foo='bar')
454
+
455
+ Alias for :meth:`Pyjoyment.Controller.Pyjoyment_Controller.stash`. ::
456
+
457
+ %= stash('name') or 'Somebody'
458
+
459
+ timing.begin ::
460
+
461
+ c.timing.begin('foo')
462
+
463
+ Create named timestamp for ``timing.elapsed``.
464
+
465
+ timing.elapsed ::
466
+
467
+ elapsed = c.timing.elapsed('foo')
468
+
469
+ Return fractional amount of time in seconds since named timstamp has been
470
+ created with ``timing.begin`` or ``None`` if no such timestamp exists. ::
471
+
472
+ # Log timing information
473
+ c.timing.begin('database_stuff')
474
+ ...
475
+ elapsed = c.timing.elapsed('database_stuff')
476
+ c.app.log.debug(f'Database stuff took {elapsed} seconds')
477
+
478
+ timing.rps ::
479
+
480
+ rps = c.timing.rps(0.001)
481
+
482
+ Return fractional number of requests that could be performed in one second if
483
+ every singe one took the given amount of time in seconds or ``None`` if the
484
+ number is too low. ::
485
+
486
+ # Log more timing information
487
+ c.timing.begin('web_stuff')
488
+ ...
489
+ elapsed = c.timing.elapsed('web_stuff')
490
+ rps = c.timing.rps(elapsed)
491
+ c.app.log.debug(f'Web stuff took {elapsed} seconds ({rps} per second)')
492
+
493
+ timing.server_timing ::
494
+
495
+ c.timing.server_timing('metric')
496
+ c.timing.server_timing('metric', 'Some Description')
497
+ c.timing.server_timing('metric', 'Some Description', 0.001)
498
+
499
+ Create ``Server-Timing`` header with optional description and duration. ::
500
+
501
+ # "Server-Timing: miss"
502
+ c.timing.server_timing('miss')
503
+
504
+ # "Server-Timing: dc;desc=atl"
505
+ c.timing.server_timing('dc', 'atl')
506
+
507
+ # "Server-Timing: db;desc=Database;dur=0.0001"
508
+ c.timing.begin('database_stuff')
509
+ ...
510
+ elapsed = c.timing.elapsed('database_stuff')
511
+ c.timing.server_timing('db', 'Database', elapsed)
512
+
513
+ # "Server-Timing: miss, dc;desc=atl"
514
+ c.timing.server_timing('miss')
515
+ c.timing.server_timing('dc', 'atl')
516
+
517
+ title ::
518
+
519
+ %= title()
520
+ % title('Welcome!')
521
+ % title('Welcome!', foo='bar')
522
+
523
+ Get or set ``title`` stash value, all additional keyword arguments get merged
524
+ into the ``stash``.
525
+
526
+ ua ::
527
+
528
+ %= ua().get('mojolicious.org').result.dom().at('title').text
529
+
530
+ Alias for :attr:`Pyjo.App.Pyjo_App.ua`.
531
+
532
+ url_for ::
533
+
534
+ %= url_for('named', foo='bar', baz='yada')
535
+
536
+ Alias for :meth:`Pyjoyment.Controller.Pyjoyment_Controller.url_for`. ::
537
+
538
+ %= url_for('/index.html').set(query={'foo': 'bar'})
539
+
540
+ url_for_asset ::
541
+
542
+ %= url_for_asset('/app.js')
543
+
544
+ Alias for :meth:`Pyjoyment.Controller.Pyjoyment_Controller.url_for_asset`.
545
+
546
+ url_for_file ::
547
+
548
+ %= url_for_file('/index.html')
549
+
550
+ Alias for :meth:`Pyjoyment.Controller.Pyjoyment_Controller.url_for_file`.
551
+
552
+ url_with ::
553
+
554
+ %= url_with('named', foo='bar', baz='yada')
555
+
556
+ Does the same as ``url_for``, but inherits query parameters from the current
557
+ request. ::
558
+
559
+ %= url_with().set(query={'page': 2})
560
+
561
+ validation ::
562
+
563
+ v = c.validation()
564
+
565
+ Get :mod:`Pyjoyment.Validator.Validation` object for current request to
566
+ validate file uploads as well as ``GET`` and ``POST`` parameters extracted
567
+ from the query string and ``application/x-www-form-urlencoded`` or
568
+ ``multipart/form-data`` message body. Parts of the request body need to be
569
+ loaded into memory to parse ``POST`` parameters, so you have to make sure it
570
+ is not excessively large. There's a 16MiB limit for requests by default. ::
571
+
572
+ # Validate GET/POST parameter
573
+ v = c.validation()
574
+ v.required('title', 'trim').size(3, 50)
575
+ title = v.param('title')
576
+
577
+ # Validate file upload
578
+ v = c.validation()
579
+ v.required('tarball').upload().size(1, 1048576)
580
+ tarball = v.param('tarball')
581
+
582
+ See also
583
+ --------
584
+
585
+ :mod:`Pyjoyment`, https://mojolicious.org.
586
+
587
+ Ported from `Mojolicious::Plugin::DefaultHelpers
588
+ <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojolicious/Plugin/DefaultHelpers.pm>`_ of Mojolicious 9.49.
589
+ """
590
+
591
+ import Pyjo.Asset
592
+ import Pyjo.Asset.File
593
+ import Pyjo.Collection
594
+ import Pyjo.Exception
595
+ import Pyjo.IOLoop
596
+ import Pyjo.Log
597
+ import Pyjo.Promise
598
+ import Pyjo.String.Bytes
599
+ import Pyjo.String.HTML
600
+ import Pyjo.String.Unicode
601
+ import Pyjo.URL
602
+ import Pyjo.UserAgent
603
+ import Pyjoyment.Plugin
604
+
605
+ from Pyjo.Util import dumper, random_bytes
606
+ from Pyjoyment.Controller import Helper
607
+
608
+ import functools
609
+ import time
610
+ import weakref
611
+
612
+ from collections.abc import Callable
613
+ from typing import TYPE_CHECKING, Any
614
+
615
+
616
+ if TYPE_CHECKING:
617
+ import Pyjoyment
618
+ import Pyjoyment.Controller
619
+ import Pyjoyment.Validator.Validation
620
+
621
+ type Controller = Pyjoyment.Controller.object
622
+
623
+
624
+ class Pyjoyment_Plugin_DefaultHelpers(Pyjoyment.Plugin.object):
625
+ """
626
+ :mod:`Pyjoyment.Plugin.DefaultHelpers` inherits all methods from
627
+ :mod:`Pyjoyment.Plugin` and implements the following new ones.
628
+ """
629
+
630
+ def register(self, app: 'Pyjoyment.object', conf: dict[str, Any] | None = None) -> None:
631
+ """::
632
+
633
+ plugin.register(Pyjoyment.new())
634
+
635
+ Register helpers in :mod:`Pyjoyment` application.
636
+ """
637
+ # Controller alias helpers
638
+ app.helper('app', _app)
639
+ for name in ('param', 'stash', 'session', 'url_for', 'url_for_asset', 'url_for_file'):
640
+ app.helper(name, _alias(name))
641
+
642
+ # Stash key shortcuts (should not generate log messages)
643
+ for name in ('extends', 'layout', 'title'):
644
+ app.helper(name, _stash_key(name))
645
+
646
+ app.helper('accepts', _accepts)
647
+ app.helper('b', _b)
648
+ # Not in Mojolicious: u, as Pyjo.String.Unicode is the Mojo::ByteStream of characters
649
+ app.helper('u', _u)
650
+ app.helper('c', _c)
651
+ app.helper('config', _config)
652
+
653
+ app.helper('content', functools.partial(_content, False, False))
654
+ app.helper('content_for', functools.partial(_content, True, False))
655
+ app.helper('content_with', functools.partial(_content, False, True))
656
+
657
+ app.helper('csrf_token', _csrf_token)
658
+ app.helper('current_route', _current_route)
659
+ app.helper('exception_format', _exception_format)
660
+ app.helper('flash', _flash)
661
+ app.helper('inactivity_timeout', _inactivity_timeout)
662
+ app.helper('is_fresh', _is_fresh)
663
+ app.helper('redirect_to', _redirect_to)
664
+ app.helper('respond_to', _respond_to)
665
+ app.helper('url_with', _url_with)
666
+ app.helper('validation', _validation)
667
+
668
+ app.helper('dumper', _dumper)
669
+ app.helper('include', _include)
670
+
671
+ app.helper('log', _log)
672
+
673
+ app.helper('proxy.get_p', functools.partial(_proxy_method_p, 'GET'))
674
+ app.helper('proxy.post_p', functools.partial(_proxy_method_p, 'POST'))
675
+ app.helper('proxy.start_p', _proxy_start_p)
676
+
677
+ app.helper('reply.asset', _asset)
678
+ app.helper('reply.file', _file)
679
+ app.helper('reply.static', _static)
680
+
681
+ app.helper('reply.exception', _exception)
682
+ app.helper('reply.not_found', _not_found)
683
+ app.helper('reply.http_exception', _http_exception)
684
+ app.helper('reply.http_not_found', _http_not_found)
685
+ app.helper('reply.html_exception', functools.partial(_development, 'exception'))
686
+ app.helper('reply.html_not_found', functools.partial(_development, 'not_found'))
687
+ app.helper('reply.json_exception', _json_exception)
688
+ app.helper('reply.json_not_found', _json_not_found)
689
+ app.helper('reply.txt_exception', _txt_exception)
690
+ app.helper('reply.txt_not_found', _txt_not_found)
691
+
692
+ app.helper('timing.begin', _timing_begin)
693
+ app.helper('timing.elapsed', _timing_elapsed)
694
+ app.helper('timing.rps', _timing_rps)
695
+ app.helper('timing.server_timing', _timing_server_timing)
696
+
697
+ app.helper('ua', _ua)
698
+
699
+
700
+ def _accepts(c: 'Controller', *formats: str) -> list[str] | str | None:
701
+ return c.app.renderer.accepts(c, *formats)
702
+
703
+
704
+ def _alias(name: str) -> Callable[..., Any]:
705
+ def helper(c: 'Controller', *args: Any, **values: Any) -> Any:
706
+ return getattr(c, name)(*args, **values)
707
+
708
+ return helper
709
+
710
+
711
+ def _app(c: 'Controller') -> 'Pyjoyment.object':
712
+ return c.app
713
+
714
+
715
+ def _asset(c: 'Controller', asset: Pyjo.Asset.object) -> 'Controller':
716
+ c.app.static.serve_asset(c, asset)
717
+ return c.rendered()
718
+
719
+
720
+ def _b(c: 'Controller', *args: Any) -> Pyjo.String.Bytes.object:
721
+ return Pyjo.String.Bytes.new(*args)
722
+
723
+
724
+ def _block(content: Any) -> Any:
725
+ return content() if callable(content) else content
726
+
727
+
728
+ def _c(c: 'Controller', *args: Any) -> Pyjo.Collection.object[Any]:
729
+ return Pyjo.Collection.new(*args)
730
+
731
+
732
+ def _config(c: 'Controller', *args: Any, **values: Any) -> Any:
733
+ return c.app.config(*args, **values)
734
+
735
+
736
+ def _content(append: bool, replace: bool, c: 'Controller', name: str | None = None, content: Any = None) -> Any:
737
+ name = name or 'content'
738
+
739
+ hash = c.stash().setdefault('mojo.content', {})
740
+ if content is not None:
741
+ if append:
742
+ # Joined as strings, as Mojo does: the concatenation of markup would escape a string that is not markup
743
+ hash[name] = str(hash.get(name) or '') + str(_block(content))
744
+ if replace:
745
+ hash[name] = _block(content)
746
+ elif hash.get(name) is None:
747
+ hash[name] = _block(content)
748
+
749
+ content = hash.get(name)
750
+ return Pyjo.String.HTML.new(content if content is not None else '')
751
+
752
+
753
+ def _convert_to_exception(e: Any) -> Pyjo.Exception.object:
754
+ return e if isinstance(e, Pyjo.Exception.object) else Pyjo.Exception.new(e)
755
+
756
+
757
+ def _csrf_token(c: 'Controller') -> str:
758
+ # Generate token and store it in the session
759
+ session = c.session()
760
+ if not session.get('csrf_token'):
761
+ session['csrf_token'] = random_bytes(20).hex()
762
+
763
+ # Mask token to protect against BREACH attacks
764
+ mask = random_bytes(20)
765
+ return mask.hex() + bytes(a ^ b for a, b in zip(mask, bytes.fromhex(session['csrf_token']), strict=False)).hex()
766
+
767
+
768
+ def _current_route(c: 'Controller', *args: str) -> str | bool | None:
769
+ if (route := c.match.endpoint) is None:
770
+ return ''
771
+ return route.name == args[0] if args else route.name
772
+
773
+
774
+ def _development(page: str, c: 'Controller', e: Any = None) -> 'Controller':
775
+ if page == 'exception':
776
+ c.helpers.log().error((e := _convert_to_exception(e)).inspect())
777
+
778
+ # Filtered stash snapshot
779
+ stash = c.stash()
780
+ stash['snapshot'] = {
781
+ key: 'DUMMY' if key == 'app' else value
782
+ for key, value in stash.items()
783
+ if not key.startswith('mojo.') and value is not None
784
+ }
785
+ stash['exception'] = e if page == 'exception' else None
786
+
787
+ # Render with fallbacks
788
+ app = c.app
789
+ mode = app.mode
790
+ options = {
791
+ 'format': stash.get('format') or app.renderer.default_format,
792
+ 'handler': None,
793
+ 'status': 500 if page == 'exception' else 404,
794
+ 'template': f'{page}.{mode}',
795
+ }
796
+ # Renamed from Mojolicious: the bundled templates are in pyjo/, where Mojo has them in mojo/
797
+ bundled = 'pyjo/' + ('debug' if mode == 'development' else page)
798
+ if _fallbacks(c, options, page, bundled):
799
+ return c
800
+ _fallbacks(c, {**options, 'format': 'html'}, page, bundled)
801
+ return c
802
+
803
+
804
+ def _dumper(c: 'Controller', *values: Any) -> str:
805
+ return dumper(*values)
806
+
807
+
808
+ def _exception(c: 'Controller', e: Any = None) -> Any:
809
+ return c.helpers.reply.http_exception(e)
810
+
811
+
812
+ def _exception_format(c: 'Controller', *args: str) -> Any:
813
+ stash = c.stash()
814
+ if not stash.get('mojo.exception_format'):
815
+ stash['mojo.exception_format'] = c.app.exception_format
816
+ if not args:
817
+ return stash['mojo.exception_format']
818
+ stash['mojo.exception_format'] = args[0]
819
+ return c
820
+
821
+
822
+ def _fallbacks(c: 'Controller', options: dict[str, Any], template: str, bundled: str) -> bool:
823
+ # Mode specific template
824
+ if c.render_maybe(**options):
825
+ return True
826
+
827
+ # Normal template
828
+ if c.render_maybe(**{**options, 'template': template}):
829
+ return True
830
+
831
+ # Inline template
832
+ stash = c.stash()
833
+ if options['format'] != 'html':
834
+ return False
835
+ stash.pop('extends', None)
836
+ stash.pop('layout', None)
837
+ return bool(c.render_maybe(bundled, **{**options, 'handler': 'ep'}))
838
+
839
+
840
+ def _file(c: 'Controller', path: Any) -> 'Controller':
841
+ return _asset(c, Pyjo.Asset.File.new(path=str(path)))
842
+
843
+
844
+ def _flash(c: 'Controller', *args: Any, **values: Any) -> Any:
845
+ # Check old flash
846
+ session = c.session()
847
+ if len(args) == 1 and not isinstance(args[0], dict) and not values:
848
+ return flash.get(args[0]) if (flash := session.get('flash')) is not None else None
849
+
850
+ # Initialize new flash and merge values
851
+ new_flash = session.setdefault('new_flash', {})
852
+ for arg in args:
853
+ new_flash.update(arg)
854
+ new_flash.update(values)
855
+
856
+ return c
857
+
858
+
859
+ def _http_exception(c: 'Controller', e: Any) -> Any:
860
+ format = c.exception_format()
861
+ if format == 'txt':
862
+ return c.helpers.reply.txt_exception(e)
863
+ if format == 'json':
864
+ return c.helpers.reply.json_exception(e)
865
+ return c.helpers.reply.html_exception(e)
866
+
867
+
868
+ def _http_not_found(c: 'Controller') -> Any:
869
+ format = c.exception_format()
870
+ if format == 'txt':
871
+ return c.helpers.reply.txt_not_found()
872
+ if format == 'json':
873
+ return c.helpers.reply.json_not_found()
874
+ return c.helpers.reply.html_not_found()
875
+
876
+
877
+ def _inactivity_timeout(c: 'Controller', timeout: float) -> 'Controller':
878
+ tx: Any = c.tx
879
+ if stream := Pyjo.IOLoop.stream(tx.connection or ''):
880
+ stream.timeout = timeout
881
+ return c
882
+
883
+
884
+ def _include(c: 'Controller', *args: Any, **values: Any) -> Pyjo.String.HTML.object | Pyjo.String.Bytes.object | None:
885
+ return c.render_to_string(*args, **values)
886
+
887
+
888
+ def _is_fresh(c: 'Controller', *, etag: str | None = None, last_modified: float | None = None) -> bool:
889
+ return c.app.static.is_fresh(c, etag=etag, last_modified=last_modified)
890
+
891
+
892
+ def _json_exception(c: 'Controller', e: Any) -> Any:
893
+ c.stash()['exception'] = _convert_to_exception(e)
894
+ if c.app.mode == 'development':
895
+ return c.render(json={'error': e}, status=500)
896
+ return c.render(json={'error': 'Internal Server Error'}, status=500)
897
+
898
+
899
+ def _json_not_found(c: 'Controller') -> Any:
900
+ return c.render(json={'error': 'Not Found'}, status=404)
901
+
902
+
903
+ def _log(c: 'Controller') -> Pyjo.Log.object:
904
+ stash = c.stash()
905
+ if not stash.get('mojo.log'):
906
+ # Not in Mojolicious: the request id as a field of its own of a message of JSON
907
+ id = c.req.request_id
908
+ stash['mojo.log'] = c.app.log.context(Pyjo.Log.Field('request_id', id, f'[{id}]'))
909
+ return stash['mojo.log']
910
+
911
+
912
+ def _not_found(c: 'Controller') -> Any:
913
+ return c.helpers.reply.http_not_found()
914
+
915
+
916
+ def _proxy_method_p(method: str, c: 'Controller', *args: Any, **kwargs: Any) -> Pyjo.Promise.object:
917
+ return _proxy_start_p(c, c.ua().build_tx(method, *args, **kwargs))
918
+
919
+
920
+ def _proxy_start_p(c: 'Controller', source_tx: Any) -> Pyjo.Promise.object:
921
+ tx: Any = c.render_later().tx
922
+
923
+ promise = Pyjo.Promise.new()
924
+ source = weakref.ref(source_tx)
925
+
926
+ def body(source_content: Any) -> None:
927
+ source_tx = source()
928
+ if source_tx is None:
929
+ return
930
+
931
+ source_res = source_tx.res
932
+ res = tx.res
933
+ content = res.content
934
+ res.code = source_res.code
935
+ res.message = source_res.message
936
+ headers = source_res.headers.clone().dehop()
937
+ content.headers = headers
938
+ promise.resolve()
939
+
940
+ source_stream = Pyjo.IOLoop.stream(source_tx.connection)
941
+ if not (stream := Pyjo.IOLoop.stream(tx.connection)):
942
+ return
943
+
944
+ write = content.write_chunk if source_content.is_chunked else content.write
945
+
946
+ def read(source_content: Any, data: bytes) -> None:
947
+ write(data if len(data) else None)
948
+ tx.resume()
949
+
950
+ # Throttle transparently when backpressure rises
951
+ if stream.can_write:
952
+ return
953
+ source_stream.stop()
954
+
955
+ @stream.once('drain')
956
+ def drain(stream: Any) -> None:
957
+ source_stream.start()
958
+
959
+ source_content.unsubscribe('read').on('read', read)
960
+
961
+ def finish(source_res: Any) -> None:
962
+ write(b'')
963
+ tx.resume()
964
+
965
+ source_res.once('finish', finish)
966
+
967
+ source_tx.res.content.set(auto_upgrade=False, auto_decompress=False).once('body', body)
968
+
969
+ @source_tx.once('finish')
970
+ def finish(source_tx: Any) -> None:
971
+ promise.reject(_tx_error(source_tx))
972
+
973
+ def ignore(*args: Any) -> None:
974
+ pass
975
+
976
+ c.ua().start_p(source_tx).catch(ignore)
977
+
978
+ return promise
979
+
980
+
981
+ def _redirect_to(c: 'Controller', *args: Any, **values: Any) -> 'Controller':
982
+ # Don't override 3xx status
983
+ res = c.res
984
+ res.headers.location = c.url_for(*args, **values).to_str()
985
+ return c.rendered(None if res.is_redirect else 302)
986
+
987
+
988
+ def _respond_to(c: 'Controller', *args: dict[str, Any], **values: Any) -> 'Controller':
989
+ targets = args[0] if args else values
990
+
991
+ # Find target
992
+ target = None
993
+ renderer = c.app.renderer
994
+ formats: list[str] = renderer.accepts(c) or [] # pyright: ignore[reportAssignmentType]
995
+ for format in formats or [renderer.default_format]:
996
+ if not (target := targets.get(format)):
997
+ continue
998
+ c.stash()['format'] = format
999
+ break
1000
+
1001
+ # Fallback
1002
+ if not target:
1003
+ if not (target := targets.get('any')):
1004
+ return c.rendered(204)
1005
+ c.stash().pop('format', None)
1006
+
1007
+ # Dispatch
1008
+ if callable(target):
1009
+ target(c)
1010
+ else:
1011
+ c.render(**target)
1012
+
1013
+ return c
1014
+
1015
+
1016
+ def _timing_begin(c: 'Controller', name: str) -> None:
1017
+ c.stash().setdefault('mojo.timing', {})[name] = time.time()
1018
+
1019
+
1020
+ def _timing_elapsed(c: 'Controller', name: str) -> float | None:
1021
+ if (started := c.stash().get('mojo.timing', {}).get(name)) is None:
1022
+ return None
1023
+ return time.time() - started
1024
+
1025
+
1026
+ def _timing_rps(c: 'Controller', elapsed: float | str) -> str | None:
1027
+ return None if float(elapsed) == 0 else f'{1 / float(elapsed):.3f}'
1028
+
1029
+
1030
+ def _timing_server_timing(c: 'Controller', metric: str, desc: str | None = None, dur: Any = None) -> None:
1031
+ value = metric
1032
+ if desc is not None:
1033
+ value += f';desc="{desc}"'
1034
+ if dur is not None:
1035
+ value += f';dur={dur}'
1036
+ c.res.headers.append('Server-Timing', value)
1037
+
1038
+
1039
+ def _tx_error(tx: Any) -> str:
1040
+ error: dict[str, Any] = tx.error or {}
1041
+ return str(error.get('message') or 'Unknown error')
1042
+
1043
+
1044
+ def _txt_exception(c: 'Controller', e: Any) -> Any:
1045
+ c.stash()['exception'] = _convert_to_exception(e)
1046
+ if c.app.mode == 'development':
1047
+ return c.render(text=str(e), format='txt', status=500)
1048
+ return c.render(text='Internal Server Error', format='txt', status=500)
1049
+
1050
+
1051
+ def _txt_not_found(c: 'Controller') -> Any:
1052
+ return c.render(text='Not Found', format='txt', status=404)
1053
+
1054
+
1055
+ def _static(c: 'Controller', file: str) -> 'Controller':
1056
+ if not c.app.static.serve(c, file):
1057
+ raise Pyjo.Exception.new(f'Static file "{file}" not found')
1058
+ return c.rendered()
1059
+
1060
+
1061
+ def _stash_key(name: str) -> Callable[..., Any]:
1062
+ def helper(c: 'Controller', *args: Any, **values: Any) -> Any:
1063
+ if not args and not values:
1064
+ return c.stash(name)
1065
+ return c.stash({name: args[0]} if args else {}, **values)
1066
+
1067
+ return helper
1068
+
1069
+
1070
+ def _url_with(c: 'Controller', *args: Any, **values: Any) -> Pyjo.URL.object:
1071
+ url = c.url_for(*args, **values)
1072
+ url.query = c.req.url.query.clone()
1073
+ return url
1074
+
1075
+
1076
+ def _u(c: 'Controller', *args: Any) -> Pyjo.String.Unicode.object:
1077
+ return Pyjo.String.Unicode.new(*args)
1078
+
1079
+
1080
+ def _ua(c: 'Controller') -> Pyjo.UserAgent.object:
1081
+ return c.app.ua
1082
+
1083
+
1084
+ def _validation(c: 'Controller') -> 'Pyjoyment.Validator.Validation.object':
1085
+ stash = c.stash()
1086
+ if (v := stash.get('mojo.validation')) is not None:
1087
+ return v
1088
+
1089
+ req = c.req
1090
+ token = c.session().get('csrf_token')
1091
+ header = req.headers.header('X-CSRF-Token')
1092
+ hash: dict[str, Any] = dict(req.params.to_dict())
1093
+ if token and header and hash.get('csrf_token') is None:
1094
+ hash['csrf_token'] = header
1095
+ for name in [upload.name for upload in req.uploads()]:
1096
+ if name is not None:
1097
+ hash[name] = req.every_upload(name)
1098
+ v = c.app.validator.validation().set(input=hash, csrf_token=token)
1099
+ stash['mojo.validation'] = v
1100
+ return v
1101
+
1102
+
1103
+ # Not in Mojolicious: the types of the helpers, for type checkers, which Pyjoyment.Controller and Pyjoyment have
1104
+ class ProxyHelpers:
1105
+ """
1106
+ The types of the nested helpers ``proxy.*``, for type checkers.
1107
+ """
1108
+
1109
+ get_p = Helper(functools.partial(_proxy_method_p, 'GET'))
1110
+ post_p = Helper(functools.partial(_proxy_method_p, 'POST'))
1111
+ start_p = Helper(_proxy_start_p)
1112
+
1113
+
1114
+ class ReplyHelpers:
1115
+ """
1116
+ The types of the nested helpers ``reply.*``, for type checkers.
1117
+ """
1118
+
1119
+ asset = Helper(_asset)
1120
+ file = Helper(_file)
1121
+ static = Helper(_static)
1122
+
1123
+ exception = Helper(_exception)
1124
+ not_found = Helper(_not_found)
1125
+ http_exception = Helper(_http_exception)
1126
+ http_not_found = Helper(_http_not_found)
1127
+ html_exception = Helper(functools.partial(_development, 'exception'))
1128
+ html_not_found = Helper(functools.partial(_development, 'not_found'))
1129
+ json_exception = Helper(_json_exception)
1130
+ json_not_found = Helper(_json_not_found)
1131
+ txt_exception = Helper(_txt_exception)
1132
+ txt_not_found = Helper(_txt_not_found)
1133
+
1134
+
1135
+ class TimingHelpers:
1136
+ """
1137
+ The types of the nested helpers ``timing.*``, for type checkers.
1138
+ """
1139
+
1140
+ begin = Helper(_timing_begin)
1141
+ elapsed = Helper(_timing_elapsed)
1142
+ rps = Helper(_timing_rps)
1143
+ server_timing = Helper(_timing_server_timing)
1144
+
1145
+
1146
+ class Helpers:
1147
+ """
1148
+ The types of the helpers of :mod:`Pyjoyment.Plugin.DefaultHelpers`, for
1149
+ type checkers and editors, which :mod:`Pyjoyment.Controller` and
1150
+ :mod:`Pyjoyment` have, while the helpers themselves are found in the
1151
+ renderer of the application at run time. See
1152
+ :class:`Pyjoyment.Controller.Helper`.
1153
+ """
1154
+
1155
+ app = Helper(_app)
1156
+ param = Helper(_alias('param'))
1157
+ stash = Helper(_alias('stash'))
1158
+ session = Helper(_alias('session'))
1159
+ url_for = Helper(_alias('url_for'))
1160
+ url_for_asset = Helper(_alias('url_for_asset'))
1161
+ url_for_file = Helper(_alias('url_for_file'))
1162
+
1163
+ extends = Helper(_stash_key('extends'))
1164
+ layout = Helper(_stash_key('layout'))
1165
+ title = Helper(_stash_key('title'))
1166
+
1167
+ accepts = Helper(_accepts)
1168
+ b = Helper(_b)
1169
+ u = Helper(_u)
1170
+ c = Helper(_c)
1171
+ config = Helper(_config)
1172
+
1173
+ content = Helper(functools.partial(_content, False, False))
1174
+ content_for = Helper(functools.partial(_content, True, False))
1175
+ content_with = Helper(functools.partial(_content, False, True))
1176
+
1177
+ csrf_token = Helper(_csrf_token)
1178
+ current_route = Helper(_current_route)
1179
+ exception_format = Helper(_exception_format)
1180
+ flash = Helper(_flash)
1181
+ inactivity_timeout = Helper(_inactivity_timeout)
1182
+ is_fresh = Helper(_is_fresh)
1183
+ redirect_to = Helper(_redirect_to)
1184
+ respond_to = Helper(_respond_to)
1185
+ url_with = Helper(_url_with)
1186
+ validation = Helper(_validation)
1187
+
1188
+ dumper = Helper(_dumper)
1189
+ include = Helper(_include)
1190
+
1191
+ log = Helper(_log)
1192
+
1193
+ proxy: ProxyHelpers
1194
+ reply: ReplyHelpers
1195
+ timing: TimingHelpers
1196
+
1197
+ ua = Helper(_ua)
1198
+
1199
+
1200
+ new = Pyjoyment_Plugin_DefaultHelpers.new
1201
+ object = Pyjoyment_Plugin_DefaultHelpers
1202
+ DefaultHelpers = Pyjoyment_Plugin_DefaultHelpers