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,1307 @@
1
+ """
2
+ Pyjoyment.Controller - Controller base class
3
+ ============================================
4
+ ::
5
+
6
+ # Controller
7
+ from Pyjoyment.Controller import Controller
8
+
9
+ class MyApp_Controller_Foo(Controller):
10
+
11
+ # Action
12
+ def bar(self):
13
+ name = self.param('name')
14
+ self.res.headers.cache_control = 'max-age=1, no-cache'
15
+ self.render(json={'hello': name})
16
+
17
+ object = MyApp_Controller_Foo
18
+
19
+ # Or import the module, as in Google Python Style Guide
20
+
21
+ # Controller
22
+ import Pyjoyment.Controller
23
+
24
+ class MyApp_Controller_Foo(Pyjoyment.Controller.object):
25
+
26
+ # Action
27
+ def bar(self):
28
+ name = self.param('name')
29
+ self.res.headers.cache_control = 'max-age=1, no-cache'
30
+ self.render(json={'hello': name})
31
+
32
+ object = MyApp_Controller_Foo
33
+
34
+ :mod:`Pyjoyment.Controller` is the base class for your :mod:`Pyjoyment`
35
+ controllers. It is also the default controller class unless you set
36
+ :attr:`Pyjoyment.Pyjoyment_App.controller_class`.
37
+
38
+ A controller is a context manager for the callbacks of a delayed response: an
39
+ exception that the ``with`` block does not catch is replied with the helper
40
+ ``reply.exception`` of :mod:`Pyjoyment.Plugin.DefaultHelpers`, as an exception
41
+ that goes on to the event loop would leave the request without a response. ::
42
+
43
+ # Exception in a callback of a delayed response
44
+ def later(self):
45
+ @Pyjo.IOLoop.next_tick
46
+ def tick(loop):
47
+ with self:
48
+ self.render(text=self.param('name').upper())
49
+
50
+ Helpers
51
+ -------
52
+
53
+ In addition to the attributes and methods below you can also call helpers
54
+ provided by :attr:`~Pyjoyment_Controller.app` on :mod:`Pyjoyment.Controller`
55
+ objects. This includes all helpers from
56
+ :mod:`Pyjoyment.Plugin.DefaultHelpers` and :mod:`Pyjoyment.Plugin.TagHelpers`. ::
57
+
58
+ # Call helpers
59
+ c.layout('green')
60
+ c.title('Welcome!')
61
+
62
+ # Longer version
63
+ c.helpers.layout('green')
64
+
65
+ Type checkers and editors know the types of these helpers, and of the helpers
66
+ of an application that :class:`Helper` declares.
67
+
68
+ See also
69
+ --------
70
+
71
+ :mod:`Pyjoyment`, https://mojolicious.org.
72
+
73
+ Ported from `Mojolicious::Controller <https://github.com/mojolicious/mojo/blob/v9.53/lib/Mojolicious/Controller.pm>`_
74
+ of Mojolicious 9.53.
75
+ """
76
+
77
+ # No imports, for security reasons!
78
+ import Pyjo.Base
79
+ import Pyjo.Exception
80
+ import Pyjo.String.Bytes
81
+ import Pyjo.String.HTML
82
+ import Pyjo.URL
83
+ import Pyjo.Util
84
+ import Pyjoyment.Routes.Match
85
+
86
+ import functools
87
+ import hashlib
88
+ import hmac
89
+ import io
90
+ import re
91
+ import sys
92
+ import weakref
93
+
94
+ from collections.abc import Callable
95
+ from types import TracebackType
96
+ from typing import TYPE_CHECKING, Any, Concatenate, Self, cast, overload
97
+
98
+
99
+ if TYPE_CHECKING:
100
+ import Pyjo.Message.Request
101
+ import Pyjo.Message.Response
102
+ import Pyjo.Transaction
103
+ import Pyjoyment
104
+ import Pyjoyment.Plugin.DefaultHelpers
105
+ import Pyjoyment.Plugin.TagHelpers
106
+
107
+ # Not in Mojolicious: the types of the helpers of the default plugins, which type checkers take from here, while
108
+ # __getattr__ finds the helpers in the renderer at run time, and any other helper is Any
109
+ class _Helpers(Pyjoyment.Plugin.DefaultHelpers.Helpers, Pyjoyment.Plugin.TagHelpers.Helpers):
110
+ def __getattr__(self, name: str) -> Any: ...
111
+
112
+ class _Base(Pyjo.Base.object, _Helpers): ...
113
+
114
+ else:
115
+ _Base = Pyjo.Base.object
116
+
117
+
118
+ _ABSOLUTE = re.compile(r'^(?:[^:/?#]+:|//|#)')
119
+
120
+ _MISSING: Any = Pyjo.Base.object()
121
+
122
+
123
+ class Pyjoyment_Controller(_Base):
124
+ """
125
+ :mod:`Pyjoyment.Controller` inherits all methods from :mod:`Pyjo.Base` and
126
+ implements the following new ones.
127
+ """
128
+
129
+ def __init__(
130
+ self,
131
+ app: 'Pyjoyment.object | None' = None,
132
+ match: Pyjoyment.Routes.Match.object | None = None,
133
+ stash: dict[str, Any] | None = None,
134
+ tx: 'Pyjo.Transaction.object | None' = None,
135
+ ) -> None:
136
+ """::
137
+
138
+ c = Pyjoyment.Controller.new()
139
+ c = Pyjoyment.Controller.new(app=Pyjoyment.new(), tx=Pyjo.Transaction.HTTP.new())
140
+
141
+ Construct a new :mod:`Pyjoyment.Controller` object.
142
+ """
143
+ self._app: weakref.ref[Pyjoyment.object] | None = None
144
+ self._match = match
145
+ self._stash = stash if stash is not None else {}
146
+ self._tx: weakref.ref[Pyjo.Transaction.object] | None = None
147
+ # The transaction that the application builds for the controller, which Mojo keeps in the hash of the
148
+ # object, past the weakening of the attribute
149
+ self._own_tx: Pyjo.Transaction.object | None = None
150
+
151
+ self.app = app
152
+ self.tx = tx
153
+
154
+ # Not in Mojolicious: a context manager, which replies with the exception that the block does not catch, as an
155
+ # exception that goes on from a callback to the event loop would leave the request without a response
156
+ def __enter__(self) -> Self:
157
+ return self
158
+
159
+ def __exit__(
160
+ self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
161
+ ) -> bool:
162
+ if not isinstance(value, Exception):
163
+ return False
164
+ self.helpers.reply.exception(value)
165
+ return True
166
+
167
+ # Unlike Mojolicious: Mojo registers the helpers with Mojo::DynamicMethods, Python looks them up in the renderer of
168
+ # the application when a method is not found
169
+ def __getattr__(self, name: str) -> Any:
170
+ app = self._app() if self._app is not None and not name.startswith('_') else None
171
+ if app is not None and (helper := app.renderer.get_helper(name)) is not None:
172
+ return helper(self) if isinstance(helper, type) else _Helper(helper, self)
173
+ package = f'{type(self).__module__}.{type(self).__qualname__}'
174
+
175
+ # The location of the caller, which croak adds to the message, the one of the attribute for the type of a helper
176
+ caller = sys._getframe(1) # pyright: ignore[reportPrivateUsage]
177
+ if caller.f_code is _HELPER_GET and caller.f_back is not None:
178
+ caller = caller.f_back
179
+ at = f'at {caller.f_code.co_filename} line {caller.f_lineno}'
180
+
181
+ # Not in Mojolicious: the attribute or the helper that the name most likely meant, as "req" for "request" of
182
+ # another framework
183
+ mean = ''
184
+ if app is not None:
185
+ names = [*dir(self), *(h.split('.')[0] for h in app.renderer.helpers)]
186
+ if (suggestion := Pyjo.Util._suggest(name, names)) is not None: # pyright: ignore[reportPrivateUsage]
187
+ mean = f' Did you mean "{suggestion}"?'
188
+ raise AttributeError(
189
+ f'Can\'t locate object method "{name}" via package "{package}" {at}.{mean}\n', name=name, obj=self
190
+ )
191
+
192
+ @property
193
+ def app(self) -> 'Pyjoyment.object':
194
+ """::
195
+
196
+ app = c.app
197
+ c.app = Pyjoyment.new()
198
+
199
+ A reference back to the application that dispatched to this
200
+ controller, usually a :mod:`Pyjoyment` object. Note that this attribute
201
+ is weakened. ::
202
+
203
+ # Use application logger
204
+ c.app.log.debug('Hello Pyjo')
205
+
206
+ # Generate path
207
+ path = c.app.home.child('templates', 'foo', 'bar.html.ep')
208
+ """
209
+ # None when the application is gone, as the weakened attribute of Mojo is undef then
210
+ return cast('Pyjoyment.object', self._app() if self._app is not None else None)
211
+
212
+ @app.setter
213
+ def app(self, value: 'Pyjoyment.object | None') -> None:
214
+ self._app = weakref.ref(value) if value is not None else None
215
+
216
+ @property
217
+ def match(self) -> Pyjoyment.Routes.Match.object:
218
+ """::
219
+
220
+ m = c.match
221
+ c.match = Pyjoyment.Routes.Match.new()
222
+
223
+ Router results for the current request, defaults to a
224
+ :mod:`Pyjoyment.Routes.Match` object. ::
225
+
226
+ # Introspect
227
+ name = c.match.endpoint.name
228
+ foo = c.match.endpoint.pattern.defaults['foo']
229
+ action = c.match.stack[-1]['action']
230
+ """
231
+ if self._match is None:
232
+ self._match = Pyjoyment.Routes.Match.new(root=self.app.routes)
233
+ return self._match
234
+
235
+ @match.setter
236
+ def match(self, value: Pyjoyment.Routes.Match.object) -> None:
237
+ self._match = value
238
+
239
+ @property
240
+ def tx(self) -> 'Pyjo.Transaction.object | None':
241
+ """::
242
+
243
+ tx = c.tx
244
+ c.tx = Pyjo.Transaction.HTTP.new()
245
+
246
+ The transaction that is currently being processed, usually a
247
+ :mod:`Pyjo.Transaction.HTTP` or :mod:`Pyjo.Transaction.WebSocket`
248
+ object. Note that this attribute is weakened. So the object needs to be
249
+ referenced elsewhere as well when you're performing non-blocking
250
+ operations and the underlying connection might get closed early. ::
251
+
252
+ # Check peer information
253
+ address = c.tx.remote_address
254
+ port = c.tx.remote_port
255
+
256
+ # Increase size limit for WebSocket messages to 16MiB
257
+ if c.tx.is_websocket:
258
+ c.tx.max_websocket_size = 16777216
259
+
260
+ # Perform non-blocking operation without knowing the connection status
261
+ tx = c.tx
262
+ Pyjo.IOLoop.timer(2, lambda loop: c.app.log.debug('Finished' if tx.is_finished else 'In progress'))
263
+ """
264
+ return self._tx() if self._tx is not None else None
265
+
266
+ @tx.setter
267
+ def tx(self, value: 'Pyjo.Transaction.object | None') -> None:
268
+ self._tx = weakref.ref(value) if value is not None else None
269
+ if value is not self._own_tx:
270
+ self._own_tx = None
271
+
272
+ # Renamed from Mojolicious: continue, which is a keyword of Python
273
+ def proceed(self) -> None:
274
+ """::
275
+
276
+ c.proceed()
277
+
278
+ Continue dispatch chain from an intermediate destination with
279
+ :meth:`Pyjoyment.Routes.Pyjoyment_Routes.proceed`. ``continue_`` is an
280
+ alias, the name of Mojolicious with an underscore.
281
+ """
282
+ self.app.routes.proceed(self)
283
+
284
+ # The name of Mojolicious, with an underscore as continue is a keyword of Python
285
+ continue_ = proceed
286
+
287
+ @overload
288
+ def cookie(self, name: str, /) -> str | None: ...
289
+
290
+ @overload
291
+ def cookie(self, name: str, value: str, options: dict[str, Any] | None = None, /) -> Self: ...
292
+
293
+ def cookie(self, name: str, /, *args: Any) -> str | None | Self:
294
+ """::
295
+
296
+ value = c.cookie('foo')
297
+ c = c.cookie('foo', 'bar')
298
+ c = c.cookie('foo', 'bar', {'path': '/'})
299
+
300
+ Access request cookie values and create new response cookies. If there
301
+ are multiple values sharing the same name, and you want to access more
302
+ than just the last one, you can use :meth:`every_cookie`. ::
303
+
304
+ # Create response cookie with domain and expiration date
305
+ c.cookie('user', 'sri', {'domain': 'example.com', 'expires': time.time() + 60})
306
+
307
+ # Create secure response cookie
308
+ c.cookie('secret', 'I <3 Pyjoyment', {'secure': True, 'httponly': True})
309
+ """
310
+ # Response cookie
311
+ if args:
312
+ # Cookie too big
313
+ values: dict[str, Any] = {'name': name, 'value': args[0], **(args[1] if len(args) > 1 and args[1] else {})}
314
+ if len(values['value'] or '') > 4096:
315
+ self.helpers.log().error(f'Cookie "{name}" is bigger than 4KiB')
316
+
317
+ self.res.cookies(values)
318
+ return self
319
+
320
+ # Request cookies
321
+ if (cookie := self.req.cookie(name)) is None:
322
+ return None
323
+ return cookie.value
324
+
325
+ # Left out from Mojolicious: encrypted_cookie, as encrypt_cookie and decrypt_cookie of Pyjo.Util are left out
326
+
327
+ def every_cookie(self, name: str) -> list[str]:
328
+ """::
329
+
330
+ values = c.every_cookie('foo')
331
+
332
+ Similar to :meth:`cookie`, but returns all request cookie values sharing
333
+ the same name as a list. ::
334
+
335
+ # Get first cookie value
336
+ first = c.every_cookie('foo')[0]
337
+ """
338
+ return [cookie.value or '' for cookie in self.req.every_cookie(name)]
339
+
340
+ # Left out from Mojolicious: every_encrypted_cookie, as encrypt_cookie and decrypt_cookie of Pyjo.Util are left out
341
+
342
+ def every_param(self, name: str) -> 'list[Any]':
343
+ """::
344
+
345
+ values = c.every_param('foo')
346
+
347
+ Similar to :meth:`param`, but returns all values sharing the same name
348
+ as a list. ::
349
+
350
+ # Get first value
351
+ first = c.every_param('foo')[0]
352
+ """
353
+ # Captured unreserved values
354
+ captures = self.stash().setdefault('mojo.captures', {})
355
+ if name in captures and not self.app.routes.is_reserved(name):
356
+ value = captures[name]
357
+ return cast(list[Any], value) if isinstance(value, list) else [value]
358
+
359
+ # Uploads or param values
360
+ req = self.req
361
+ uploads: list[Any] = req.every_upload(name)
362
+ return uploads if uploads else req.every_param(name)
363
+
364
+ def every_signed_cookie(self, name: str) -> list[str]:
365
+ """::
366
+
367
+ values = c.every_signed_cookie('foo')
368
+
369
+ Similar to :meth:`signed_cookie`, but returns all signed request cookie
370
+ values sharing the same name as a list. ::
371
+
372
+ # Get first signed cookie value
373
+ first = c.every_signed_cookie('foo')[0]
374
+ """
375
+ secrets = self.app.secrets
376
+ results: list[str] = []
377
+ for value in self.every_cookie(name):
378
+ # Check signature with rotating secrets
379
+ if m := re.search(r'--([^\-]+)$', value):
380
+ signature, value = m[1], value[: m.start()]
381
+
382
+ if any(Pyjo.Util.secure_compare(signature, _hmac(f'{name}={value}', secret)) for secret in secrets):
383
+ results.append(value)
384
+
385
+ else:
386
+ self.helpers.log().trace(f'Cookie "{name}" has bad signature')
387
+
388
+ else:
389
+ self.helpers.log().trace(f'Cookie "{name}" is not signed')
390
+
391
+ return results
392
+
393
+ def finish(self, *args: Any) -> Self:
394
+ """::
395
+
396
+ c = c.finish()
397
+ c = c.finish(1000)
398
+ c = c.finish(1003, 'Cannot accept data!')
399
+ c = c.finish(b'Bye!')
400
+
401
+ Close WebSocket connection or long poll stream gracefully. This method
402
+ will automatically respond to WebSocket handshake requests with a
403
+ ``101`` response status, to establish the WebSocket connection.
404
+ """
405
+ # WebSocket
406
+ tx: Any = self._transaction()
407
+ if tx.is_websocket:
408
+ tx.finish(*args)
409
+ return self if tx.established else self.rendered(101)
410
+
411
+ # Chunked stream
412
+ if tx.res.content.is_chunked:
413
+ return self.write_chunk(*args).write_chunk(b'') if args else self.write_chunk(b'')
414
+
415
+ # Normal stream
416
+ return self.write(*args).write(b'') if args else self.write(b'')
417
+
418
+ @property
419
+ def helpers(self) -> '_Helpers':
420
+ """::
421
+
422
+ helpers = c.helpers
423
+
424
+ Return a proxy object containing the current controller object and on
425
+ which helpers provided by :attr:`app` can be called. This includes all
426
+ helpers from :mod:`Pyjoyment.Plugin.DefaultHelpers` and
427
+ :mod:`Pyjoyment.Plugin.TagHelpers`. ::
428
+
429
+ # Make sure to use the "title" helper and not the controller method
430
+ c.helpers.title('Welcome!')
431
+
432
+ # Use a nested helper instead of the "reply" controller method
433
+ c.helpers.reply.not_found()
434
+ """
435
+ return cast(Any, self.app.renderer.get_helper(''))(self)
436
+
437
+ @overload
438
+ def on(self, name: str, cb: None = None) -> Callable[[Callable[..., Any]], Callable[..., Any]]: ...
439
+
440
+ @overload
441
+ def on(self, name: str, cb: Callable[..., Any]) -> Callable[..., Any]: ...
442
+
443
+ def on(self, name: str, cb: Callable[..., Any] | None = None) -> Any:
444
+ """::
445
+
446
+ cb = c.on('finish', lambda c: ...)
447
+
448
+ @c.on('finish')
449
+ def cb(c): ...
450
+
451
+ Subscribe to events of :attr:`tx`, which is usually a
452
+ :mod:`Pyjo.Transaction.HTTP` or :mod:`Pyjo.Transaction.WebSocket`
453
+ object. This method will automatically respond to WebSocket handshake
454
+ requests with a ``101`` response status, to establish the WebSocket
455
+ connection. ::
456
+
457
+ # Do something after the transaction has been finished
458
+ @c.on('finish')
459
+ def finish(c):
460
+ c.app.log.debug('All data has been sent')
461
+
462
+ # Receive WebSocket message
463
+ @c.on('message')
464
+ def message(c, msg):
465
+ c.app.log.debug(f'Message: {msg}')
466
+
467
+ # Receive JSON object via WebSocket message
468
+ @c.on('json')
469
+ def json(c, hash):
470
+ c.app.log.debug(f'Test: {hash["test"]}')
471
+
472
+ # Receive WebSocket "Binary" message
473
+ @c.on('binary')
474
+ def binary(c, data):
475
+ c.app.log.debug(f'Received {len(data)} bytes')
476
+ """
477
+ if cb is None:
478
+
479
+ def decorator(cb: Callable[..., Any]) -> Callable[..., Any]:
480
+ return self.on(name, cb)
481
+
482
+ return decorator
483
+
484
+ tx: Any = self._transaction()
485
+ if tx.is_websocket and not tx.established:
486
+ self.rendered(101)
487
+
488
+ def event(tx: Any, *args: Any) -> Any:
489
+ try:
490
+ return cb(self, *args)
491
+ except TypeError as e:
492
+ # Not in Mojolicious: the arguments of a callback, which Perl does not check
493
+ Pyjo.Util._call_hint(e, f'the callbacks of the event "{name}" get the controller first') # pyright: ignore[reportPrivateUsage]
494
+ raise
495
+
496
+ return tx.on(name, event)
497
+
498
+ @overload
499
+ def param(self, name: str, /) -> Any: ...
500
+
501
+ @overload
502
+ def param(self, name: str, value: Any, /, *values: Any) -> Self: ...
503
+
504
+ def param(self, name: str, /, *values: Any) -> Any:
505
+ """::
506
+
507
+ value = c.param('foo')
508
+ c = c.param('foo', 'ba;r')
509
+ c = c.param('foo', 'ba;r', 'baz')
510
+ c = c.param('foo', ['ba;r', 'baz'])
511
+
512
+ Access route placeholder values that are not reserved stash values,
513
+ file uploads as well as ``GET`` and ``POST`` parameters extracted from
514
+ the query string and ``application/x-www-form-urlencoded`` or
515
+ ``multipart/form-data`` message body, in that order. If there are
516
+ multiple values sharing the same name, and you want to access more than
517
+ just the last one, you can use :meth:`every_param`. Parts of the request
518
+ body need to be loaded into memory to parse ``POST`` parameters, so you
519
+ have to make sure it is not excessively large. There's a 16MiB limit for
520
+ requests by default. ::
521
+
522
+ # Get first value
523
+ first = c.every_param('foo')[0]
524
+
525
+ For more control you can also access request information directly. ::
526
+
527
+ # Only GET parameters
528
+ foo = c.req.query_params.param('foo')
529
+
530
+ # Only POST parameters
531
+ foo = c.req.body_params.param('foo')
532
+
533
+ # Only GET and POST parameters
534
+ foo = c.req.param('foo')
535
+
536
+ # Only file uploads
537
+ foo = c.req.upload('foo')
538
+ """
539
+ if not values:
540
+ every = self.every_param(name)
541
+ return every[-1] if every else None
542
+ captures: dict[str, Any] = self.stash().setdefault('mojo.captures', {})
543
+ captures[name] = list(values) if len(values) > 1 else next(iter(values))
544
+ return self
545
+
546
+ def render(self, template: str | None = None, /, **args: Any) -> Any:
547
+ """::
548
+
549
+ boolean = c.render()
550
+ boolean = c.render(foo='bar', baz=23)
551
+ boolean = c.render(template='foo/index')
552
+ boolean = c.render(template='index', format='html')
553
+ boolean = c.render(data=data)
554
+ boolean = c.render(text='Hello!')
555
+ boolean = c.render(json={'foo': 'bar'})
556
+ boolean = c.render(yaml={'foo': 'bar'})
557
+ boolean = c.render(toml={'foo': 'bar'})
558
+ boolean = c.render(handler='something')
559
+ boolean = c.render('foo/index')
560
+
561
+ Render content with :attr:`Pyjoyment.Pyjoyment_App.renderer` and emit
562
+ hooks ``before_render`` as well as ``after_render``, or raises an
563
+ exception if no response could be generated. All additional keyword
564
+ arguments get merged into the :meth:`stash`. ::
565
+
566
+ # Render characters
567
+ c.render(text='I ♥ Pyjoyment!')
568
+
569
+ # Render characters (alternative)
570
+ c.stash(text='I ♥ Pyjoyment!').render()
571
+
572
+ # Render binary data
573
+ from Pyjo.JSON import encode_json
574
+ c.render(data=encode_json({'test': 'I ♥ Pyjoyment!'}))
575
+
576
+ # Render JSON
577
+ c.render(json={'test': 'I ♥ Pyjoyment!'})
578
+
579
+ # Render YAML, in the style of index.yaml of a chart repository of Helm
580
+ c.render(yaml={'entries': {'test': [{'version': '1.0.0'}]}})
581
+
582
+ # Render TOML, of a table
583
+ c.render(toml={'server': {'workers': 4}})
584
+
585
+ # Render inline template
586
+ c.render(inline='<%= 1 + 1 %>')
587
+
588
+ # Render template "foo/bar.html.ep"
589
+ c.render(template='foo/bar', format='html', handler='ep')
590
+
591
+ # Render template "test.*.*" with arbitrary values "foo" and "bar"
592
+ c.render(template='test', foo='test', bar=23)
593
+
594
+ # Render template "test.xml.*"
595
+ c.render(template='test', format='xml')
596
+
597
+ # Render template "test.xml.*" (alternative)
598
+ c.render('test', format='xml')
599
+ """
600
+
601
+ # Template may be first argument
602
+ if template:
603
+ args['template'] = template
604
+ app = self.app
605
+ plugins = app.plugins.emit_hook('before_render', self, args)
606
+
607
+ # Localize "extends" and "layout" to allow argument overrides
608
+ maybe, ts = args.get('mojo.maybe'), args.get('mojo.string')
609
+ stash = self.stash()
610
+ saved = [(key, stash[key]) for key in ('layout', 'extends') if key in stash]
611
+
612
+ # Rendering to string
613
+ if ts or maybe:
614
+ saved.extend((key, stash.get(key, _MISSING)) for key in args)
615
+ try:
616
+ if ts:
617
+ stash.pop('layout', None)
618
+ stash.pop('extends', None)
619
+
620
+ # All other arguments just become part of the stash
621
+ stash.update(args)
622
+ renderer = app.renderer
623
+ output, format = renderer.render(self) or (None, None)
624
+
625
+ # Maybe no 404
626
+ if ts:
627
+ if output is None:
628
+ return None
629
+ return Pyjo.String.Bytes.new(output) if isinstance(output, bytes) else Pyjo.String.HTML.new(output)
630
+ if output is None:
631
+ if maybe:
632
+ return None
633
+ raise Pyjo.Exception.new('Could not render a response')
634
+
635
+ # The hooks change the output through a file object, as Mojo passes a reference to it
636
+ ref = io.BytesIO(output) if isinstance(output, bytes) else io.StringIO(output)
637
+ plugins.emit_hook('after_render', self, ref, format)
638
+ return renderer.respond(self, ref.getvalue(), format, stash.get('status'))
639
+
640
+ finally:
641
+ for key, value in reversed(saved):
642
+ if value is _MISSING:
643
+ stash.pop(key, None)
644
+ else:
645
+ stash[key] = value
646
+
647
+ def render_later(self) -> Self:
648
+ """::
649
+
650
+ c = c.render_later()
651
+
652
+ Disable automatic rendering to delay response generation, only
653
+ necessary if automatic rendering would result in a response. ::
654
+
655
+ # Delayed rendering
656
+ c.render_later()
657
+ Pyjo.IOLoop.timer(2, lambda loop: c.render(text='Delayed by 2 seconds!'))
658
+ """
659
+ return self.stash({'mojo.rendered': True})
660
+
661
+ def render_maybe(self, template: str | None = None, /, **args: Any) -> Any:
662
+ """::
663
+
664
+ boolean = c.render_maybe()
665
+ boolean = c.render_maybe(foo='bar', baz=23)
666
+ boolean = c.render_maybe('foo/index', format='html')
667
+
668
+ Try to render content, but do not call
669
+ :meth:`Pyjoyment.Plugin.DefaultHelpers` ``reply.not_found`` if no
670
+ response could be generated, all arguments get localized automatically
671
+ and are only available during this render operation, takes the same
672
+ arguments as :meth:`render`. ::
673
+
674
+ # Render template "index_local" only if it exists
675
+ c.render_maybe('index_local') or c.render('index')
676
+ """
677
+ return self.render(template, **args, **{'mojo.maybe': True})
678
+
679
+ def render_to_string(
680
+ self, template: str | None = None, /, **args: Any
681
+ ) -> Pyjo.String.HTML.object | Pyjo.String.Bytes.object | None:
682
+ """::
683
+
684
+ output = c.render_to_string('foo/index', format='pdf')
685
+
686
+ Try to render content and return it wrapped in a
687
+ :mod:`Pyjo.String.HTML` object, markup that templates do not escape, or
688
+ a :mod:`Pyjo.String.Bytes` object or return ``None``, all arguments get
689
+ localized automatically and are only available during this render
690
+ operation, takes the same arguments as :meth:`render`. ::
691
+
692
+ # Render inline template
693
+ two = c.render_to_string(inline='<%= 1 + 1 %>')
694
+ """
695
+ return self.render(template, **args, **{'mojo.string': True})
696
+
697
+ def rendered(self, status: int | str | None = None) -> Self:
698
+ """::
699
+
700
+ c = c.rendered()
701
+ c = c.rendered(302)
702
+
703
+ Finalize response and emit hook ``after_dispatch``, defaults to using a
704
+ ``200`` response code. ::
705
+
706
+ # Custom response
707
+ c.res.headers.content_type = 'text/plain'
708
+ c.res.body = b'Hello World!'
709
+ c.rendered(200)
710
+ """
711
+ # Make sure we have a status
712
+ if status:
713
+ self.res.code = int(status)
714
+
715
+ # Finish transaction
716
+ stash = self.stash()
717
+ if not stash.get('mojo.finished'):
718
+ stash['mojo.finished'] = True
719
+ res = self.res
720
+ if not status and not res.code:
721
+ res.code = 200
722
+
723
+ # Disable auto rendering and stop timer
724
+ app = self.render_later().app
725
+ if not stash.get('mojo.static'):
726
+
727
+ def message() -> str:
728
+ timing = self.helpers.timing
729
+ elapsed = timing.elapsed('mojo.timer')
730
+ elapsed = elapsed if elapsed is not None else 0
731
+ rps = timing.rps(elapsed)
732
+ code = res.code
733
+ msg = res.message or res.default_message(code)
734
+ return f'{code} {msg} ({elapsed:.15g}s, {rps if rps is not None else "??"}/s)'
735
+
736
+ self.helpers.log().trace(message)
737
+
738
+ app.plugins.emit_hook_reverse('after_dispatch', self)
739
+ app.sessions.store(self)
740
+ cast(Any, self._transaction()).resume()
741
+ return self
742
+
743
+ @property
744
+ def req(self) -> 'Pyjo.Message.Request.object':
745
+ """::
746
+
747
+ req = c.req
748
+
749
+ Get :mod:`Pyjo.Message.Request` object from :attr:`tx`. ::
750
+
751
+ # Longer version
752
+ req = c.tx.req
753
+
754
+ # Extract request information
755
+ id = c.req.request_id
756
+ method = c.req.method
757
+ url = c.req.url.to_abs()
758
+ info = c.req.url.to_abs().userinfo
759
+ host = c.req.url.to_abs().host
760
+ agent = c.req.headers.user_agent
761
+ custom = c.req.headers.header('Custom-Header')
762
+ data = c.req.body
763
+ string = c.req.text
764
+ hash = c.req.params.to_dict()
765
+ all = c.req.uploads
766
+ value = c.req.json()
767
+ foo = c.req.json('/23/foo')
768
+ dom = c.req.dom()
769
+ bar = c.req.dom('div.bar').first().text
770
+ """
771
+ return self._transaction().req
772
+
773
+ @property
774
+ def res(self) -> 'Pyjo.Message.Response.object':
775
+ """::
776
+
777
+ res = c.res
778
+
779
+ Get :mod:`Pyjo.Message.Response` object from :attr:`tx`. ::
780
+
781
+ # Longer version
782
+ res = c.tx.res
783
+
784
+ # Force file download by setting a response header
785
+ c.res.headers.content_disposition = 'attachment; filename=foo.png;'
786
+
787
+ # Use a custom response header
788
+ c.res.headers.header('Custom-Header', 'whatever')
789
+
790
+ # Make sure response is cached correctly
791
+ c.res.headers.cache_control = 'public, max-age=300'
792
+ c.res.headers.append('Vary', 'Accept-Encoding')
793
+ """
794
+ return self._transaction().res
795
+
796
+ def send(self, msg: Any, cb: Callable[[Self], Any] | None = None) -> Self:
797
+ """::
798
+
799
+ c = c.send({'binary': data})
800
+ c = c.send({'text': data})
801
+ c = c.send({'json': {'test': [1, 2, 3]}})
802
+ c = c.send([fin, rsv1, rsv2, rsv3, op, payload])
803
+ c = c.send(chars)
804
+ c = c.send(chars, lambda c: ...)
805
+
806
+ Send message or frame non-blocking via WebSocket, the optional drain
807
+ callback will be executed once all data has been written. This method
808
+ will automatically respond to WebSocket handshake requests with a
809
+ ``101`` response status, to establish the WebSocket connection. ::
810
+
811
+ # Send "Text" message
812
+ c.send('I ♥ Pyjoyment!')
813
+
814
+ # Send JSON object as "Text" message
815
+ c.send({'json': {'test': 'I ♥ Pyjoyment!'}})
816
+
817
+ # Send JSON object as "Binary" message
818
+ from Pyjo.JSON import encode_json
819
+ c.send({'binary': encode_json({'test': 'I ♥ Pyjoyment!'})})
820
+
821
+ # Send "Ping" frame
822
+ from Pyjo.WebSocket import WS_PING
823
+ c.send([1, 0, 0, 0, WS_PING, b'Hello World!'])
824
+
825
+ # Make sure the first message has been written before continuing
826
+ c.send('First message!', lambda c: c.send('Second message!'))
827
+
828
+ For mostly idle WebSockets you might also want to increase the
829
+ inactivity timeout with the helper ``inactivity_timeout`` of
830
+ :mod:`Pyjoyment.Plugin.DefaultHelpers`, which usually defaults to
831
+ ``30`` seconds. ::
832
+
833
+ # Increase inactivity timeout for connection to 300 seconds
834
+ c.inactivity_timeout(300)
835
+ """
836
+ tx: Any = self._transaction()
837
+ if not tx.is_websocket:
838
+ raise Pyjo.Exception.new('No WebSocket connection to send message to')
839
+ tx.send(msg, _drain(self, cb))
840
+ return self if tx.established else self.rendered(101)
841
+
842
+ @overload
843
+ def session(self) -> dict[str, Any]: ... # pyright: ignore[reportOverlappingOverload]
844
+
845
+ @overload
846
+ def session(self, name: str, /) -> Any: ...
847
+
848
+ @overload
849
+ def session(self, values: dict[str, Any], /) -> Self: ...
850
+
851
+ @overload
852
+ def session(self, **values: Any) -> Self: ...
853
+
854
+ def session(self, *args: Any, **values: Any) -> Any:
855
+ """::
856
+
857
+ session = c.session()
858
+ foo = c.session('foo')
859
+ c = c.session({'foo': 'bar'})
860
+ c = c.session(foo='bar')
861
+
862
+ Persistent data storage for the next few requests, all session data
863
+ gets serialized with :mod:`Pyjo.JSON` and stored Base64 encoded in
864
+ HMAC-SHA256 signed cookies, to prevent tampering. Note that cookies
865
+ usually have a ``4096`` byte (4KiB) limit, depending on browser. ::
866
+
867
+ # Manipulate session
868
+ c.session()['foo'] = 'bar'
869
+ foo = c.session()['foo']
870
+ del c.session()['foo']
871
+
872
+ # Expiration date in seconds from now (persists between requests)
873
+ c.session(expiration=604800)
874
+
875
+ # Expiration date as absolute epoch time (only valid for one request)
876
+ c.session(expires=time.time() + 604800)
877
+
878
+ # Delete whole session by setting an expiration date in the past
879
+ c.session(expires=1)
880
+ """
881
+ stash = self.stash()
882
+ if 'mojo.active_session' not in stash:
883
+ self.app.sessions.load(self)
884
+
885
+ # Dict
886
+ session: dict[str, Any] = stash.setdefault('mojo.session', {})
887
+ if not args and not values:
888
+ return session
889
+
890
+ # Get
891
+ if len(args) == 1 and not isinstance(args[0], dict) and not values:
892
+ return session.get(args[0])
893
+
894
+ # Set
895
+ for arg in args:
896
+ session.update(arg)
897
+ session.update(values)
898
+
899
+ return self
900
+
901
+ @overload
902
+ def signed_cookie(self, name: str, /) -> str | None: ...
903
+
904
+ @overload
905
+ def signed_cookie(self, name: str, value: str, options: dict[str, Any] | None = None, /) -> Self: ...
906
+
907
+ def signed_cookie(
908
+ self, name: str, value: str | None = None, options: dict[str, Any] | None = None, /
909
+ ) -> str | None | Self:
910
+ """::
911
+
912
+ value = c.signed_cookie('foo')
913
+ c = c.signed_cookie('foo', 'bar')
914
+ c = c.signed_cookie('foo', 'bar', {'path': '/'})
915
+
916
+ Access signed request cookie values and create new signed response
917
+ cookies. If there are multiple values sharing the same name, and you
918
+ want to access more than just the last one, you can use
919
+ :meth:`every_signed_cookie`. Cookies are cryptographically signed with
920
+ HMAC-SHA256, to prevent tampering, and the ones failing signature
921
+ verification will be automatically discarded.
922
+ """
923
+ # Request cookie
924
+ if value is None:
925
+ every = self.every_signed_cookie(name)
926
+ return every[-1] if every else None
927
+
928
+ # Response cookie
929
+ sum = _hmac(f'{name}={value}', self.app.secrets[0])
930
+ return self.cookie(name, f'{value}--{sum}', options)
931
+
932
+ # stash(**values) with no values is stash(), which the first overload types
933
+ @overload
934
+ def stash(self) -> dict[str, Any]: ... # pyright: ignore[reportOverlappingOverload]
935
+
936
+ @overload
937
+ def stash(self, name: str, /) -> Any: ...
938
+
939
+ @overload
940
+ def stash(self, values: dict[str, Any], /) -> Self: ...
941
+
942
+ @overload
943
+ def stash(self, **values: Any) -> Self: ...
944
+
945
+ def stash(self, *args: Any, **values: Any) -> Any:
946
+ """::
947
+
948
+ hash = c.stash()
949
+ foo = c.stash('foo')
950
+ c = c.stash({'foo': 'bar', 'baz': 23})
951
+ c = c.stash(foo='bar', baz=23)
952
+
953
+ Non-persistent data storage and exchange for the current request,
954
+ application wide default values can be set with
955
+ :meth:`Pyjoyment.Pyjoyment_App.defaults`. Some stash values have a
956
+ special meaning and are reserved, the full list is currently
957
+ ``action``, ``app``, ``cb``, ``controller``, ``data``, ``extends``,
958
+ ``format``, ``handler``, ``inline``, ``json``, ``layout``,
959
+ ``namespace``, ``path``, ``status``, ``template``, ``text``,
960
+ ``toml``, ``variant`` and ``yaml``. Note that all stash values with a ``mojo.*`` prefix are
961
+ reserved for internal use. ::
962
+
963
+ # Remove value
964
+ foo = c.stash().pop('foo')
965
+
966
+ # Assign multiple values at once
967
+ c.stash(foo='test', bar=23)
968
+ """
969
+ return Pyjo.Util._stash('stash', self, args, values) # pyright: ignore[reportPrivateUsage]
970
+
971
+ def url_for(
972
+ self, target: 'str | dict[str, Any] | Pyjo.URL.object | None' = None, /, *args: dict[str, Any], **values: Any
973
+ ) -> Pyjo.URL.object:
974
+ """::
975
+
976
+ url = c.url_for()
977
+ url = c.url_for(name='sebastian')
978
+ url = c.url_for({'name': 'sebastian'})
979
+ url = c.url_for('test', name='sebastian')
980
+ url = c.url_for('test', {'name': 'sebastian'})
981
+ url = c.url_for('/index.html')
982
+ url = c.url_for('//example.com/index.html')
983
+ url = c.url_for('http://example.com/index.html')
984
+ url = c.url_for('mailto:sri@example.com')
985
+ url = c.url_for('#whatever')
986
+
987
+ Generate a portable :mod:`Pyjo.URL` object with base for a path, URL or
988
+ route. ::
989
+
990
+ # Rebuild URL for the current route
991
+ c.url_for()
992
+
993
+ # Rebuild URL for the current route, but replace the "name" placeholder value
994
+ c.url_for(name='sebastian')
995
+
996
+ # Absolute URL for the current route
997
+ c.url_for().to_abs()
998
+
999
+ # Build URL for route "test" with two placeholder values
1000
+ c.url_for('test', name='sebastian', foo='bar')
1001
+
1002
+ # "http://127.0.0.1:3000/index.html" if application was started with Pyjo.Server.Reload
1003
+ c.url_for('/index.html').to_abs()
1004
+
1005
+ # "https://127.0.0.1:443/index.html" if application was started with Pyjo.Server.Reload
1006
+ c.url_for('/index.html').to_abs().set(scheme='https', port=443)
1007
+
1008
+ # "/index.html?foo=bar" if application is deployed under "/"
1009
+ c.url_for('/index.html').set(query={'foo': 'bar'})
1010
+
1011
+ # "/myapp/index.html?foo=bar" if application is deployed under "/myapp"
1012
+ c.url_for('/index.html').set(query={'foo': 'bar'})
1013
+
1014
+ You can also use the helper ``url_with`` of
1015
+ :mod:`Pyjoyment.Plugin.DefaultHelpers` to inherit query parameters from
1016
+ the current request. ::
1017
+
1018
+ # "/list?q=mojo&page=2" if current request was for "/list?q=mojo&page=1"
1019
+ c.url_with().set(query={'page': 2})
1020
+ """
1021
+ if target is None:
1022
+ target = ''
1023
+
1024
+ # Absolute URL
1025
+ if isinstance(target, Pyjo.URL.object):
1026
+ return target
1027
+ if isinstance(target, str) and _ABSOLUTE.match(target):
1028
+ return Pyjo.URL.new(target)
1029
+
1030
+ # Base
1031
+ url = Pyjo.URL.new()
1032
+ req = self.req
1033
+ url.base = req.url.base.clone()
1034
+ base = url.base
1035
+ base.userinfo = None
1036
+
1037
+ # Relative URL
1038
+ path = url.path
1039
+ if isinstance(target, str) and target.startswith('/'):
1040
+ if (prefix := self.stash().get('path')) is not None:
1041
+ real = req.url.path.to_route()
1042
+ if m := re.search(rf'/?{re.escape(prefix)}$', real):
1043
+ real = real[: m.start()] + target + real[m.end() :]
1044
+ target = real
1045
+ url.parse(target)
1046
+
1047
+ # Route
1048
+ else:
1049
+ generated = self.match.path_for(target, *args, **values)
1050
+ if generated.get('path'):
1051
+ path.parse(generated['path'])
1052
+ if generated.get('websocket'):
1053
+ base.scheme = 'wss' if base.protocol == 'https' else 'ws'
1054
+
1055
+ # Make path absolute (skip parsing if there is no base path and nothing would need to be decoded)
1056
+ base_path = base.path
1057
+ if base_path.to_str() == '' and '%' not in path.to_str():
1058
+ return url
1059
+ path.parts[0:0] = base_path.parts
1060
+ base_path.parts = []
1061
+ base_path.trailing_slash = False
1062
+
1063
+ return url
1064
+
1065
+ def url_for_asset(self, asset: str) -> Pyjo.URL.object:
1066
+ """::
1067
+
1068
+ url = c.url_for_asset('/app.js')
1069
+
1070
+ Generate a portable :mod:`Pyjo.URL` object with base for a static
1071
+ asset. In ``development`` mode the asset directories are scanned again
1072
+ every time, so assets that have just been rebuilt with a different name
1073
+ are found too.
1074
+ """
1075
+ if _ABSOLUTE.match(asset):
1076
+ return self.url_for(asset)
1077
+
1078
+ # Bundlers can rebuild assets with new names at any time during development
1079
+ app = self.app
1080
+ if app.mode == 'development':
1081
+ app.static.warmup()
1082
+ return self.url_for(app.static.asset_path(asset))
1083
+
1084
+ def url_for_file(self, file: str) -> Pyjo.URL.object:
1085
+ """::
1086
+
1087
+ url = c.url_for_file('/index.html')
1088
+
1089
+ Generate a portable :mod:`Pyjo.URL` object with base for a static file.
1090
+ """
1091
+ return self.url_for(file if _ABSOLUTE.match(file) else self.app.static.file_path(file))
1092
+
1093
+ def write(self, chunk: bytes | None = None, cb: Callable[[Self], Any] | None = None) -> Self:
1094
+ """::
1095
+
1096
+ c = c.write()
1097
+ c = c.write(b'')
1098
+ c = c.write(data)
1099
+ c = c.write(data, lambda c: ...)
1100
+
1101
+ Write dynamic content non-blocking, the optional drain callback will be
1102
+ executed once all data has been written. Calling this method without a
1103
+ chunk of data will finalize the response headers and allow for dynamic
1104
+ content to be written later. ::
1105
+
1106
+ # Keep connection alive (with Content-Length header)
1107
+ c.res.headers.content_length = 6
1108
+ c.write(b'Hel', lambda c: c.write(b'lo!'))
1109
+
1110
+ # Close connection when finished (without Content-Length header)
1111
+ c.write(b'Hel', lambda c: c.write(b'lo!', lambda c: c.finish()))
1112
+
1113
+ You can call :meth:`finish` or write an empty chunk of data at any time
1114
+ to end the stream. ::
1115
+
1116
+ HTTP/1.1 200 OK
1117
+ Date: Sat, 13 Sep 2014 16:48:29 GMT
1118
+ Content-Length: 6
1119
+ Server: Pyjoyment (Python)
1120
+
1121
+ Hello!
1122
+
1123
+ HTTP/1.1 200 OK
1124
+ Connection: close
1125
+ Date: Sat, 13 Sep 2014 16:48:29 GMT
1126
+ Server: Pyjoyment (Python)
1127
+
1128
+ Hello!
1129
+
1130
+ For Comet (long polling) you might also want to increase the inactivity
1131
+ timeout with the helper ``inactivity_timeout`` of
1132
+ :mod:`Pyjoyment.Plugin.DefaultHelpers`, which usually defaults to
1133
+ ``30`` seconds. ::
1134
+
1135
+ # Increase inactivity timeout for connection to 300 seconds
1136
+ c.inactivity_timeout(300)
1137
+ """
1138
+ self.res.content.write(chunk, _drain(self, cb))
1139
+ return self.rendered()
1140
+
1141
+ def write_chunk(self, chunk: bytes | None = None, cb: Callable[[Self], Any] | None = None) -> Self:
1142
+ """::
1143
+
1144
+ c = c.write_chunk()
1145
+ c = c.write_chunk(b'')
1146
+ c = c.write_chunk(data)
1147
+ c = c.write_chunk(data, lambda c: ...)
1148
+
1149
+ Write dynamic content non-blocking with chunked transfer encoding, the
1150
+ optional drain callback will be executed once all data has been
1151
+ written. Calling this method without a chunk of data will finalize the
1152
+ response headers and allow for dynamic content to be written later. ::
1153
+
1154
+ # Make sure previous chunk has been written before continuing
1155
+ c.write_chunk(b'H', lambda c: c.write_chunk(b'ell', lambda c: c.finish(b'o!')))
1156
+
1157
+ You can call :meth:`finish` or write an empty chunk of data at any time
1158
+ to end the stream. ::
1159
+
1160
+ HTTP/1.1 200 OK
1161
+ Date: Sat, 13 Sep 2014 16:48:29 GMT
1162
+ Transfer-Encoding: chunked
1163
+ Server: Pyjoyment (Python)
1164
+
1165
+ 1
1166
+ H
1167
+ 3
1168
+ ell
1169
+ 2
1170
+ o!
1171
+ 0
1172
+ """
1173
+ self.res.content.write_chunk(chunk, _drain(self, cb))
1174
+ return self.rendered()
1175
+
1176
+ def write_sse(self, event: dict[str, Any] | None = None, cb: Callable[[Self], Any] | None = None) -> Self:
1177
+ """::
1178
+
1179
+ c = c.write_sse()
1180
+ c = c.write_sse(event)
1181
+ c = c.write_sse(event, lambda c: ...)
1182
+
1183
+ Write Server-Sent Event (SSE) non-blocking, the optional drain callback
1184
+ will be executed once all data has been written. Calling this method
1185
+ without an event will finalize the response headers and allow for
1186
+ events to be written later. Note that this method is **EXPERIMENTAL**
1187
+ and might change without warning! ::
1188
+
1189
+ # Send event with default "message" type
1190
+ c.write_sse({'text': 'Hello World!'})
1191
+
1192
+ # You can send comments in regular intervals to keep the connection alive
1193
+ c.write_sse({'comment': 'Keep connection alive!'})
1194
+
1195
+ # Make sure previous event has been written before continuing (id and type are optional)
1196
+ c.write_sse(
1197
+ {'id': 23, 'type': 'greeting', 'text': 'Hello World!'},
1198
+ lambda c: c.write_sse({'id': 24, 'type': 'farewell', 'text': 'Goodbye World!'}, lambda c: c.finish()),
1199
+ )
1200
+ """
1201
+ self.res.content.write_sse(event, _drain(self, cb))
1202
+ return self.rendered()
1203
+
1204
+ def _transaction(self) -> 'Pyjo.Transaction.object':
1205
+ if (tx := self.tx) is None:
1206
+ raise Pyjo.Exception.new('Transaction already destroyed')
1207
+ return tx
1208
+
1209
+
1210
+ # A drain callback of the transaction or the content that the controller gets
1211
+ def _drain[C](c: C, cb: Callable[[C], Any] | None) -> Any:
1212
+ if cb is None:
1213
+ return None
1214
+
1215
+ def drain(*args: Any) -> None:
1216
+ cb(c)
1217
+
1218
+ return drain
1219
+
1220
+
1221
+ # A helper bound to the controller, whose missing attribute names the call of the helper, as c.ua.get_p(...) is a
1222
+ # common mistake for c.ua().get_p(...)
1223
+ class _Helper(functools.partial[Any]):
1224
+ def __getattr__(self, name: str) -> Any:
1225
+ if name.startswith('_'):
1226
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'", name=name, obj=self)
1227
+ app = self.args[0].app
1228
+ helpers: dict[str, Any] = app.renderer.helpers if app is not None else {}
1229
+ helper = next((h for h, cb in helpers.items() if cb is self.func), getattr(self.func, '__name__', 'helper'))
1230
+ raise AttributeError(
1231
+ f'Helper "{helper}" has no attribute "{name}", call the helper first: {helper}().{name}',
1232
+ name=name,
1233
+ obj=self,
1234
+ )
1235
+
1236
+
1237
+ # Not in Mojolicious: Perl has no type checkers, and mojo.js declares the types of its helpers in an interface that
1238
+ # TypeScript merges
1239
+ class Helper[**P, R]:
1240
+ """::
1241
+
1242
+ users = Pyjoyment.Controller.Helper(users)
1243
+
1244
+ Declare the type of a helper, for type checkers and editors, as an
1245
+ attribute of a class with the name of the helper. The type is the one of
1246
+ the function of the helper, without the controller that the helper gets
1247
+ first, so that ``c.users()`` returns what ``users(c)`` does. The helper
1248
+ itself is still found by its name in the renderer of the application, as
1249
+ any other helper, so an application or a plugin can replace it with
1250
+ :meth:`Pyjoyment.Pyjoyment_App.helper`. It is not an action, as a route
1251
+ only reaches the methods of a controller. ::
1252
+
1253
+ import Pyjoyment.Controller
1254
+
1255
+ # The helper, which startup registers with app.helper('users', users)
1256
+ def users(c: Pyjoyment.Controller.object) -> MyApp.Model.Users.object:
1257
+ return c.app.users
1258
+
1259
+ # The base class of the controllers of the application
1260
+ class Controller(Pyjoyment.Controller.object):
1261
+ users = Pyjoyment.Controller.Helper(users)
1262
+
1263
+ class Users(Controller):
1264
+ def index(self) -> None:
1265
+ self.render(json=self.users().list())
1266
+
1267
+ A plugin declares the types of its helpers in a class of its own, which
1268
+ the base class of the controllers inherits, as
1269
+ :class:`Pyjoyment.Plugin.DefaultHelpers.Helpers` declares the default
1270
+ helpers, which :mod:`Pyjoyment.Controller` and :mod:`Pyjoyment` have for
1271
+ type checkers. A prefix of nested helpers is a class of its own too::
1272
+
1273
+ class Reply:
1274
+ not_found = Pyjoyment.Controller.Helper(_not_found)
1275
+
1276
+ class Helpers:
1277
+ reply: Reply
1278
+ """
1279
+
1280
+ def __init__(self, cb: Callable[Concatenate[Any, P], R]) -> None:
1281
+ self.cb = cb
1282
+ self.name = ''
1283
+
1284
+ def __set_name__(self, owner: type, name: str) -> None:
1285
+ self.name = name
1286
+
1287
+ @overload
1288
+ def __get__(self, obj: None, owner: type | None = None) -> Self: ...
1289
+
1290
+ @overload
1291
+ def __get__(self, obj: Any, owner: type | None = None) -> Callable[P, R]: ...
1292
+
1293
+ def __get__(self, obj: Any, owner: type | None = None) -> Self | Callable[P, R]:
1294
+ return self if obj is None else cast(Any, type(obj)).__getattr__(obj, self.name)
1295
+
1296
+
1297
+ # The code of a helper that is got through its type, whose frame __getattr__ skips for the location of the caller
1298
+ _HELPER_GET = vars(Helper)['__get__'].__code__
1299
+
1300
+
1301
+ def _hmac(data: str, secret: str) -> str:
1302
+ return hmac.new(secret.encode(), data.encode(), hashlib.sha256).hexdigest()
1303
+
1304
+
1305
+ new = Pyjoyment_Controller.new
1306
+ object = Pyjoyment_Controller
1307
+ Controller = Pyjoyment_Controller