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,251 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjoyment.Routes.Match - Find routes
|
|
3
|
+
====================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
import Pyjoyment.Controller
|
|
7
|
+
import Pyjoyment.Routes
|
|
8
|
+
|
|
9
|
+
from Pyjoyment.Routes.Match import Match
|
|
10
|
+
|
|
11
|
+
# Routes
|
|
12
|
+
r = Pyjoyment.Routes.new()
|
|
13
|
+
r.get('/user/:id')
|
|
14
|
+
r.put('/user/:id')
|
|
15
|
+
|
|
16
|
+
# Match
|
|
17
|
+
c = Pyjoyment.Controller.new()
|
|
18
|
+
match = Match(root=r)
|
|
19
|
+
match.find(c, method='PUT', path='/user/23')
|
|
20
|
+
print(match.stack[0]['id'])
|
|
21
|
+
|
|
22
|
+
# Or import the module, as in Google Python Style Guide
|
|
23
|
+
|
|
24
|
+
import Pyjoyment.Controller
|
|
25
|
+
import Pyjoyment.Routes
|
|
26
|
+
import Pyjoyment.Routes.Match
|
|
27
|
+
|
|
28
|
+
# Routes
|
|
29
|
+
r = Pyjoyment.Routes.new()
|
|
30
|
+
r.get('/user/:id')
|
|
31
|
+
r.put('/user/:id')
|
|
32
|
+
|
|
33
|
+
# Match
|
|
34
|
+
c = Pyjoyment.Controller.new()
|
|
35
|
+
match = Pyjoyment.Routes.Match.new(root=r)
|
|
36
|
+
match.find(c, method='PUT', path='/user/23')
|
|
37
|
+
print(match.stack[0]['id'])
|
|
38
|
+
|
|
39
|
+
# Render
|
|
40
|
+
print(match.path_for()['path'])
|
|
41
|
+
print(match.path_for(id=24)['path'])
|
|
42
|
+
|
|
43
|
+
:mod:`Pyjoyment.Routes.Match` finds routes in :mod:`Pyjoyment.Routes`
|
|
44
|
+
structures.
|
|
45
|
+
|
|
46
|
+
See also
|
|
47
|
+
--------
|
|
48
|
+
|
|
49
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
50
|
+
|
|
51
|
+
Ported from `Mojolicious::Routes::Match
|
|
52
|
+
<https://github.com/mojolicious/mojo/blob/v9.52/lib/Mojolicious/Routes/Match.pm>`_ of Mojolicious 9.52.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
import Pyjo.Base
|
|
56
|
+
import Pyjo.Util
|
|
57
|
+
import Pyjoyment.Routes.Route
|
|
58
|
+
|
|
59
|
+
from typing import TYPE_CHECKING, Any
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if TYPE_CHECKING:
|
|
63
|
+
import Pyjoyment.Controller
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_MISSING = object()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Pyjoyment_Routes_Match(Pyjo.Base.object):
|
|
70
|
+
"""
|
|
71
|
+
:mod:`Pyjoyment.Routes.Match` inherits all methods from :mod:`Pyjo.Base`
|
|
72
|
+
and implements the following new ones.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
endpoint: Pyjoyment.Routes.Route.object | None = None,
|
|
78
|
+
position: int = 0,
|
|
79
|
+
root: Any = None,
|
|
80
|
+
stack: list[dict[str, Any]] | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""::
|
|
83
|
+
|
|
84
|
+
match = Pyjoyment.Routes.Match.new(root=r)
|
|
85
|
+
|
|
86
|
+
Construct a new :mod:`Pyjoyment.Routes.Match` object.
|
|
87
|
+
"""
|
|
88
|
+
self.endpoint = endpoint
|
|
89
|
+
"""::
|
|
90
|
+
|
|
91
|
+
route = match.endpoint
|
|
92
|
+
match.endpoint = Pyjoyment.Routes.Route.new()
|
|
93
|
+
|
|
94
|
+
The route endpoint that matched, usually a :mod:`Pyjoyment.Routes.Route`
|
|
95
|
+
object.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
self.position = position
|
|
99
|
+
"""::
|
|
100
|
+
|
|
101
|
+
position = match.position
|
|
102
|
+
match.position = 2
|
|
103
|
+
|
|
104
|
+
Current position on the :attr:`stack`, defaults to ``0``.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
# The root has the attributes of Pyjoyment.Routes, which imports this module
|
|
108
|
+
self.root = root
|
|
109
|
+
"""::
|
|
110
|
+
|
|
111
|
+
root = match.root
|
|
112
|
+
match.root = Pyjoyment.Routes.new()
|
|
113
|
+
|
|
114
|
+
The root of the route structure, usually a :mod:`Pyjoyment.Routes`
|
|
115
|
+
object.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
self.stack: list[dict[str, Any]] = stack if stack is not None else []
|
|
119
|
+
"""::
|
|
120
|
+
|
|
121
|
+
stack = match.stack
|
|
122
|
+
match.stack = [{'action': 'foo'}, {'action': 'bar'}]
|
|
123
|
+
|
|
124
|
+
Captured parameters with nesting history.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
self._captures: dict[str, Any] = {}
|
|
128
|
+
self._conditions: dict[str, Any] | None = None
|
|
129
|
+
|
|
130
|
+
def find(self, c: 'Pyjoyment.Controller.object', *, method: str, path: str, websocket: bool = False) -> bool:
|
|
131
|
+
"""::
|
|
132
|
+
|
|
133
|
+
match.find(Pyjoyment.Controller.new(), method='GET', path='/')
|
|
134
|
+
|
|
135
|
+
Match controller and options against :attr:`root` to find an
|
|
136
|
+
appropriate :attr:`endpoint`.
|
|
137
|
+
"""
|
|
138
|
+
return self._match(self.root, c, method, path, websocket)
|
|
139
|
+
|
|
140
|
+
def path_for(self, *args: Any, **values: Any) -> dict[str, Any]:
|
|
141
|
+
"""::
|
|
142
|
+
|
|
143
|
+
info = match.path_for()
|
|
144
|
+
info = match.path_for(foo='bar')
|
|
145
|
+
info = match.path_for({'foo': 'bar'})
|
|
146
|
+
info = match.path_for('named')
|
|
147
|
+
info = match.path_for('named', foo='bar')
|
|
148
|
+
info = match.path_for('named', {'foo': 'bar'})
|
|
149
|
+
|
|
150
|
+
Render matching route with parameters into path.
|
|
151
|
+
"""
|
|
152
|
+
name, values = Pyjo.Util._options(args, values) # pyright: ignore[reportPrivateUsage]
|
|
153
|
+
|
|
154
|
+
# Current route
|
|
155
|
+
current = not name or name == 'current'
|
|
156
|
+
if current:
|
|
157
|
+
if (route := self.endpoint) is None:
|
|
158
|
+
return {}
|
|
159
|
+
|
|
160
|
+
# Find endpoint
|
|
161
|
+
elif (route := self.root.lookup(name)) is None:
|
|
162
|
+
return {'path': name}
|
|
163
|
+
|
|
164
|
+
# Merge values (clear format)
|
|
165
|
+
captures = self.stack[-1] if self.stack else {}
|
|
166
|
+
merged = {**captures, 'format': None, **values}
|
|
167
|
+
pattern = route.pattern
|
|
168
|
+
constraints = pattern.constraints
|
|
169
|
+
if 'format' not in values and (format := constraints.get('format')) and format is not True:
|
|
170
|
+
value = captures.get('format') if current else None
|
|
171
|
+
merged['format'] = value if value is not None else pattern.defaults.get('format')
|
|
172
|
+
|
|
173
|
+
return {'path': route.render(merged), 'websocket': route.has_websocket}
|
|
174
|
+
|
|
175
|
+
def _match(
|
|
176
|
+
self,
|
|
177
|
+
r: Pyjoyment.Routes.Route.object,
|
|
178
|
+
c: 'Pyjoyment.Controller.object',
|
|
179
|
+
method: str,
|
|
180
|
+
path: str,
|
|
181
|
+
websocket: bool,
|
|
182
|
+
) -> bool:
|
|
183
|
+
# Pattern
|
|
184
|
+
partial = r.partial
|
|
185
|
+
detect = (endpoint := r.is_endpoint) and not partial
|
|
186
|
+
if (result := r.pattern.match_partial(path, detect)) is None:
|
|
187
|
+
return False
|
|
188
|
+
found, path = result
|
|
189
|
+
|
|
190
|
+
# The captures of this route are set until it returns, as Mojo does with local
|
|
191
|
+
captures = self._captures
|
|
192
|
+
saved = [(key, captures.get(key, _MISSING)) for key in found]
|
|
193
|
+
captures.update(found)
|
|
194
|
+
try:
|
|
195
|
+
# Method
|
|
196
|
+
if (methods := r.methods()) and method not in methods:
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
# Conditions
|
|
200
|
+
if over := r.requires():
|
|
201
|
+
if (conditions := self._conditions) is None:
|
|
202
|
+
conditions = self._conditions = self.root.conditions
|
|
203
|
+
for name, value in over:
|
|
204
|
+
if not (condition := conditions.get(name)):
|
|
205
|
+
return False
|
|
206
|
+
if not condition(r, c, captures, value):
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
# WebSocket
|
|
210
|
+
if r.is_websocket and not websocket:
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
# Partial
|
|
214
|
+
empty = not path or path == '/'
|
|
215
|
+
if partial:
|
|
216
|
+
captures['path'] = path
|
|
217
|
+
self.endpoint = r
|
|
218
|
+
empty = True
|
|
219
|
+
|
|
220
|
+
# Endpoint (or intermediate destination)
|
|
221
|
+
if (endpoint and empty) or r.inline:
|
|
222
|
+
self.stack.append({**captures})
|
|
223
|
+
if endpoint and empty:
|
|
224
|
+
if format := captures.get('format'):
|
|
225
|
+
for frame in self.stack:
|
|
226
|
+
frame['format'] = format
|
|
227
|
+
self.endpoint = r
|
|
228
|
+
return True
|
|
229
|
+
captures.pop('app', None)
|
|
230
|
+
captures.pop('cb', None)
|
|
231
|
+
|
|
232
|
+
# Match children
|
|
233
|
+
snapshot = list(self.stack) if r.parent is not None else []
|
|
234
|
+
for child in r._candidates(path): # pyright: ignore[reportPrivateUsage]
|
|
235
|
+
if self._match(child, c, method, path, websocket):
|
|
236
|
+
return True
|
|
237
|
+
self.stack = list(snapshot)
|
|
238
|
+
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
finally:
|
|
242
|
+
for key, value in saved:
|
|
243
|
+
if value is _MISSING:
|
|
244
|
+
captures.pop(key, None)
|
|
245
|
+
else:
|
|
246
|
+
captures[key] = value
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
new = Pyjoyment_Routes_Match.new
|
|
250
|
+
object = Pyjoyment_Routes_Match
|
|
251
|
+
Match = Pyjoyment_Routes_Match
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjoyment.Routes.Pattern - Route pattern
|
|
3
|
+
========================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjoyment.Routes.Pattern import Pattern
|
|
7
|
+
|
|
8
|
+
# Create pattern
|
|
9
|
+
pattern = Pattern('/test/:name')
|
|
10
|
+
|
|
11
|
+
# Or import the module, as in Google Python Style Guide
|
|
12
|
+
|
|
13
|
+
import Pyjoyment.Routes.Pattern
|
|
14
|
+
|
|
15
|
+
# Create pattern
|
|
16
|
+
pattern = Pyjoyment.Routes.Pattern.new('/test/:name')
|
|
17
|
+
|
|
18
|
+
# Match routes
|
|
19
|
+
captures = pattern.match('/test/sebastian')
|
|
20
|
+
print(captures['name'])
|
|
21
|
+
|
|
22
|
+
:mod:`Pyjoyment.Routes.Pattern` is the core of :mod:`Pyjoyment.Routes`.
|
|
23
|
+
|
|
24
|
+
See also
|
|
25
|
+
--------
|
|
26
|
+
|
|
27
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
28
|
+
|
|
29
|
+
Ported from `Mojolicious::Routes::Pattern
|
|
30
|
+
<https://github.com/mojolicious/mojo/blob/v9.52/lib/Mojolicious/Routes/Pattern.pm>`_ of Mojolicious 9.52.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import Pyjo.Base
|
|
34
|
+
|
|
35
|
+
import re
|
|
36
|
+
|
|
37
|
+
from typing import Any, Self
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# A constraint is a regular expression, as a string or compiled, a list of alternatives, or for the format True to
|
|
41
|
+
# detect any and False to detect none
|
|
42
|
+
type Constraint = str | re.Pattern[str] | list[str] | bool
|
|
43
|
+
|
|
44
|
+
# A token of the tree is a list of its kind and its values, which the tokenizer changes as it reads the characters:
|
|
45
|
+
# ['text', '/foo'], ['slash'] or ['placeholder', [name, type], 'relaxed' | 'wildcard']
|
|
46
|
+
type Token = list[Any]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Pyjoyment_Routes_Pattern(Pyjo.Base.object):
|
|
50
|
+
"""
|
|
51
|
+
:mod:`Pyjoyment.Routes.Pattern` inherits all methods from :mod:`Pyjo.Base`
|
|
52
|
+
and implements the following new ones.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, pattern: str | None = None, /, **constraints: Constraint) -> None:
|
|
56
|
+
"""::
|
|
57
|
+
|
|
58
|
+
pattern = Pyjoyment.Routes.Pattern.new()
|
|
59
|
+
pattern = Pyjoyment.Routes.Pattern.new('/users/:id')
|
|
60
|
+
pattern = Pyjoyment.Routes.Pattern.new('/user/:id', id=re.compile(r'\\d+'))
|
|
61
|
+
pattern = Pyjoyment.Routes.Pattern.new(format=['json', 'yaml'])
|
|
62
|
+
|
|
63
|
+
Construct a new :mod:`Pyjoyment.Routes.Pattern` object and
|
|
64
|
+
:meth:`parse` pattern if necessary.
|
|
65
|
+
"""
|
|
66
|
+
self.constraints: dict[str, Constraint] = {}
|
|
67
|
+
"""::
|
|
68
|
+
|
|
69
|
+
constraints = pattern.constraints
|
|
70
|
+
pattern.constraints = {'foo': re.compile(r'\\w+')}
|
|
71
|
+
|
|
72
|
+
Regular expression constraints.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
self.defaults: dict[str, Any] = {}
|
|
76
|
+
"""::
|
|
77
|
+
|
|
78
|
+
defaults = pattern.defaults
|
|
79
|
+
pattern.defaults = {'foo': 'bar'}
|
|
80
|
+
|
|
81
|
+
Default parameters.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
self.placeholder_start = ':'
|
|
85
|
+
"""::
|
|
86
|
+
|
|
87
|
+
start = pattern.placeholder_start
|
|
88
|
+
pattern.placeholder_start = ':'
|
|
89
|
+
|
|
90
|
+
Character indicating a placeholder, defaults to ``:``.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
self.placeholders: list[str] = []
|
|
94
|
+
"""::
|
|
95
|
+
|
|
96
|
+
placeholders = pattern.placeholders
|
|
97
|
+
pattern.placeholders = ['foo', 'bar']
|
|
98
|
+
|
|
99
|
+
Placeholder names.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
self.quote_end = '>'
|
|
103
|
+
"""::
|
|
104
|
+
|
|
105
|
+
end = pattern.quote_end
|
|
106
|
+
pattern.quote_end = '}'
|
|
107
|
+
|
|
108
|
+
Character indicating the end of a quoted placeholder, defaults to ``>``.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
self.quote_start = '<'
|
|
112
|
+
"""::
|
|
113
|
+
|
|
114
|
+
start = pattern.quote_start
|
|
115
|
+
pattern.quote_start = '{'
|
|
116
|
+
|
|
117
|
+
Character indicating the start of a quoted placeholder, defaults to
|
|
118
|
+
``<``.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
self.regex: re.Pattern[str] | None = None
|
|
122
|
+
"""::
|
|
123
|
+
|
|
124
|
+
regex = pattern.regex
|
|
125
|
+
pattern.regex = regex
|
|
126
|
+
|
|
127
|
+
Pattern in compiled regular expression form.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
self.relaxed_start = '#'
|
|
131
|
+
"""::
|
|
132
|
+
|
|
133
|
+
start = pattern.relaxed_start
|
|
134
|
+
pattern.relaxed_start = '*'
|
|
135
|
+
|
|
136
|
+
Character indicating a relaxed placeholder, defaults to ``#``.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
self.tree: list[Token] = []
|
|
140
|
+
"""::
|
|
141
|
+
|
|
142
|
+
tree = pattern.tree
|
|
143
|
+
pattern.tree = [['text', '/foo']]
|
|
144
|
+
|
|
145
|
+
Pattern in parsed form. Note that this structure should only be used
|
|
146
|
+
very carefully since it is very dynamic.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
self.type_start = ':'
|
|
150
|
+
"""::
|
|
151
|
+
|
|
152
|
+
start = pattern.type_start
|
|
153
|
+
pattern.type_start = '|'
|
|
154
|
+
|
|
155
|
+
Character indicating the start of a placeholder type, defaults to ``:``.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
self.types: dict[str, Constraint] = {}
|
|
159
|
+
"""::
|
|
160
|
+
|
|
161
|
+
types = pattern.types
|
|
162
|
+
pattern.types = {'int': re.compile(r'[0-9]+')}
|
|
163
|
+
|
|
164
|
+
Placeholder types.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
self.unparsed: str | None = None
|
|
168
|
+
"""::
|
|
169
|
+
|
|
170
|
+
unparsed = pattern.unparsed
|
|
171
|
+
pattern.unparsed = '/:foo/:bar'
|
|
172
|
+
|
|
173
|
+
Raw unparsed pattern.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
self.wildcard_start = '*'
|
|
177
|
+
"""::
|
|
178
|
+
|
|
179
|
+
start = pattern.wildcard_start
|
|
180
|
+
pattern.wildcard_start = '*'
|
|
181
|
+
|
|
182
|
+
Character indicating the start of a wildcard placeholder, defaults to
|
|
183
|
+
``*``.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
# The regex that _compile made, which tells a custom one apart
|
|
187
|
+
self._compiled: re.Pattern[str] | None = None
|
|
188
|
+
|
|
189
|
+
if pattern is not None or constraints:
|
|
190
|
+
self.parse(pattern, **constraints)
|
|
191
|
+
|
|
192
|
+
def match(self, path: str, detect: bool = False) -> dict[str, Any] | None:
|
|
193
|
+
"""::
|
|
194
|
+
|
|
195
|
+
captures = pattern.match('/foo/bar')
|
|
196
|
+
captures = pattern.match('/foo/bar', True)
|
|
197
|
+
|
|
198
|
+
Match pattern against entire path, format detection is disabled by
|
|
199
|
+
default.
|
|
200
|
+
"""
|
|
201
|
+
if (result := self.match_partial(path, detect)) is None:
|
|
202
|
+
return None
|
|
203
|
+
captures, path = result
|
|
204
|
+
# Unlike Mojolicious: a rest of "0" is no match, which Mojo's !$path takes for an empty one, as 0 is false
|
|
205
|
+
return captures if not path or path == '/' else None
|
|
206
|
+
|
|
207
|
+
def match_partial(self, path: str, detect: bool = False) -> tuple[dict[str, Any], str] | None:
|
|
208
|
+
"""::
|
|
209
|
+
|
|
210
|
+
captures, path = pattern.match_partial(path)
|
|
211
|
+
captures, path = pattern.match_partial(path, True)
|
|
212
|
+
|
|
213
|
+
Match pattern against path and return the captures with the rest of the
|
|
214
|
+
path, without the matching parts, format detection is disabled by
|
|
215
|
+
default.
|
|
216
|
+
"""
|
|
217
|
+
# Compile on demand
|
|
218
|
+
if (regex := self.regex) is None:
|
|
219
|
+
regex = self._compile(detect)
|
|
220
|
+
|
|
221
|
+
if (m := regex.match(path)) is None:
|
|
222
|
+
return None
|
|
223
|
+
groups = list(m.groups())
|
|
224
|
+
captures = {**self.defaults}
|
|
225
|
+
for placeholder in [*self.placeholders, 'format']:
|
|
226
|
+
if not groups:
|
|
227
|
+
break
|
|
228
|
+
if (capture := groups.pop(0)) is not None:
|
|
229
|
+
captures[placeholder] = capture
|
|
230
|
+
|
|
231
|
+
return captures, path[m.end() :]
|
|
232
|
+
|
|
233
|
+
def parse(self, pattern: str | None = None, /, **constraints: Constraint) -> Self:
|
|
234
|
+
"""::
|
|
235
|
+
|
|
236
|
+
pattern = pattern.parse('/user/:id')
|
|
237
|
+
pattern = pattern.parse('/user/:id', id=re.compile(r'\\d+'))
|
|
238
|
+
pattern = pattern.parse(format=['json', 'yaml'])
|
|
239
|
+
|
|
240
|
+
Parse pattern.
|
|
241
|
+
"""
|
|
242
|
+
pattern = re.sub(r'^/*|/+', '/', pattern if pattern is not None else '/')
|
|
243
|
+
self.constraints = constraints
|
|
244
|
+
if pattern == '/':
|
|
245
|
+
return self
|
|
246
|
+
|
|
247
|
+
return self._tokenize(re.sub(r'/$', '', pattern, count=1))
|
|
248
|
+
|
|
249
|
+
def render(self, values: dict[str, Any] | None = None, endpoint: bool = False) -> str:
|
|
250
|
+
"""::
|
|
251
|
+
|
|
252
|
+
path = pattern.render({'id': 24})
|
|
253
|
+
path = pattern.render({'id': 24}, True)
|
|
254
|
+
|
|
255
|
+
Render pattern into a path with parameters, format rendering is
|
|
256
|
+
disabled by default.
|
|
257
|
+
"""
|
|
258
|
+
values = values if values is not None else {}
|
|
259
|
+
|
|
260
|
+
# Placeholders can only be optional without a format
|
|
261
|
+
optional = not (format := values.get('format'))
|
|
262
|
+
|
|
263
|
+
parts: list[str] = []
|
|
264
|
+
for token in reversed(self.tree):
|
|
265
|
+
op = token[0]
|
|
266
|
+
part = ''
|
|
267
|
+
|
|
268
|
+
# Text
|
|
269
|
+
if op == 'text':
|
|
270
|
+
part, optional = token[1], False
|
|
271
|
+
|
|
272
|
+
# Slash
|
|
273
|
+
elif op == 'slash':
|
|
274
|
+
if not optional:
|
|
275
|
+
part = '/'
|
|
276
|
+
|
|
277
|
+
# Placeholder
|
|
278
|
+
else:
|
|
279
|
+
name = token[1][0]
|
|
280
|
+
default = self.defaults.get(name)
|
|
281
|
+
value = values.get(name)
|
|
282
|
+
part = str(value if value is not None else default if default is not None else '')
|
|
283
|
+
if default is None or str(default) != part:
|
|
284
|
+
optional = False
|
|
285
|
+
elif optional:
|
|
286
|
+
part = ''
|
|
287
|
+
|
|
288
|
+
parts.append(part)
|
|
289
|
+
|
|
290
|
+
# Format can be optional
|
|
291
|
+
path = ''.join(reversed(parts))
|
|
292
|
+
return f'{path}.{format}' if endpoint and format else path
|
|
293
|
+
|
|
294
|
+
def _compile(self, detect: bool) -> re.Pattern[str]:
|
|
295
|
+
constraints = self.constraints
|
|
296
|
+
defaults = self.defaults
|
|
297
|
+
types = self.types
|
|
298
|
+
|
|
299
|
+
block = regex = ''
|
|
300
|
+
optional = True
|
|
301
|
+
for token in reversed(self.tree):
|
|
302
|
+
op = token[0]
|
|
303
|
+
part = ''
|
|
304
|
+
|
|
305
|
+
# Text
|
|
306
|
+
if op == 'text':
|
|
307
|
+
part, optional = re.escape(token[1]), False
|
|
308
|
+
|
|
309
|
+
# Slash
|
|
310
|
+
elif op == 'slash':
|
|
311
|
+
regex = (f'(?:/{block})?' if optional else f'/{block}') + regex
|
|
312
|
+
block, optional = '', True
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
# Placeholder
|
|
316
|
+
else:
|
|
317
|
+
value: list[str] = token[1]
|
|
318
|
+
if len(value) > 1:
|
|
319
|
+
part = _compile_req(types.get(value[1], '?!'))
|
|
320
|
+
else:
|
|
321
|
+
type = token[2] if len(token) > 2 else None
|
|
322
|
+
part = ('([^/]+)' if type == 'relaxed' else '(.+)') if type else '([^/.]+)'
|
|
323
|
+
|
|
324
|
+
# Custom regex
|
|
325
|
+
if c := constraints.get(value[0]):
|
|
326
|
+
part = _compile_req(c)
|
|
327
|
+
|
|
328
|
+
# Optional placeholder
|
|
329
|
+
if value[0] in defaults:
|
|
330
|
+
part += '?'
|
|
331
|
+
else:
|
|
332
|
+
optional = False
|
|
333
|
+
|
|
334
|
+
block = part + block
|
|
335
|
+
|
|
336
|
+
# Not rooted with a slash
|
|
337
|
+
if block:
|
|
338
|
+
regex = block + regex
|
|
339
|
+
|
|
340
|
+
# Format
|
|
341
|
+
if detect:
|
|
342
|
+
regex += _compile_format(constraints.get('format'), 'format' in defaults)
|
|
343
|
+
|
|
344
|
+
self.regex = self._compiled = re.compile(f'^{regex}', re.DOTALL)
|
|
345
|
+
return self.regex
|
|
346
|
+
|
|
347
|
+
def _tokenize(self, pattern: str) -> Self:
|
|
348
|
+
placeholders = self.placeholders
|
|
349
|
+
type_start = self.type_start
|
|
350
|
+
quote_end = self.quote_end
|
|
351
|
+
quote_start = self.quote_start
|
|
352
|
+
start = self.placeholder_start
|
|
353
|
+
relaxed = self.relaxed_start
|
|
354
|
+
wildcard = self.wildcard_start
|
|
355
|
+
|
|
356
|
+
tree: list[Token] = []
|
|
357
|
+
spec = more = 0
|
|
358
|
+
for char in pattern:
|
|
359
|
+
# Quoted
|
|
360
|
+
if char == quote_start:
|
|
361
|
+
spec += 1
|
|
362
|
+
tree.append(['placeholder', ''])
|
|
363
|
+
elif char == quote_end:
|
|
364
|
+
spec = more = 0
|
|
365
|
+
|
|
366
|
+
# Placeholder
|
|
367
|
+
elif not more and char == start:
|
|
368
|
+
if not spec:
|
|
369
|
+
tree.append(['placeholder', ''])
|
|
370
|
+
spec += 1
|
|
371
|
+
|
|
372
|
+
# Relaxed or wildcard (upgrade when quoted)
|
|
373
|
+
elif not more and char in (relaxed, wildcard):
|
|
374
|
+
if not spec:
|
|
375
|
+
tree.append(['placeholder', ''])
|
|
376
|
+
spec += 1
|
|
377
|
+
tree[-1][2:] = ['relaxed' if char == relaxed else 'wildcard']
|
|
378
|
+
|
|
379
|
+
# Slash
|
|
380
|
+
elif char == '/':
|
|
381
|
+
tree.append(['slash'])
|
|
382
|
+
spec = more = 0
|
|
383
|
+
|
|
384
|
+
# Placeholder
|
|
385
|
+
elif spec:
|
|
386
|
+
more += 1
|
|
387
|
+
tree[-1][1] += char
|
|
388
|
+
|
|
389
|
+
# Text (optimize slash+text and *+text+slash+text)
|
|
390
|
+
elif tree and tree[-1][0] == 'text':
|
|
391
|
+
tree[-1][-1] += char
|
|
392
|
+
elif len(tree) == 1 and tree[-1][0] == 'slash':
|
|
393
|
+
tree = [['text', f'/{char}']]
|
|
394
|
+
elif len(tree) > 1 and tree[-2][0] == 'text' and tree[-1][0] == 'slash':
|
|
395
|
+
tree.pop()
|
|
396
|
+
tree[-1][-1] += f'/{char}'
|
|
397
|
+
else:
|
|
398
|
+
tree.append(['text', char])
|
|
399
|
+
|
|
400
|
+
# Placeholder types
|
|
401
|
+
for token in reversed(tree):
|
|
402
|
+
if token[0] != 'placeholder':
|
|
403
|
+
continue
|
|
404
|
+
m = re.match(rf'^(.+){re.escape(type_start)}(.+)$', token[1])
|
|
405
|
+
token[1] = [m[1], m[2]] if m else [token[1]]
|
|
406
|
+
placeholders.insert(0, token[1][0])
|
|
407
|
+
|
|
408
|
+
self.unparsed = pattern
|
|
409
|
+
self.tree = tree
|
|
410
|
+
return self
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _compile_format(format: Constraint | None, has_default: bool) -> str:
|
|
414
|
+
# No regex
|
|
415
|
+
if not format:
|
|
416
|
+
return ''
|
|
417
|
+
|
|
418
|
+
# Default regex
|
|
419
|
+
if format is True:
|
|
420
|
+
return r'/?(?:\.([^/]+))?$'
|
|
421
|
+
|
|
422
|
+
# Compile custom regex
|
|
423
|
+
regex = r'\.' + _compile_req(format)
|
|
424
|
+
return f'/?(?:{regex})?$' if has_default else f'/?{regex}$'
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _compile_req(req: Constraint) -> str:
|
|
428
|
+
if isinstance(req, list):
|
|
429
|
+
return '(' + '|'.join(re.escape(alternative) for alternative in sorted(map(str, req), reverse=True)) + ')'
|
|
430
|
+
if isinstance(req, re.Pattern):
|
|
431
|
+
return f'({_regex_source(req)})'
|
|
432
|
+
return f'({req})'
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# The source of a compiled regular expression with its flags, as Perl stringifies a qr// with (?^flags:...)
|
|
436
|
+
def _regex_source(regex: re.Pattern[str]) -> str:
|
|
437
|
+
flags = ''.join(flag for flag, value in (('i', re.I), ('m', re.M), ('s', re.S), ('x', re.X)) if regex.flags & value)
|
|
438
|
+
return f'(?{flags}:{regex.pattern})' if flags else regex.pattern
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
new = Pyjoyment_Routes_Pattern.new
|
|
442
|
+
object = Pyjoyment_Routes_Pattern
|
|
443
|
+
Pattern = Pyjoyment_Routes_Pattern
|