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
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjoyment.Routes - Always find your destination with routes
|
|
3
|
+
===========================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjoyment.Routes import Routes
|
|
7
|
+
|
|
8
|
+
# Simple route
|
|
9
|
+
r = Routes()
|
|
10
|
+
r.any('/').to(controller='blog', action='welcome')
|
|
11
|
+
|
|
12
|
+
# Or import the module, as in Google Python Style Guide
|
|
13
|
+
|
|
14
|
+
import Pyjoyment.Routes
|
|
15
|
+
|
|
16
|
+
# Simple route
|
|
17
|
+
r = Pyjoyment.Routes.new()
|
|
18
|
+
r.any('/').to(controller='blog', action='welcome')
|
|
19
|
+
|
|
20
|
+
# More advanced routes
|
|
21
|
+
blog = r.under('/blog')
|
|
22
|
+
blog.get('/list').to('blog#list')
|
|
23
|
+
blog.get('/:id', id=re.compile(r'\\d+')).to('blog#show', id=23)
|
|
24
|
+
blog.patch(lambda c: c.render(text='Go away!', status=405))
|
|
25
|
+
|
|
26
|
+
:mod:`Pyjoyment.Routes` is the core of the :mod:`Pyjoyment` web framework.
|
|
27
|
+
|
|
28
|
+
Types
|
|
29
|
+
-----
|
|
30
|
+
|
|
31
|
+
These placeholder types are available by default.
|
|
32
|
+
|
|
33
|
+
num ::
|
|
34
|
+
|
|
35
|
+
r.get('/article/<id:num>')
|
|
36
|
+
|
|
37
|
+
Placeholder value needs to be a non-fractional number, similar to the regular
|
|
38
|
+
expression ``([0-9]+)``.
|
|
39
|
+
|
|
40
|
+
See also
|
|
41
|
+
--------
|
|
42
|
+
|
|
43
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
44
|
+
|
|
45
|
+
Ported from `Mojolicious::Routes <https://github.com/mojolicious/mojo/blob/v9.52/lib/Mojolicious/Routes.pm>`_
|
|
46
|
+
of Mojolicious 9.52.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
import Pyjo.App
|
|
50
|
+
import Pyjo.Cache
|
|
51
|
+
import Pyjo.Exception
|
|
52
|
+
import Pyjo.Loader
|
|
53
|
+
import Pyjoyment.Routes.Pattern
|
|
54
|
+
import Pyjoyment.Routes.Route
|
|
55
|
+
|
|
56
|
+
from Pyjo.Util import camelize
|
|
57
|
+
from Pyjoyment.Routes.Route import Pyjoyment_Routes_Route
|
|
58
|
+
|
|
59
|
+
import re
|
|
60
|
+
import sys
|
|
61
|
+
|
|
62
|
+
from collections.abc import Callable
|
|
63
|
+
from typing import TYPE_CHECKING, Any, Self, cast
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if TYPE_CHECKING:
|
|
67
|
+
import Pyjoyment
|
|
68
|
+
import Pyjoyment.Controller
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# A condition gets the route, the controller, the captures and the value that the route requires
|
|
72
|
+
type Condition = Callable[[Pyjoyment.Routes.Route.object, Any, dict[str, Any], Any], Any]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Pyjoyment.Routes is not an attribute of Pyjoyment until this package is loaded
|
|
76
|
+
class Pyjoyment_Routes(Pyjoyment_Routes_Route):
|
|
77
|
+
"""
|
|
78
|
+
:mod:`Pyjoyment.Routes` inherits all attributes and methods from
|
|
79
|
+
:mod:`Pyjoyment.Routes.Route` and implements the following new ones.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self) -> None:
|
|
83
|
+
"""::
|
|
84
|
+
|
|
85
|
+
r = Pyjoyment.Routes.new()
|
|
86
|
+
|
|
87
|
+
Construct a new :mod:`Pyjoyment.Routes` object.
|
|
88
|
+
"""
|
|
89
|
+
super().__init__()
|
|
90
|
+
|
|
91
|
+
self.base_classes = ['Pyjoyment.Controller', 'Pyjoyment']
|
|
92
|
+
"""::
|
|
93
|
+
|
|
94
|
+
classes = r.base_classes
|
|
95
|
+
r.base_classes = ['MyApp.Controller']
|
|
96
|
+
|
|
97
|
+
Base classes used to identify controllers, defaults to
|
|
98
|
+
:mod:`Pyjoyment.Controller` and :mod:`Pyjoyment`.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
self.cache: Pyjo.Cache.object[str, Any] = Pyjo.Cache.new()
|
|
102
|
+
"""::
|
|
103
|
+
|
|
104
|
+
cache = r.cache
|
|
105
|
+
r.cache = Pyjo.Cache.new()
|
|
106
|
+
|
|
107
|
+
Routing cache, defaults to a :mod:`Pyjo.Cache` object.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
self.conditions: dict[str, Condition] = {}
|
|
111
|
+
"""::
|
|
112
|
+
|
|
113
|
+
conditions = r.conditions
|
|
114
|
+
r.conditions = {'foo': lambda route, c, captures, arg: ...}
|
|
115
|
+
|
|
116
|
+
Contains all available conditions.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
self.namespaces: list[str] = []
|
|
120
|
+
"""::
|
|
121
|
+
|
|
122
|
+
namespaces = r.namespaces
|
|
123
|
+
r.namespaces = ['MyApp.Controller', 'MyApp']
|
|
124
|
+
|
|
125
|
+
Namespaces to load controllers from. ::
|
|
126
|
+
|
|
127
|
+
# Add another namespace to load controllers from
|
|
128
|
+
r.namespaces.append('MyApp.MyController')
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
self.shortcuts: dict[str, Callable[..., Any]] = {}
|
|
132
|
+
"""::
|
|
133
|
+
|
|
134
|
+
shortcuts = r.shortcuts
|
|
135
|
+
r.shortcuts = {'foo': lambda route, *args: ...}
|
|
136
|
+
|
|
137
|
+
Contains all available shortcuts.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
self.types: dict[str, Pyjoyment.Routes.Pattern.Constraint] = {'num': re.compile(r'[0-9]+')}
|
|
141
|
+
"""::
|
|
142
|
+
|
|
143
|
+
types = r.types
|
|
144
|
+
r.types = {'lower': re.compile(r'[a-z]+')}
|
|
145
|
+
|
|
146
|
+
Registered placeholder types, by default only ``num`` is already
|
|
147
|
+
defined.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
self._loaded: dict[str, type] = {}
|
|
151
|
+
self._reverse: dict[str, Pyjoyment.Routes.Route.object] | None = None
|
|
152
|
+
|
|
153
|
+
def add_condition(self, name: str, cb: Condition) -> Self:
|
|
154
|
+
"""::
|
|
155
|
+
|
|
156
|
+
r = r.add_condition('foo', lambda route, c, captures, arg: ...)
|
|
157
|
+
|
|
158
|
+
Register a condition. ::
|
|
159
|
+
|
|
160
|
+
def foo(route, c, captures, arg):
|
|
161
|
+
...
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
r.add_condition('foo', foo)
|
|
165
|
+
"""
|
|
166
|
+
self.conditions[name] = cb
|
|
167
|
+
return self
|
|
168
|
+
|
|
169
|
+
def add_shortcut(self, name: str, cb: Callable[..., Any]) -> Self:
|
|
170
|
+
"""::
|
|
171
|
+
|
|
172
|
+
r = r.add_shortcut('foo', lambda route, *args: ...)
|
|
173
|
+
|
|
174
|
+
Register a shortcut. ::
|
|
175
|
+
|
|
176
|
+
r.add_shortcut('foo', lambda route, *args: ...)
|
|
177
|
+
"""
|
|
178
|
+
self.shortcuts[name] = cb
|
|
179
|
+
return self
|
|
180
|
+
|
|
181
|
+
def add_type(self, name: str, type: 'Pyjoyment.Routes.Pattern.Constraint') -> Self:
|
|
182
|
+
"""::
|
|
183
|
+
|
|
184
|
+
r = r.add_type('foo', re.compile(r'\\w+'))
|
|
185
|
+
r = r.add_type('foo', ['bar', 'baz'])
|
|
186
|
+
|
|
187
|
+
Register a placeholder type. ::
|
|
188
|
+
|
|
189
|
+
r.add_type('lower', re.compile(r'[a-z]+'))
|
|
190
|
+
"""
|
|
191
|
+
self.types[name] = type
|
|
192
|
+
return self
|
|
193
|
+
|
|
194
|
+
# Renamed from Mojolicious: continue, which is a keyword of Python
|
|
195
|
+
def proceed(self, c: 'Pyjoyment.Controller.object') -> None:
|
|
196
|
+
"""::
|
|
197
|
+
|
|
198
|
+
r.proceed(Pyjoyment.Controller.new())
|
|
199
|
+
|
|
200
|
+
Continue dispatch chain and emit the hook ``around_action`` of
|
|
201
|
+
:mod:`Pyjoyment` for every action. ``continue_`` is an alias, the name
|
|
202
|
+
of Mojolicious with an underscore.
|
|
203
|
+
"""
|
|
204
|
+
match = c.match
|
|
205
|
+
stack = match.stack
|
|
206
|
+
position = match.position
|
|
207
|
+
if position >= len(stack):
|
|
208
|
+
return _render(c)
|
|
209
|
+
field = stack[position]
|
|
210
|
+
|
|
211
|
+
# Merge captures into stash
|
|
212
|
+
stash = c.stash()
|
|
213
|
+
stash.setdefault('mojo.captures', {}).update(field)
|
|
214
|
+
stash.update(field)
|
|
215
|
+
|
|
216
|
+
position += 1
|
|
217
|
+
last = position >= len(stack)
|
|
218
|
+
if cb := field.get('cb'):
|
|
219
|
+
proceed = self._callback(c, cb, last)
|
|
220
|
+
else:
|
|
221
|
+
proceed = self._controller(c, field, last)
|
|
222
|
+
match.position = position
|
|
223
|
+
if last or proceed:
|
|
224
|
+
self.proceed(c)
|
|
225
|
+
|
|
226
|
+
# The name of Mojolicious, with an underscore as continue is a keyword of Python
|
|
227
|
+
continue_ = proceed
|
|
228
|
+
|
|
229
|
+
def dispatch(self, c: 'Pyjoyment.Controller.object') -> bool:
|
|
230
|
+
"""::
|
|
231
|
+
|
|
232
|
+
boolean = r.dispatch(Pyjoyment.Controller.new())
|
|
233
|
+
|
|
234
|
+
Match routes with :meth:`match` and dispatch with :meth:`proceed`.
|
|
235
|
+
"""
|
|
236
|
+
self.match(c)
|
|
237
|
+
if not c.match.stack:
|
|
238
|
+
return False
|
|
239
|
+
self.proceed(c)
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
def lookup(self, name: str) -> 'Pyjoyment.Routes.Route.object | None':
|
|
243
|
+
"""::
|
|
244
|
+
|
|
245
|
+
route = r.lookup('foo')
|
|
246
|
+
|
|
247
|
+
Find route by name with
|
|
248
|
+
:meth:`~Pyjoyment.Routes.Route.Pyjoyment_Routes_Route.find` and cache
|
|
249
|
+
all results for future lookups.
|
|
250
|
+
"""
|
|
251
|
+
if self._reverse is None:
|
|
252
|
+
self._reverse = self._index()
|
|
253
|
+
return self._reverse.get(name)
|
|
254
|
+
|
|
255
|
+
def match(self, c: 'Pyjoyment.Controller.object') -> None:
|
|
256
|
+
"""::
|
|
257
|
+
|
|
258
|
+
r.match(Pyjoyment.Controller.new())
|
|
259
|
+
|
|
260
|
+
Match routes with :mod:`Pyjoyment.Routes.Match`.
|
|
261
|
+
"""
|
|
262
|
+
# Path (partial path gets priority)
|
|
263
|
+
req = c.req
|
|
264
|
+
path = c.stash().get('path')
|
|
265
|
+
if path is not None:
|
|
266
|
+
if not path.startswith('/'):
|
|
267
|
+
path = f'/{path}'
|
|
268
|
+
else:
|
|
269
|
+
path = req.url.path.to_route()
|
|
270
|
+
|
|
271
|
+
# Method (HEAD will be treated as GET)
|
|
272
|
+
method = req.method.upper()
|
|
273
|
+
if method == 'POST' and (override := req.url.query.clone().param('_method')):
|
|
274
|
+
method = override.upper()
|
|
275
|
+
if method == 'HEAD':
|
|
276
|
+
method = 'GET'
|
|
277
|
+
|
|
278
|
+
# Check cache
|
|
279
|
+
tx: Any = c.tx
|
|
280
|
+
ws = 1 if tx.is_websocket else 0
|
|
281
|
+
match = c.match
|
|
282
|
+
match.root = self
|
|
283
|
+
cache = self.cache
|
|
284
|
+
if result := cache.get(f'{method}:{path}:{ws}'):
|
|
285
|
+
match.endpoint, match.stack = result['endpoint'], result['stack']
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
# Check routes
|
|
289
|
+
match.find(c, method=method, path=path, websocket=bool(ws))
|
|
290
|
+
if (route := match.endpoint) is None:
|
|
291
|
+
return
|
|
292
|
+
cache.set(f'{method}:{path}:{ws}', {'endpoint': route, 'stack': match.stack})
|
|
293
|
+
|
|
294
|
+
def _callback(self, c: 'Pyjoyment.Controller.object', cb: Callable[..., Any], last: bool) -> Any:
|
|
295
|
+
if last:
|
|
296
|
+
c.stash()['mojo.routed'] = True
|
|
297
|
+
c.helpers.log().trace('Routing to a callback')
|
|
298
|
+
return _action(c.app, c, cb, last)
|
|
299
|
+
|
|
300
|
+
def _class(self, c: 'Pyjoyment.Controller.object', field: dict[str, Any]) -> Any:
|
|
301
|
+
# Application instance
|
|
302
|
+
if field.get('app') is not None and not isinstance(field['app'], str):
|
|
303
|
+
return field['app']
|
|
304
|
+
|
|
305
|
+
# Application class
|
|
306
|
+
classes: list[str] = []
|
|
307
|
+
cls = camelize(field['controller']).replace('::', '.') if field.get('controller') else ''
|
|
308
|
+
if field.get('app'):
|
|
309
|
+
classes.append(field['app'])
|
|
310
|
+
|
|
311
|
+
# Specific namespace
|
|
312
|
+
elif (namespace := field.get('namespace')) is not None:
|
|
313
|
+
if not cls:
|
|
314
|
+
raise Pyjo.Exception.new(f'Namespace "{namespace}" requires a controller')
|
|
315
|
+
classes.append(f'{namespace}.{cls}' if namespace else cls)
|
|
316
|
+
|
|
317
|
+
# All namespaces
|
|
318
|
+
elif cls:
|
|
319
|
+
classes.extend(f'{namespace}.{cls}' for namespace in self.namespaces)
|
|
320
|
+
|
|
321
|
+
# Try to load all classes
|
|
322
|
+
for name in classes:
|
|
323
|
+
# Failed
|
|
324
|
+
if (found := self._load(name)) is None:
|
|
325
|
+
continue
|
|
326
|
+
if not found:
|
|
327
|
+
raise Pyjo.Exception.new(f'Class "{name}" is not a controller')
|
|
328
|
+
|
|
329
|
+
# Success
|
|
330
|
+
return cast(type[Any], found).new(app=c.app, match=c.match, stash=c.stash(), tx=c.tx)
|
|
331
|
+
|
|
332
|
+
# Nothing found
|
|
333
|
+
if classes:
|
|
334
|
+
raise Pyjo.Exception.new(f'Controller "{classes[-1]}" does not exist')
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
def _controller(self, old: 'Pyjoyment.Controller.object', field: dict[str, Any], last: bool) -> bool:
|
|
338
|
+
# Load and instantiate controller/application
|
|
339
|
+
new: Any = self._class(old, field)
|
|
340
|
+
if not new:
|
|
341
|
+
return new is not None
|
|
342
|
+
|
|
343
|
+
# Application
|
|
344
|
+
controller = cast(type[Any], type(new))
|
|
345
|
+
cls = f'{controller.__module__}.{controller.__qualname__}'
|
|
346
|
+
log = old.helpers.log()
|
|
347
|
+
# Unlike Mojolicious: any application of Pyjo, where Mojolicious embeds its own only, as Mojolicious 6 embedded
|
|
348
|
+
# any Mojo, so that a route embeds an application of another framework, which Pyjo.App.WSGI runs
|
|
349
|
+
if isinstance(new, Pyjo.App.object):
|
|
350
|
+
log.trace(f'Routing to application "{cls}"')
|
|
351
|
+
|
|
352
|
+
# Try to connect routes
|
|
353
|
+
if (r := getattr(new, 'routes', None)) is not None and r.parent is None:
|
|
354
|
+
r.parent = old.match.endpoint
|
|
355
|
+
new.handler(old)
|
|
356
|
+
old.stash()['mojo.routed'] = True
|
|
357
|
+
|
|
358
|
+
# Action
|
|
359
|
+
elif method := field.get('action'):
|
|
360
|
+
log.trace(f'Routing to controller "{cls}" and action "{method}"')
|
|
361
|
+
|
|
362
|
+
if callable(sub := getattr(controller, method, None)):
|
|
363
|
+
if last:
|
|
364
|
+
old.stash()['mojo.routed'] = True
|
|
365
|
+
if _action(old.app, new, sub, last):
|
|
366
|
+
return True
|
|
367
|
+
|
|
368
|
+
else:
|
|
369
|
+
log.trace('Action not found in controller')
|
|
370
|
+
|
|
371
|
+
else:
|
|
372
|
+
raise Pyjo.Exception.new(f'Controller "{cls}" requires an action')
|
|
373
|
+
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
def _load(self, name: str) -> type | bool | None:
|
|
377
|
+
# Load unless already loaded
|
|
378
|
+
if (loaded := self._loaded.get(name)) is not None:
|
|
379
|
+
return loaded
|
|
380
|
+
if isinstance(e := Pyjo.Loader.load_class(name), Exception):
|
|
381
|
+
raise e
|
|
382
|
+
|
|
383
|
+
# Not in Mojolicious: the class of the application of a module of Pyjoyment.Lite, which Lite keeps with the
|
|
384
|
+
# functions of the module, as a module is not a class
|
|
385
|
+
lite: dict[str, Any] | None = getattr(sys.modules.get(name), '__pyjoyment_lite__', None)
|
|
386
|
+
if (cls := lite['class'] if lite else Pyjo.Loader._class(name)) is None: # pyright: ignore[reportPrivateUsage]
|
|
387
|
+
return None
|
|
388
|
+
|
|
389
|
+
# Check base classes
|
|
390
|
+
bases = [Pyjo.Loader._class(base) for base in self.base_classes] # pyright: ignore[reportPrivateUsage]
|
|
391
|
+
if not any(base is not None and issubclass(cls, base) for base in bases):
|
|
392
|
+
return False
|
|
393
|
+
self._loaded[name] = cls
|
|
394
|
+
return cls
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _action(app: Any, *args: Any) -> Any:
|
|
398
|
+
return app.plugins.emit_chain('around_action', *args)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _render(c: 'Pyjoyment.Controller.object') -> None:
|
|
402
|
+
stash = c.stash()
|
|
403
|
+
if stash.get('mojo.rendered'):
|
|
404
|
+
return
|
|
405
|
+
if not c.render_maybe():
|
|
406
|
+
if not stash.get('mojo.routed'):
|
|
407
|
+
raise Pyjo.Exception.new('Route without action and nothing to render')
|
|
408
|
+
# Not in Mojolicious: an action that returned its response, as a view of another framework, which would wait
|
|
409
|
+
# for a delayed response until the inactivity timeout
|
|
410
|
+
if returned := stash.get('pyjo.returned'):
|
|
411
|
+
raise Pyjo.Exception.new(
|
|
412
|
+
f'Action returned a {returned}, which is not a response: c.render(text=...) renders a text and '
|
|
413
|
+
'c.render(json=...) a JSON document'
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
new = Pyjoyment_Routes.new
|
|
418
|
+
object = Pyjoyment_Routes
|
|
419
|
+
Routes = Pyjoyment_Routes
|
Pyjoyment/Sessions.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjoyment.Sessions - Session manager based on signed cookies
|
|
3
|
+
============================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjoyment.Sessions import Sessions
|
|
7
|
+
|
|
8
|
+
sessions = Sessions()
|
|
9
|
+
sessions.cookie_name = 'myapp'
|
|
10
|
+
sessions.default_expiration = 86400
|
|
11
|
+
|
|
12
|
+
# Or import the module, as in Google Python Style Guide
|
|
13
|
+
|
|
14
|
+
import Pyjoyment.Sessions
|
|
15
|
+
|
|
16
|
+
sessions = Pyjoyment.Sessions.new()
|
|
17
|
+
sessions.cookie_name = 'myapp'
|
|
18
|
+
sessions.default_expiration = 86400
|
|
19
|
+
|
|
20
|
+
:mod:`Pyjoyment.Sessions` manages sessions based on signed cookies for
|
|
21
|
+
:mod:`Pyjoyment`. All data gets serialized with :mod:`Pyjo.JSON` and stored
|
|
22
|
+
Base64 encoded on the client-side, but is protected from unwanted changes with
|
|
23
|
+
a HMAC-SHA256 signature.
|
|
24
|
+
|
|
25
|
+
See also
|
|
26
|
+
--------
|
|
27
|
+
|
|
28
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
29
|
+
|
|
30
|
+
Ported from `Mojolicious::Sessions <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojolicious/Sessions.pm>`_
|
|
31
|
+
of Mojolicious 9.49.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import Pyjo.Base
|
|
35
|
+
import Pyjo.JSON
|
|
36
|
+
|
|
37
|
+
from Pyjo.Util import b64_decode, b64_encode
|
|
38
|
+
|
|
39
|
+
import time
|
|
40
|
+
|
|
41
|
+
from collections.abc import Callable
|
|
42
|
+
from typing import TYPE_CHECKING, Any
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
import Pyjoyment.Controller
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Pyjoyment_Sessions(Pyjo.Base.object):
|
|
50
|
+
"""
|
|
51
|
+
:mod:`Pyjoyment.Sessions` inherits all methods from :mod:`Pyjo.Base` and
|
|
52
|
+
implements the following new ones.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
"""::
|
|
57
|
+
|
|
58
|
+
sessions = Pyjoyment.Sessions.new()
|
|
59
|
+
|
|
60
|
+
Construct a new :mod:`Pyjoyment.Sessions` object.
|
|
61
|
+
"""
|
|
62
|
+
self.cookie_domain: str | None = None
|
|
63
|
+
"""::
|
|
64
|
+
|
|
65
|
+
domain = sessions.cookie_domain
|
|
66
|
+
sessions.cookie_domain = '.example.com'
|
|
67
|
+
|
|
68
|
+
Domain for session cookies, not defined by default.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
# Unlike Mojolicious: the cookie is named after Pyjoyment, where Mojo names it "mojolicious"
|
|
72
|
+
self.cookie_name = 'pyjoyment'
|
|
73
|
+
"""::
|
|
74
|
+
|
|
75
|
+
name = sessions.cookie_name
|
|
76
|
+
sessions.cookie_name = 'session'
|
|
77
|
+
|
|
78
|
+
Name for session cookies, defaults to ``pyjoyment``.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
self.cookie_path = '/'
|
|
82
|
+
"""::
|
|
83
|
+
|
|
84
|
+
path = sessions.cookie_path
|
|
85
|
+
sessions.cookie_path = '/foo'
|
|
86
|
+
|
|
87
|
+
Path for session cookies, defaults to ``/``.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
self.default_expiration = 3600
|
|
91
|
+
"""::
|
|
92
|
+
|
|
93
|
+
time = sessions.default_expiration
|
|
94
|
+
sessions.default_expiration = 3600
|
|
95
|
+
|
|
96
|
+
Default time for sessions to expire in seconds from now, defaults to
|
|
97
|
+
``3600``. The expiration timeout gets refreshed for every request.
|
|
98
|
+
Setting the value to ``0`` will allow sessions to persist until the
|
|
99
|
+
browser window is closed, this can have security implications though.
|
|
100
|
+
For more control you can also use the ``expiration`` and ``expires``
|
|
101
|
+
session values. ::
|
|
102
|
+
|
|
103
|
+
# Expiration date in seconds from now (persists between requests)
|
|
104
|
+
c.session(expiration=604800)
|
|
105
|
+
|
|
106
|
+
# Expiration date as absolute epoch time (only valid for one request)
|
|
107
|
+
c.session(expires=time.time() + 604800)
|
|
108
|
+
|
|
109
|
+
# Delete whole session by setting an expiration date in the past
|
|
110
|
+
c.session(expires=1)
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
self.deserialize: Callable[[bytes], Any] = Pyjo.JSON.decode_json
|
|
114
|
+
"""::
|
|
115
|
+
|
|
116
|
+
cb = sessions.deserialize
|
|
117
|
+
sessions.deserialize = lambda data: ...
|
|
118
|
+
|
|
119
|
+
A callback used to deserialize sessions, defaults to
|
|
120
|
+
:func:`Pyjo.JSON.decode_json`. ::
|
|
121
|
+
|
|
122
|
+
sessions.deserialize = lambda data: {}
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
# Left out from Mojolicious: encrypted, as encrypt_cookie and decrypt_cookie of Pyjo.Util are left out
|
|
126
|
+
|
|
127
|
+
self.partitioned: bool | None = None
|
|
128
|
+
"""::
|
|
129
|
+
|
|
130
|
+
boolean = sessions.partitioned
|
|
131
|
+
sessions.partitioned = boolean
|
|
132
|
+
|
|
133
|
+
Partitioned flag, this is to be used in accordance to the CHIPS
|
|
134
|
+
amendment to :rfc:`6265`.
|
|
135
|
+
|
|
136
|
+
Note that this attribute is **EXPERIMENTAL** because even though most
|
|
137
|
+
commonly used browsers support the feature, there is no specification
|
|
138
|
+
yet besides `this draft
|
|
139
|
+
<https://www.ietf.org/archive/id/draft-cutler-httpbis-partitioned-cookies-00.html>`_.
|
|
140
|
+
|
|
141
|
+
Partitioned cookies are held within a separate cookie jar per top-level
|
|
142
|
+
site.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
self.samesite: str | None = 'Lax'
|
|
146
|
+
"""::
|
|
147
|
+
|
|
148
|
+
samesite = sessions.samesite
|
|
149
|
+
sessions.samesite = 'Strict'
|
|
150
|
+
|
|
151
|
+
Set the SameSite value on all session cookies, defaults to ``Lax``. ::
|
|
152
|
+
|
|
153
|
+
# Disable SameSite feature
|
|
154
|
+
sessions.samesite = None
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
self.secure: bool | None = None
|
|
158
|
+
"""::
|
|
159
|
+
|
|
160
|
+
boolean = sessions.secure
|
|
161
|
+
sessions.secure = boolean
|
|
162
|
+
|
|
163
|
+
Set the secure flag on all session cookies, so that browsers send them
|
|
164
|
+
only over HTTPS connections.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
self.serialize: Callable[[dict[str, Any]], bytes] = Pyjo.JSON.encode_json
|
|
168
|
+
"""::
|
|
169
|
+
|
|
170
|
+
cb = sessions.serialize
|
|
171
|
+
sessions.serialize = lambda session: ...
|
|
172
|
+
|
|
173
|
+
A callback used to serialize sessions, defaults to
|
|
174
|
+
:func:`Pyjo.JSON.encode_json`. ::
|
|
175
|
+
|
|
176
|
+
sessions.serialize = lambda session: b''
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def load(self, c: 'Pyjoyment.Controller.object') -> None:
|
|
180
|
+
"""::
|
|
181
|
+
|
|
182
|
+
sessions.load(Pyjoyment.Controller.new())
|
|
183
|
+
|
|
184
|
+
Load session data from signed cookie.
|
|
185
|
+
"""
|
|
186
|
+
if not (value := c.signed_cookie(self.cookie_name)):
|
|
187
|
+
return
|
|
188
|
+
if not (session := self.deserialize(b64_decode(value.replace('-', '=')))):
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
# "expiration" value is inherited
|
|
192
|
+
expiration = session.get('expiration')
|
|
193
|
+
if expiration is None:
|
|
194
|
+
expiration = self.default_expiration
|
|
195
|
+
if not (expires := session.pop('expires', None)) and expiration:
|
|
196
|
+
return
|
|
197
|
+
if expires is not None and expires <= time.time():
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
stash = c.stash()
|
|
201
|
+
stash['mojo.active_session'] = len(session)
|
|
202
|
+
if not session:
|
|
203
|
+
return
|
|
204
|
+
stash['mojo.session'] = session
|
|
205
|
+
if session.get('new_flash'):
|
|
206
|
+
session['flash'] = session.pop('new_flash')
|
|
207
|
+
|
|
208
|
+
def store(self, c: 'Pyjoyment.Controller.object') -> None:
|
|
209
|
+
"""::
|
|
210
|
+
|
|
211
|
+
sessions.store(Pyjoyment.Controller.new())
|
|
212
|
+
|
|
213
|
+
Store session data in signed cookie.
|
|
214
|
+
"""
|
|
215
|
+
# Make sure session was active
|
|
216
|
+
stash = c.stash()
|
|
217
|
+
if (session := stash.get('mojo.session')) is None:
|
|
218
|
+
return
|
|
219
|
+
if not session and not stash.get('mojo.active_session'):
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
# Don't reset flash for static files
|
|
223
|
+
old = session.pop('flash', None)
|
|
224
|
+
if stash.get('mojo.static'):
|
|
225
|
+
session['new_flash'] = old
|
|
226
|
+
if not session.get('new_flash'):
|
|
227
|
+
session.pop('new_flash', None)
|
|
228
|
+
|
|
229
|
+
# Generate "expires" value from "expiration" if necessary
|
|
230
|
+
expiration = session.get('expiration')
|
|
231
|
+
if expiration is None:
|
|
232
|
+
expiration = self.default_expiration
|
|
233
|
+
default = session.pop('expires', None)
|
|
234
|
+
if expiration or default:
|
|
235
|
+
session['expires'] = default or int(time.time()) + expiration
|
|
236
|
+
|
|
237
|
+
value = b64_encode(self.serialize(session), '').replace('=', '-')
|
|
238
|
+
options = {
|
|
239
|
+
'domain': self.cookie_domain,
|
|
240
|
+
'expires': session.get('expires'),
|
|
241
|
+
'httponly': True,
|
|
242
|
+
'partitioned': self.partitioned,
|
|
243
|
+
'path': self.cookie_path,
|
|
244
|
+
'samesite': self.samesite,
|
|
245
|
+
'secure': self.secure,
|
|
246
|
+
}
|
|
247
|
+
c.signed_cookie(self.cookie_name, value, options)
|
|
248
|
+
|
|
249
|
+
# Left out from Mojolicious: the removal of the padding of old sessions in _deserialize, which is deprecated, as
|
|
250
|
+
# Pyjoyment has never padded them
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
new = Pyjoyment_Sessions.new
|
|
254
|
+
object = Pyjoyment_Sessions
|
|
255
|
+
Sessions = Pyjoyment_Sessions
|