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,827 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjoyment.Routes.Route - Route
|
|
3
|
+
==============================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjoyment.Routes.Route import Route
|
|
7
|
+
|
|
8
|
+
r = Route()
|
|
9
|
+
|
|
10
|
+
# Or import the module, as in Google Python Style Guide
|
|
11
|
+
|
|
12
|
+
import Pyjoyment.Routes.Route
|
|
13
|
+
|
|
14
|
+
r = Pyjoyment.Routes.Route.new()
|
|
15
|
+
|
|
16
|
+
:mod:`Pyjoyment.Routes.Route` is the route container used by
|
|
17
|
+
:mod:`Pyjoyment.Routes`.
|
|
18
|
+
|
|
19
|
+
Shortcuts
|
|
20
|
+
---------
|
|
21
|
+
|
|
22
|
+
In addition to the attributes and methods below you can also call shortcuts
|
|
23
|
+
provided by :attr:`~Pyjoyment_Routes_Route.root` on
|
|
24
|
+
:mod:`Pyjoyment.Routes.Route` objects. ::
|
|
25
|
+
|
|
26
|
+
# Add a "firefox" shortcut
|
|
27
|
+
r.root.add_shortcut('firefox', lambda r, path: r.get(path).requires(agent=re.compile(r'Firefox')))
|
|
28
|
+
|
|
29
|
+
# Use "firefox" shortcut to generate routes
|
|
30
|
+
r.firefox('/welcome').to('firefox#welcome')
|
|
31
|
+
r.firefox('/bye').to('firefox#bye')
|
|
32
|
+
|
|
33
|
+
See also
|
|
34
|
+
--------
|
|
35
|
+
|
|
36
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
37
|
+
|
|
38
|
+
Ported from `Mojolicious::Routes::Route
|
|
39
|
+
<https://github.com/mojolicious/mojo/blob/v9.52/lib/Mojolicious/Routes/Route.pm>`_ of Mojolicious 9.52.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
import Pyjo.Base
|
|
43
|
+
import Pyjo.Exception
|
|
44
|
+
import Pyjo.Util
|
|
45
|
+
import Pyjoyment.Routes.Pattern
|
|
46
|
+
|
|
47
|
+
import functools
|
|
48
|
+
import re
|
|
49
|
+
import sys
|
|
50
|
+
import weakref
|
|
51
|
+
|
|
52
|
+
from collections.abc import Callable
|
|
53
|
+
from typing import Any, Self, cast, overload
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Reserved stash values
|
|
57
|
+
# Not in Mojolicious: toml and yaml, which the renderer renders as json
|
|
58
|
+
_RESERVED = frozenset(
|
|
59
|
+
'action app cb controller data extends format handler inline json layout namespace path status template text '
|
|
60
|
+
'toml variant yaml'.split()
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# The children without a text that their paths start with, and the ones with it, by its length and by the text
|
|
65
|
+
type _PrefixIndex = tuple[list[int], dict[int, dict[str, list[int]]]]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Pyjoyment_Routes_Route(Pyjo.Base.object):
|
|
69
|
+
"""
|
|
70
|
+
:mod:`Pyjoyment.Routes.Route` inherits all methods from :mod:`Pyjo.Base`
|
|
71
|
+
and implements the following new ones.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self) -> None:
|
|
75
|
+
"""::
|
|
76
|
+
|
|
77
|
+
r = Pyjoyment.Routes.Route.new()
|
|
78
|
+
|
|
79
|
+
Construct a new :mod:`Pyjoyment.Routes.Route` object.
|
|
80
|
+
"""
|
|
81
|
+
self.children: list[Pyjoyment_Routes_Route] = []
|
|
82
|
+
"""::
|
|
83
|
+
|
|
84
|
+
children = r.children
|
|
85
|
+
r.children = [Pyjoyment.Routes.Route.new()]
|
|
86
|
+
|
|
87
|
+
The children of this route, used for nesting routes.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
self.inline: bool | None = None
|
|
91
|
+
"""::
|
|
92
|
+
|
|
93
|
+
boolean = r.inline
|
|
94
|
+
r.inline = boolean
|
|
95
|
+
|
|
96
|
+
Allow :meth:`under` semantics for this route.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
self.partial: bool | None = None
|
|
100
|
+
"""::
|
|
101
|
+
|
|
102
|
+
boolean = r.partial
|
|
103
|
+
r.partial = boolean
|
|
104
|
+
|
|
105
|
+
Route has no specific end, remaining characters will be captured in
|
|
106
|
+
``path``.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
self.pattern: Pyjoyment.Routes.Pattern.object = Pyjoyment.Routes.Pattern.new()
|
|
110
|
+
"""::
|
|
111
|
+
|
|
112
|
+
pattern = r.pattern
|
|
113
|
+
r.pattern = Pyjoyment.Routes.Pattern.new()
|
|
114
|
+
|
|
115
|
+
Pattern for this route, defaults to a :mod:`Pyjoyment.Routes.Pattern`
|
|
116
|
+
object.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
self._custom = False
|
|
120
|
+
self._has_websocket: bool | None = None
|
|
121
|
+
self._methods: list[str] | None = None
|
|
122
|
+
self._name: str | None = None
|
|
123
|
+
self._parent: weakref.ref[Pyjoyment_Routes_Route] | None = None
|
|
124
|
+
self._prefix_index: _PrefixIndex | None = None
|
|
125
|
+
self._requires: list[tuple[str, Any]] | None = None
|
|
126
|
+
self._websocket = False
|
|
127
|
+
|
|
128
|
+
# Not in Mojolicious: a route is a decorator of its callback, as the functions of Pyjoyment.Lite and the methods of
|
|
129
|
+
# the routes are used without one
|
|
130
|
+
def __call__(self, cb: Callable[..., Any]) -> Self:
|
|
131
|
+
"""::
|
|
132
|
+
|
|
133
|
+
@r.get('/foo')
|
|
134
|
+
def foo(c):
|
|
135
|
+
c.render(text='Hello World!')
|
|
136
|
+
|
|
137
|
+
Set the callback of the route, as a decorator, and return the route.
|
|
138
|
+
"""
|
|
139
|
+
return self.to(cb=cb)
|
|
140
|
+
|
|
141
|
+
# Unlike Mojolicious: Mojo registers the shortcuts of a root with Mojo::DynamicMethods, Python looks them up in the
|
|
142
|
+
# root when a method is not found
|
|
143
|
+
def __getattr__(self, name: str) -> Any:
|
|
144
|
+
if not name.startswith('_'):
|
|
145
|
+
shortcuts: dict[str, Callable[..., Any]] = vars(self._chain()[0]).get('shortcuts', {})
|
|
146
|
+
if dynamic := shortcuts.get(name):
|
|
147
|
+
return functools.partial(dynamic, self)
|
|
148
|
+
package = f'{type(self).__module__}.{type(self).__qualname__}'
|
|
149
|
+
|
|
150
|
+
# The location of the caller, which croak adds to the message
|
|
151
|
+
caller = sys._getframe(1) # pyright: ignore[reportPrivateUsage]
|
|
152
|
+
at = f'at {caller.f_code.co_filename} line {caller.f_lineno}'
|
|
153
|
+
raise AttributeError(
|
|
154
|
+
f'Can\'t locate object method "{name}" via package "{package}" {at}.\n', name=name, obj=self
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def parent(self) -> 'Pyjoyment_Routes_Route | None':
|
|
159
|
+
"""::
|
|
160
|
+
|
|
161
|
+
parent = r.parent
|
|
162
|
+
r.parent = Pyjoyment.Routes.Route.new()
|
|
163
|
+
|
|
164
|
+
The parent of this route, usually a :mod:`Pyjoyment.Routes.Route`
|
|
165
|
+
object. Note that this attribute is weakened.
|
|
166
|
+
"""
|
|
167
|
+
return self._parent() if self._parent is not None else None
|
|
168
|
+
|
|
169
|
+
@parent.setter
|
|
170
|
+
def parent(self, value: 'Pyjoyment_Routes_Route | None') -> None:
|
|
171
|
+
self._parent = weakref.ref(value) if value is not None else None
|
|
172
|
+
|
|
173
|
+
def add_child(self, route: 'Pyjoyment_Routes_Route') -> Self:
|
|
174
|
+
"""::
|
|
175
|
+
|
|
176
|
+
r = r.add_child(Pyjoyment.Routes.Route.new())
|
|
177
|
+
|
|
178
|
+
Add a child to this route, it will be automatically removed from its
|
|
179
|
+
current parent if necessary. ::
|
|
180
|
+
|
|
181
|
+
# Reattach route
|
|
182
|
+
r.add_child(r.find('foo'))
|
|
183
|
+
"""
|
|
184
|
+
_invalidate(self)
|
|
185
|
+
self.children.append(route.remove().set(parent=self))
|
|
186
|
+
route.pattern.types = self.root.types
|
|
187
|
+
return self
|
|
188
|
+
|
|
189
|
+
def any(self, *args: Any, **constraints: 'Pyjoyment.Routes.Pattern.Constraint') -> 'Pyjoyment_Routes_Route':
|
|
190
|
+
"""::
|
|
191
|
+
|
|
192
|
+
route = r.any()
|
|
193
|
+
route = r.any('/:foo')
|
|
194
|
+
route = r.any('/:foo', lambda c: ...)
|
|
195
|
+
route = r.any('/:foo', lambda c: ..., 'name')
|
|
196
|
+
route = r.any('/:foo', {'foo': 'bar'}, lambda c: ...)
|
|
197
|
+
route = r.any('/:foo', lambda c: ..., foo=re.compile(r'\\w+'))
|
|
198
|
+
route = r.any(['GET', 'POST'], '/:foo', lambda c: ...)
|
|
199
|
+
route = r.any(['GET', 'POST'], '/:foo', foo=re.compile(r'\\w+'))
|
|
200
|
+
|
|
201
|
+
Generate :mod:`Pyjoyment.Routes.Route` object matching any of the listed
|
|
202
|
+
HTTP request methods or all. ::
|
|
203
|
+
|
|
204
|
+
# Route with pattern and destination
|
|
205
|
+
r.any('/user').to('user#whatever')
|
|
206
|
+
|
|
207
|
+
All arguments are optional, but some have to appear in a certain order,
|
|
208
|
+
like the list of the HTTP methods to match, which comes first, and the
|
|
209
|
+
restrictive placeholders, which are keyword arguments. ::
|
|
210
|
+
|
|
211
|
+
# Route with HTTP methods, pattern, restrictive placeholders and destination
|
|
212
|
+
r.any(['DELETE', 'PUT'], '/:foo', foo=re.compile(r'\\w+')).to('foo#bar')
|
|
213
|
+
|
|
214
|
+
There are also two supported string values, containing the route
|
|
215
|
+
pattern and the route name, defaulting to the pattern ``/`` and a name
|
|
216
|
+
based on the pattern. ::
|
|
217
|
+
|
|
218
|
+
# Route with pattern, name and destination
|
|
219
|
+
r.any('/:foo', 'foo_route').to('foo#bar')
|
|
220
|
+
|
|
221
|
+
Route conditions are activated with :meth:`requires`. ::
|
|
222
|
+
|
|
223
|
+
# Route with pattern, condition and destination
|
|
224
|
+
r.any('/').requires(agent=re.compile(r'Firefox')).to('foo#bar')
|
|
225
|
+
|
|
226
|
+
A dict is used to specify optional placeholders and default values for
|
|
227
|
+
the stash. ::
|
|
228
|
+
|
|
229
|
+
# Route with pattern, optional placeholder and destination
|
|
230
|
+
r.any('/:foo', {'foo': 'bar'}).to('foo#bar')
|
|
231
|
+
|
|
232
|
+
And a callable can be used to specify a ``cb`` value to be merged into
|
|
233
|
+
the default values for the stash. ::
|
|
234
|
+
|
|
235
|
+
# Route with pattern and a closure as destination
|
|
236
|
+
r.any('/:foo', lambda c: c.render(text='Hello World!'))
|
|
237
|
+
"""
|
|
238
|
+
if args and isinstance(methods := args[0], list):
|
|
239
|
+
return self._generate_route(cast(list[str], methods), *args[1:], **constraints)
|
|
240
|
+
return self._generate_route([], *args, **constraints)
|
|
241
|
+
|
|
242
|
+
def delete(self, *args: Any, **constraints: 'Pyjoyment.Routes.Pattern.Constraint') -> 'Pyjoyment_Routes_Route':
|
|
243
|
+
"""::
|
|
244
|
+
|
|
245
|
+
route = r.delete()
|
|
246
|
+
route = r.delete('/:foo')
|
|
247
|
+
route = r.delete('/:foo', lambda c: ...)
|
|
248
|
+
route = r.delete('/:foo', lambda c: ..., 'name')
|
|
249
|
+
route = r.delete('/:foo', {'foo': 'bar'}, lambda c: ...)
|
|
250
|
+
route = r.delete('/:foo', lambda c: ..., foo=re.compile(r'\\w+'))
|
|
251
|
+
|
|
252
|
+
Generate :mod:`Pyjoyment.Routes.Route` object matching only ``DELETE``
|
|
253
|
+
requests, takes the same arguments as :meth:`any` (except for the HTTP
|
|
254
|
+
methods to match, which are implied). ::
|
|
255
|
+
|
|
256
|
+
# Route with destination
|
|
257
|
+
r.delete('/user').to('user#remove')
|
|
258
|
+
"""
|
|
259
|
+
return self._generate_route('DELETE', *args, **constraints)
|
|
260
|
+
|
|
261
|
+
def find(self, name: str) -> 'Pyjoyment_Routes_Route | None':
|
|
262
|
+
"""::
|
|
263
|
+
|
|
264
|
+
route = r.find('foo')
|
|
265
|
+
|
|
266
|
+
Find child route by name, custom names have precedence over
|
|
267
|
+
automatically generated ones. ::
|
|
268
|
+
|
|
269
|
+
# Change default parameters of a named route
|
|
270
|
+
r.find('show_user').to(foo='bar')
|
|
271
|
+
"""
|
|
272
|
+
return self._index().get(name)
|
|
273
|
+
|
|
274
|
+
def get(self, *args: Any, **constraints: 'Pyjoyment.Routes.Pattern.Constraint') -> 'Pyjoyment_Routes_Route':
|
|
275
|
+
"""::
|
|
276
|
+
|
|
277
|
+
route = r.get()
|
|
278
|
+
route = r.get('/:foo')
|
|
279
|
+
route = r.get('/:foo', lambda c: ...)
|
|
280
|
+
route = r.get('/:foo', lambda c: ..., 'name')
|
|
281
|
+
route = r.get('/:foo', {'foo': 'bar'}, lambda c: ...)
|
|
282
|
+
route = r.get('/:foo', lambda c: ..., foo=re.compile(r'\\w+'))
|
|
283
|
+
|
|
284
|
+
Generate :mod:`Pyjoyment.Routes.Route` object matching only ``GET``
|
|
285
|
+
requests, takes the same arguments as :meth:`any` (except for the HTTP
|
|
286
|
+
methods to match, which are implied). ::
|
|
287
|
+
|
|
288
|
+
# Route with destination
|
|
289
|
+
r.get('/user').to('user#show')
|
|
290
|
+
"""
|
|
291
|
+
return self._generate_route('GET', *args, **constraints)
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def has_custom_name(self) -> bool:
|
|
295
|
+
"""::
|
|
296
|
+
|
|
297
|
+
boolean = r.has_custom_name
|
|
298
|
+
|
|
299
|
+
Check if this route has a custom name.
|
|
300
|
+
"""
|
|
301
|
+
return self._custom
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def has_websocket(self) -> bool:
|
|
305
|
+
"""::
|
|
306
|
+
|
|
307
|
+
boolean = r.has_websocket
|
|
308
|
+
|
|
309
|
+
Check if this route has a WebSocket ancestor and cache the result for
|
|
310
|
+
future checks.
|
|
311
|
+
"""
|
|
312
|
+
if self._has_websocket is None:
|
|
313
|
+
self._has_websocket = any(route.is_websocket for route in self._chain())
|
|
314
|
+
return self._has_websocket
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def is_endpoint(self) -> bool:
|
|
318
|
+
"""::
|
|
319
|
+
|
|
320
|
+
boolean = r.is_endpoint
|
|
321
|
+
|
|
322
|
+
Check if this route qualifies as an endpoint.
|
|
323
|
+
"""
|
|
324
|
+
return not self.inline and not self.children
|
|
325
|
+
|
|
326
|
+
def is_reserved(self, name: str) -> bool:
|
|
327
|
+
"""::
|
|
328
|
+
|
|
329
|
+
boolean = r.is_reserved('controller')
|
|
330
|
+
|
|
331
|
+
Check if string is a reserved stash value.
|
|
332
|
+
"""
|
|
333
|
+
return name in _RESERVED
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def is_websocket(self) -> bool:
|
|
337
|
+
"""::
|
|
338
|
+
|
|
339
|
+
boolean = r.is_websocket
|
|
340
|
+
|
|
341
|
+
Check if this route is a WebSocket.
|
|
342
|
+
"""
|
|
343
|
+
return self._websocket
|
|
344
|
+
|
|
345
|
+
@overload
|
|
346
|
+
def methods(self) -> list[str] | None: ...
|
|
347
|
+
|
|
348
|
+
@overload
|
|
349
|
+
def methods(self, method: str | list[str], /, *methods: str) -> Self: ...
|
|
350
|
+
|
|
351
|
+
def methods(self, *methods: str | list[str]) -> list[str] | None | Self:
|
|
352
|
+
"""::
|
|
353
|
+
|
|
354
|
+
methods = r.methods()
|
|
355
|
+
r = r.methods('GET')
|
|
356
|
+
r = r.methods('GET', 'POST')
|
|
357
|
+
r = r.methods(['GET', 'POST'])
|
|
358
|
+
|
|
359
|
+
Restrict HTTP methods this route is allowed to handle, defaults to no
|
|
360
|
+
restrictions. ::
|
|
361
|
+
|
|
362
|
+
# Route with two methods and destination
|
|
363
|
+
r.any('/foo').methods('GET', 'POST').to('foo#bar')
|
|
364
|
+
"""
|
|
365
|
+
if not methods:
|
|
366
|
+
return self._methods
|
|
367
|
+
first = methods[0]
|
|
368
|
+
names = [
|
|
369
|
+
method.upper() for method in (first if isinstance(first, list) else methods) if isinstance(method, str)
|
|
370
|
+
]
|
|
371
|
+
if names:
|
|
372
|
+
self._methods = names
|
|
373
|
+
return self
|
|
374
|
+
|
|
375
|
+
@property
|
|
376
|
+
def name(self) -> str | None:
|
|
377
|
+
"""::
|
|
378
|
+
|
|
379
|
+
name = r.name
|
|
380
|
+
r.name = 'foo'
|
|
381
|
+
|
|
382
|
+
The name of this route, defaults to an automatically generated name
|
|
383
|
+
based on the route pattern. Note that the name ``current`` is reserved
|
|
384
|
+
for referring to the current route. ::
|
|
385
|
+
|
|
386
|
+
# Route with destination and custom name
|
|
387
|
+
r.get('/user').to('user#show').set(name='show_user')
|
|
388
|
+
"""
|
|
389
|
+
return self._name
|
|
390
|
+
|
|
391
|
+
@name.setter
|
|
392
|
+
def name(self, value: str) -> None:
|
|
393
|
+
self._name, self._custom = value, True
|
|
394
|
+
|
|
395
|
+
def options(self, *args: Any, **constraints: 'Pyjoyment.Routes.Pattern.Constraint') -> 'Pyjoyment_Routes_Route':
|
|
396
|
+
"""::
|
|
397
|
+
|
|
398
|
+
route = r.options()
|
|
399
|
+
route = r.options('/:foo')
|
|
400
|
+
route = r.options('/:foo', lambda c: ...)
|
|
401
|
+
route = r.options('/:foo', lambda c: ..., 'name')
|
|
402
|
+
route = r.options('/:foo', {'foo': 'bar'}, lambda c: ...)
|
|
403
|
+
route = r.options('/:foo', lambda c: ..., foo=re.compile(r'\\w+'))
|
|
404
|
+
|
|
405
|
+
Generate :mod:`Pyjoyment.Routes.Route` object matching only ``OPTIONS``
|
|
406
|
+
requests, takes the same arguments as :meth:`any` (except for the HTTP
|
|
407
|
+
methods to match, which are implied). ::
|
|
408
|
+
|
|
409
|
+
# Route with destination
|
|
410
|
+
r.options('/user').to('user#overview')
|
|
411
|
+
"""
|
|
412
|
+
return self._generate_route('OPTIONS', *args, **constraints)
|
|
413
|
+
|
|
414
|
+
def parse(self, pattern: str | None = None, /, **constraints: 'Pyjoyment.Routes.Pattern.Constraint') -> Self:
|
|
415
|
+
"""::
|
|
416
|
+
|
|
417
|
+
r = r.parse('/user/:id')
|
|
418
|
+
r = r.parse('/user/:id', id=re.compile(r'\\d+'))
|
|
419
|
+
r = r.parse(format=['json', 'yaml'])
|
|
420
|
+
|
|
421
|
+
Parse pattern.
|
|
422
|
+
"""
|
|
423
|
+
self._name = re.sub(r'\W+', '', self.pattern.parse(pattern, **constraints).unparsed or '')
|
|
424
|
+
return self
|
|
425
|
+
|
|
426
|
+
def patch(self, *args: Any, **constraints: 'Pyjoyment.Routes.Pattern.Constraint') -> 'Pyjoyment_Routes_Route':
|
|
427
|
+
"""::
|
|
428
|
+
|
|
429
|
+
route = r.patch()
|
|
430
|
+
route = r.patch('/:foo')
|
|
431
|
+
route = r.patch('/:foo', lambda c: ...)
|
|
432
|
+
route = r.patch('/:foo', lambda c: ..., 'name')
|
|
433
|
+
route = r.patch('/:foo', {'foo': 'bar'}, lambda c: ...)
|
|
434
|
+
route = r.patch('/:foo', lambda c: ..., foo=re.compile(r'\\w+'))
|
|
435
|
+
|
|
436
|
+
Generate :mod:`Pyjoyment.Routes.Route` object matching only ``PATCH``
|
|
437
|
+
requests, takes the same arguments as :meth:`any` (except for the HTTP
|
|
438
|
+
methods to match, which are implied). ::
|
|
439
|
+
|
|
440
|
+
# Route with destination
|
|
441
|
+
r.patch('/user').to('user#update')
|
|
442
|
+
"""
|
|
443
|
+
return self._generate_route('PATCH', *args, **constraints)
|
|
444
|
+
|
|
445
|
+
def post(self, *args: Any, **constraints: 'Pyjoyment.Routes.Pattern.Constraint') -> 'Pyjoyment_Routes_Route':
|
|
446
|
+
"""::
|
|
447
|
+
|
|
448
|
+
route = r.post()
|
|
449
|
+
route = r.post('/:foo')
|
|
450
|
+
route = r.post('/:foo', lambda c: ...)
|
|
451
|
+
route = r.post('/:foo', lambda c: ..., 'name')
|
|
452
|
+
route = r.post('/:foo', {'foo': 'bar'}, lambda c: ...)
|
|
453
|
+
route = r.post('/:foo', lambda c: ..., foo=re.compile(r'\\w+'))
|
|
454
|
+
|
|
455
|
+
Generate :mod:`Pyjoyment.Routes.Route` object matching only ``POST``
|
|
456
|
+
requests, takes the same arguments as :meth:`any` (except for the HTTP
|
|
457
|
+
methods to match, which are implied). ::
|
|
458
|
+
|
|
459
|
+
# Route with destination
|
|
460
|
+
r.post('/user').to('user#create')
|
|
461
|
+
"""
|
|
462
|
+
return self._generate_route('POST', *args, **constraints)
|
|
463
|
+
|
|
464
|
+
def put(self, *args: Any, **constraints: 'Pyjoyment.Routes.Pattern.Constraint') -> 'Pyjoyment_Routes_Route':
|
|
465
|
+
"""::
|
|
466
|
+
|
|
467
|
+
route = r.put()
|
|
468
|
+
route = r.put('/:foo')
|
|
469
|
+
route = r.put('/:foo', lambda c: ...)
|
|
470
|
+
route = r.put('/:foo', lambda c: ..., 'name')
|
|
471
|
+
route = r.put('/:foo', {'foo': 'bar'}, lambda c: ...)
|
|
472
|
+
route = r.put('/:foo', lambda c: ..., foo=re.compile(r'\\w+'))
|
|
473
|
+
|
|
474
|
+
Generate :mod:`Pyjoyment.Routes.Route` object matching only ``PUT``
|
|
475
|
+
requests, takes the same arguments as :meth:`any` (except for the HTTP
|
|
476
|
+
methods to match, which are implied). ::
|
|
477
|
+
|
|
478
|
+
# Route with destination
|
|
479
|
+
r.put('/user').to('user#replace')
|
|
480
|
+
"""
|
|
481
|
+
return self._generate_route('PUT', *args, **constraints)
|
|
482
|
+
|
|
483
|
+
def query(self, *args: Any, **constraints: 'Pyjoyment.Routes.Pattern.Constraint') -> 'Pyjoyment_Routes_Route':
|
|
484
|
+
"""::
|
|
485
|
+
|
|
486
|
+
route = r.query()
|
|
487
|
+
route = r.query('/:foo')
|
|
488
|
+
route = r.query('/:foo', lambda c: ...)
|
|
489
|
+
route = r.query('/:foo', lambda c: ..., 'name')
|
|
490
|
+
route = r.query('/:foo', {'foo': 'bar'}, lambda c: ...)
|
|
491
|
+
route = r.query('/:foo', lambda c: ..., foo=re.compile(r'\\w+'))
|
|
492
|
+
|
|
493
|
+
Generate :mod:`Pyjoyment.Routes.Route` object matching only ``QUERY``
|
|
494
|
+
requests, takes the same arguments as :meth:`any` (except for the HTTP
|
|
495
|
+
methods to match, which are implied). ::
|
|
496
|
+
|
|
497
|
+
# Route with destination
|
|
498
|
+
r.query('/user').to('user#search')
|
|
499
|
+
"""
|
|
500
|
+
return self._generate_route('QUERY', *args, **constraints)
|
|
501
|
+
|
|
502
|
+
def remove(self) -> Self:
|
|
503
|
+
"""::
|
|
504
|
+
|
|
505
|
+
r = r.remove()
|
|
506
|
+
|
|
507
|
+
Remove route from parent. ::
|
|
508
|
+
|
|
509
|
+
# Remove route completely
|
|
510
|
+
r.find('foo').remove()
|
|
511
|
+
|
|
512
|
+
# Reattach route to new parent
|
|
513
|
+
r.any('/foo').add_child(r.find('bar').remove())
|
|
514
|
+
"""
|
|
515
|
+
if (parent := self.parent) is None:
|
|
516
|
+
return self
|
|
517
|
+
_invalidate(parent)
|
|
518
|
+
parent.children[:] = [child for child in parent.children if child is not self]
|
|
519
|
+
self.parent = None
|
|
520
|
+
return self
|
|
521
|
+
|
|
522
|
+
def render(self, values: dict[str, Any]) -> str:
|
|
523
|
+
"""::
|
|
524
|
+
|
|
525
|
+
path = r.render({'foo': 'bar'})
|
|
526
|
+
|
|
527
|
+
Render route with parameters into a path.
|
|
528
|
+
"""
|
|
529
|
+
path = ''.join(
|
|
530
|
+
route.pattern.render(values, not route.children and not route.partial) for route in self._chain()
|
|
531
|
+
)
|
|
532
|
+
return path or '/'
|
|
533
|
+
|
|
534
|
+
# requires(**conditions) with no conditions is requires(), which the first overload types
|
|
535
|
+
@overload
|
|
536
|
+
def requires(self) -> list[tuple[str, Any]] | None: ... # pyright: ignore[reportOverlappingOverload]
|
|
537
|
+
|
|
538
|
+
@overload
|
|
539
|
+
def requires(self, conditions: list[tuple[str, Any]], /) -> Self: ...
|
|
540
|
+
|
|
541
|
+
@overload
|
|
542
|
+
def requires(self, **conditions: Any) -> Self: ...
|
|
543
|
+
|
|
544
|
+
def requires(self, *args: list[tuple[str, Any]], **conditions: Any) -> list[tuple[str, Any]] | None | Self:
|
|
545
|
+
"""::
|
|
546
|
+
|
|
547
|
+
requires = r.requires()
|
|
548
|
+
r = r.requires(foo=1)
|
|
549
|
+
r = r.requires(foo=1, bar={'baz': 'yada'})
|
|
550
|
+
r = r.requires([('foo', 1), ('bar', {'baz': 'yada'})])
|
|
551
|
+
|
|
552
|
+
Activate conditions for this route. Note that this automatically
|
|
553
|
+
disables the routing cache, since conditions are too complex for
|
|
554
|
+
caching. ::
|
|
555
|
+
|
|
556
|
+
# Route with condition and destination
|
|
557
|
+
r.get('/foo').requires(host=re.compile(r'mojolicious\\.org')).to('foo#bar')
|
|
558
|
+
"""
|
|
559
|
+
# Routes with conditions can't be cached
|
|
560
|
+
if not args and not conditions:
|
|
561
|
+
return self._requires
|
|
562
|
+
if not (requires := args[0] if args else list(conditions.items())):
|
|
563
|
+
return self
|
|
564
|
+
self._requires = requires
|
|
565
|
+
self.root.cache.max_keys = 0
|
|
566
|
+
|
|
567
|
+
return self
|
|
568
|
+
|
|
569
|
+
@property
|
|
570
|
+
def root(self) -> Any:
|
|
571
|
+
"""::
|
|
572
|
+
|
|
573
|
+
root = r.root
|
|
574
|
+
|
|
575
|
+
The :mod:`Pyjoyment.Routes` object this route is a descendant of.
|
|
576
|
+
"""
|
|
577
|
+
# The root has the attributes of Pyjoyment.Routes, which is a subclass of this one
|
|
578
|
+
return self._chain()[0]
|
|
579
|
+
|
|
580
|
+
@property
|
|
581
|
+
def suggested_method(self) -> str:
|
|
582
|
+
"""::
|
|
583
|
+
|
|
584
|
+
method = r.suggested_method
|
|
585
|
+
|
|
586
|
+
Suggested HTTP method for reaching this route, ``GET`` and ``POST`` are
|
|
587
|
+
preferred.
|
|
588
|
+
"""
|
|
589
|
+
via: set[str] = set()
|
|
590
|
+
for route in self._chain():
|
|
591
|
+
if not (methods := route.methods()):
|
|
592
|
+
continue
|
|
593
|
+
via = {method for method in methods if method in via} if via else set(methods)
|
|
594
|
+
|
|
595
|
+
if 'POST' in via and 'GET' not in via:
|
|
596
|
+
return 'POST'
|
|
597
|
+
return 'GET' if 'GET' in via else (sorted(via)[0] if via else 'GET')
|
|
598
|
+
|
|
599
|
+
# to(**defaults) with no defaults is to(), which the first overload types
|
|
600
|
+
@overload
|
|
601
|
+
def to(self) -> dict[str, Any]: ... # pyright: ignore[reportOverlappingOverload]
|
|
602
|
+
|
|
603
|
+
@overload
|
|
604
|
+
def to(self, shortcut: Any, /, *args: Any, **defaults: Any) -> Self: ...
|
|
605
|
+
|
|
606
|
+
@overload
|
|
607
|
+
def to(self, **defaults: Any) -> Self: ...
|
|
608
|
+
|
|
609
|
+
def to(self, *args: Any, **defaults: Any) -> dict[str, Any] | Self:
|
|
610
|
+
"""::
|
|
611
|
+
|
|
612
|
+
defaults = r.to()
|
|
613
|
+
r = r.to(action='foo')
|
|
614
|
+
r = r.to({'action': 'foo'})
|
|
615
|
+
r = r.to('controller#action')
|
|
616
|
+
r = r.to('controller#action', foo='bar')
|
|
617
|
+
r = r.to('controller#action', {'foo': 'bar'})
|
|
618
|
+
r = r.to(Pyjoyment.new())
|
|
619
|
+
r = r.to(Pyjoyment.new(), foo='bar')
|
|
620
|
+
r = r.to(Pyjoyment.new(), {'foo': 'bar'})
|
|
621
|
+
r = r.to('MyApp')
|
|
622
|
+
r = r.to('MyApp', foo='bar')
|
|
623
|
+
r = r.to('MyApp', {'foo': 'bar'})
|
|
624
|
+
|
|
625
|
+
Set default parameters for this route.
|
|
626
|
+
"""
|
|
627
|
+
pattern = self.pattern
|
|
628
|
+
if not args and not defaults:
|
|
629
|
+
return pattern.defaults
|
|
630
|
+
shortcut, defaults = Pyjo.Util._options(args, defaults) # pyright: ignore[reportPrivateUsage]
|
|
631
|
+
|
|
632
|
+
if shortcut:
|
|
633
|
+
# Application
|
|
634
|
+
if not isinstance(shortcut, str) or re.match(r'^[\w.]+$', shortcut):
|
|
635
|
+
defaults['app'] = shortcut
|
|
636
|
+
|
|
637
|
+
# Controller and action
|
|
638
|
+
elif m := re.match(r'^([\w\-.]+)?#(\w+)?$', shortcut):
|
|
639
|
+
if m[1] is not None:
|
|
640
|
+
defaults['controller'] = m[1]
|
|
641
|
+
if m[2] is not None:
|
|
642
|
+
defaults['action'] = m[2]
|
|
643
|
+
|
|
644
|
+
pattern.defaults.update(defaults)
|
|
645
|
+
|
|
646
|
+
return self
|
|
647
|
+
|
|
648
|
+
def to_str(self) -> str:
|
|
649
|
+
"""::
|
|
650
|
+
|
|
651
|
+
string = r.to_str()
|
|
652
|
+
|
|
653
|
+
Stringify the whole route.
|
|
654
|
+
"""
|
|
655
|
+
return ''.join(route.pattern.unparsed or '' for route in self._chain())
|
|
656
|
+
|
|
657
|
+
def under(self, *args: Any, **constraints: 'Pyjoyment.Routes.Pattern.Constraint') -> 'Pyjoyment_Routes_Route':
|
|
658
|
+
"""::
|
|
659
|
+
|
|
660
|
+
route = r.under(lambda c: ...)
|
|
661
|
+
route = r.under('/:foo', lambda c: ...)
|
|
662
|
+
route = r.under('/:foo', {'foo': 'bar'})
|
|
663
|
+
route = r.under('/:foo', foo=re.compile(r'\\w+'))
|
|
664
|
+
route = r.under(format=['json', 'yaml'])
|
|
665
|
+
|
|
666
|
+
Generate :mod:`Pyjoyment.Routes.Route` object for a nested route with
|
|
667
|
+
its own intermediate destination, takes the same arguments as
|
|
668
|
+
:meth:`any` (except for the HTTP methods to match, which are not
|
|
669
|
+
available). ::
|
|
670
|
+
|
|
671
|
+
# Longer version
|
|
672
|
+
r.any('/:foo', lambda c: ...).set(inline=True)
|
|
673
|
+
|
|
674
|
+
# Intermediate destination and prefix shared between two routes
|
|
675
|
+
auth = r.under('/user').to('user#auth')
|
|
676
|
+
auth.get('/show').to('#show')
|
|
677
|
+
auth.post('/create').to('#create')
|
|
678
|
+
"""
|
|
679
|
+
return self._generate_route('under', *args, **constraints)
|
|
680
|
+
|
|
681
|
+
def websocket(self, *args: Any, **constraints: 'Pyjoyment.Routes.Pattern.Constraint') -> 'Pyjoyment_Routes_Route':
|
|
682
|
+
"""::
|
|
683
|
+
|
|
684
|
+
route = r.websocket()
|
|
685
|
+
route = r.websocket('/:foo')
|
|
686
|
+
route = r.websocket('/:foo', lambda c: ...)
|
|
687
|
+
route = r.websocket('/:foo', lambda c: ..., 'name')
|
|
688
|
+
route = r.websocket('/:foo', {'foo': 'bar'}, lambda c: ...)
|
|
689
|
+
route = r.websocket('/:foo', lambda c: ..., foo=re.compile(r'\\w+'))
|
|
690
|
+
|
|
691
|
+
Generate :mod:`Pyjoyment.Routes.Route` object matching only WebSocket
|
|
692
|
+
handshakes, takes the same arguments as :meth:`any` (except for the HTTP
|
|
693
|
+
methods to match, which are implied). ::
|
|
694
|
+
|
|
695
|
+
# Route with destination
|
|
696
|
+
r.websocket('/echo').to('example#echo')
|
|
697
|
+
"""
|
|
698
|
+
route = self.get(*args, **constraints)
|
|
699
|
+
route._websocket = True
|
|
700
|
+
return route
|
|
701
|
+
|
|
702
|
+
def _chain(self) -> 'list[Pyjoyment_Routes_Route]':
|
|
703
|
+
chain: list[Pyjoyment_Routes_Route] = [self]
|
|
704
|
+
parent: Pyjoyment_Routes_Route | None = self
|
|
705
|
+
while (parent := parent.parent) is not None:
|
|
706
|
+
chain.insert(0, parent)
|
|
707
|
+
return chain
|
|
708
|
+
|
|
709
|
+
# Unlike Mojolicious: Mojo takes pairs of strings between the pattern and the name for conditions and a list for
|
|
710
|
+
# the constraints, Python takes the constraints as keyword arguments, so the conditions go to requires
|
|
711
|
+
def _generate_route(
|
|
712
|
+
self, methods: str | list[str], *args: Any, **constraints: 'Pyjoyment.Routes.Pattern.Constraint'
|
|
713
|
+
) -> 'Pyjoyment_Routes_Route':
|
|
714
|
+
defaults: dict[str, Any] = {}
|
|
715
|
+
name = pattern = None
|
|
716
|
+
for arg in args:
|
|
717
|
+
# First string is the pattern
|
|
718
|
+
if isinstance(arg, str) and pattern is None:
|
|
719
|
+
pattern = arg
|
|
720
|
+
|
|
721
|
+
# Last string is the route name
|
|
722
|
+
elif isinstance(arg, str):
|
|
723
|
+
name = arg
|
|
724
|
+
|
|
725
|
+
# Defaults
|
|
726
|
+
elif isinstance(arg, dict):
|
|
727
|
+
defaults.update(cast(dict[str, Any], arg))
|
|
728
|
+
|
|
729
|
+
# Callback
|
|
730
|
+
elif callable(arg):
|
|
731
|
+
defaults['cb'] = arg
|
|
732
|
+
|
|
733
|
+
route = self._route(pattern, **constraints).to(defaults)
|
|
734
|
+
if methods == 'under':
|
|
735
|
+
route.inline = True
|
|
736
|
+
else:
|
|
737
|
+
route.methods(methods)
|
|
738
|
+
|
|
739
|
+
if name is not None:
|
|
740
|
+
route.name = name
|
|
741
|
+
return route
|
|
742
|
+
|
|
743
|
+
def _candidates(self, path: str) -> 'list[Pyjoyment_Routes_Route]':
|
|
744
|
+
|
|
745
|
+
# Index children by the text their patterns start with (routes are frozen after the first request)
|
|
746
|
+
children = self.children
|
|
747
|
+
if not children:
|
|
748
|
+
return children
|
|
749
|
+
if (index := self._prefix_index) is None:
|
|
750
|
+
index = self._prefix_index = ([], {})
|
|
751
|
+
for i, child in enumerate(children):
|
|
752
|
+
if (prefixes := child._prefixes()) is not None:
|
|
753
|
+
for prefix in prefixes:
|
|
754
|
+
index[1].setdefault(len(prefix), {}).setdefault(prefix, []).append(i)
|
|
755
|
+
else:
|
|
756
|
+
index[0].append(i)
|
|
757
|
+
|
|
758
|
+
# Children that could match the path, in their original order
|
|
759
|
+
if not (text := index[1]):
|
|
760
|
+
return children
|
|
761
|
+
found = set(index[0])
|
|
762
|
+
for length, prefixes in text.items():
|
|
763
|
+
if hit := prefixes.get(path[:length]):
|
|
764
|
+
found.update(hit)
|
|
765
|
+
return [children[i] for i in sorted(found)]
|
|
766
|
+
|
|
767
|
+
# Text one of the paths matched by this route has to start with (None if it could be anything)
|
|
768
|
+
def _prefixes(self) -> list[str] | None:
|
|
769
|
+
|
|
770
|
+
# Custom regexes and pattern classes could match anything
|
|
771
|
+
pattern = self.pattern
|
|
772
|
+
regex, compiled = pattern.regex, pattern._compiled # pyright: ignore[reportPrivateUsage]
|
|
773
|
+
if type(pattern) is not Pyjoyment.Routes.Pattern.object or (regex is not None and regex is not compiled):
|
|
774
|
+
return None
|
|
775
|
+
if tree := pattern.tree:
|
|
776
|
+
first = tree[0]
|
|
777
|
+
return [first[1]] if first[0] == 'text' else None
|
|
778
|
+
|
|
779
|
+
# Routes with an empty pattern pass the path on to their children
|
|
780
|
+
children = self.children
|
|
781
|
+
if self.partial or not children:
|
|
782
|
+
return None
|
|
783
|
+
prefixes: list[str] = []
|
|
784
|
+
for child in children:
|
|
785
|
+
if (child_prefixes := child._prefixes()) is None:
|
|
786
|
+
return None
|
|
787
|
+
prefixes.extend(child_prefixes)
|
|
788
|
+
return prefixes
|
|
789
|
+
|
|
790
|
+
def _index(self) -> 'dict[str, Pyjoyment_Routes_Route]':
|
|
791
|
+
auto: dict[str, Pyjoyment_Routes_Route] = {}
|
|
792
|
+
custom: dict[str, Pyjoyment_Routes_Route] = {}
|
|
793
|
+
children = list(self.children)
|
|
794
|
+
while children:
|
|
795
|
+
child = children.pop(0)
|
|
796
|
+
names = custom if child.has_custom_name else auto
|
|
797
|
+
names.setdefault(child.name or '', child)
|
|
798
|
+
children.extend(child.children)
|
|
799
|
+
|
|
800
|
+
return {**auto, **custom}
|
|
801
|
+
|
|
802
|
+
def _route(
|
|
803
|
+
self, pattern: str | None, **constraints: 'Pyjoyment.Routes.Pattern.Constraint'
|
|
804
|
+
) -> 'Pyjoyment_Routes_Route':
|
|
805
|
+
route = self.add_child(Pyjoyment_Routes_Route.new().parse(pattern, **constraints)).children[-1]
|
|
806
|
+
new_pattern = route.pattern
|
|
807
|
+
if any(self.is_reserved(placeholder) for placeholder in new_pattern.placeholders):
|
|
808
|
+
raise Pyjo.Exception.new(f'Route pattern "{new_pattern.unparsed}" contains a reserved stash value')
|
|
809
|
+
|
|
810
|
+
old_pattern = self.pattern
|
|
811
|
+
if 'format' in (constraints := old_pattern.constraints) and new_pattern.constraints.get('format') is None:
|
|
812
|
+
new_pattern.constraints['format'] = constraints['format']
|
|
813
|
+
if 'format' in (defaults := old_pattern.defaults) and new_pattern.defaults.get('format') is None:
|
|
814
|
+
new_pattern.defaults['format'] = defaults['format']
|
|
815
|
+
|
|
816
|
+
return route
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def _invalidate(route: Pyjoyment_Routes_Route | None) -> None:
|
|
820
|
+
while route is not None:
|
|
821
|
+
route._prefix_index = None # pyright: ignore[reportPrivateUsage]
|
|
822
|
+
route = route.parent
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
new = Pyjoyment_Routes_Route.new
|
|
826
|
+
object = Pyjoyment_Routes_Route
|
|
827
|
+
Route = Pyjoyment_Routes_Route
|