Pyjoyment 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjoyment/Controller.py
ADDED
|
@@ -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
|