Pyjoyment 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. Pyjo/App/ASGI.py +735 -0
  2. Pyjo/App/CGI.py +773 -0
  3. Pyjo/App/FastCGI.py +631 -0
  4. Pyjo/App/Lambda.py +987 -0
  5. Pyjo/App/WSGI.py +427 -0
  6. Pyjo/App/__init__.py +409 -0
  7. Pyjo/Asset/File.py +377 -0
  8. Pyjo/Asset/Memory.py +235 -0
  9. Pyjo/Asset/__init__.py +200 -0
  10. Pyjo/Base.py +215 -0
  11. Pyjo/Cache.py +119 -0
  12. Pyjo/Collection.py +570 -0
  13. Pyjo/Content/MultiPart.py +325 -0
  14. Pyjo/Content/Single.py +193 -0
  15. Pyjo/Content/__init__.py +806 -0
  16. Pyjo/Cookie/Request.py +90 -0
  17. Pyjo/Cookie/Response.py +294 -0
  18. Pyjo/Cookie/__init__.py +143 -0
  19. Pyjo/DOM/CSS.py +1019 -0
  20. Pyjo/DOM/HTML.py +550 -0
  21. Pyjo/DOM/__init__.py +1329 -0
  22. Pyjo/Date.py +294 -0
  23. Pyjo/EventEmitter.py +522 -0
  24. Pyjo/Exception.py +494 -0
  25. Pyjo/File.py +959 -0
  26. Pyjo/GetOpt.py +605 -0
  27. Pyjo/Headers.py +760 -0
  28. Pyjo/HelloWorld.py +77 -0
  29. Pyjo/Home.py +109 -0
  30. Pyjo/IOLoop/ChildProcess.py +591 -0
  31. Pyjo/IOLoop/Client.py +736 -0
  32. Pyjo/IOLoop/Datagram.py +982 -0
  33. Pyjo/IOLoop/Messages.py +664 -0
  34. Pyjo/IOLoop/Server.py +458 -0
  35. Pyjo/IOLoop/Stdio.py +446 -0
  36. Pyjo/IOLoop/Stream.py +644 -0
  37. Pyjo/IOLoop/Subprocess.py +419 -0
  38. Pyjo/IOLoop/TLS.py +360 -0
  39. Pyjo/IOLoop/__init__.py +1036 -0
  40. Pyjo/IOLoop/certs/README.md +41 -0
  41. Pyjo/IOLoop/certs/ca.crt +20 -0
  42. Pyjo/IOLoop/certs/server.crt +21 -0
  43. Pyjo/IOLoop/certs/server.key +28 -0
  44. Pyjo/JSON/Pointer.py +135 -0
  45. Pyjo/JSON/RPC.py +772 -0
  46. Pyjo/JSON/Schema.py +2506 -0
  47. Pyjo/JSON/__init__.py +573 -0
  48. Pyjo/Loader.py +342 -0
  49. Pyjo/Log.py +723 -0
  50. Pyjo/Message/Request.py +725 -0
  51. Pyjo/Message/Response.py +476 -0
  52. Pyjo/Message/__init__.py +830 -0
  53. Pyjo/Parameters.py +472 -0
  54. Pyjo/Path.py +419 -0
  55. Pyjo/Promise.py +894 -0
  56. Pyjo/Prove.py +1305 -0
  57. Pyjo/Reactor/Asyncio.py +854 -0
  58. Pyjo/Reactor/Poll.py +439 -0
  59. Pyjo/Reactor/__init__.py +525 -0
  60. Pyjo/SSE.py +119 -0
  61. Pyjo/Server/ASGI.py +486 -0
  62. Pyjo/Server/CGI.py +259 -0
  63. Pyjo/Server/Daemon.py +1007 -0
  64. Pyjo/Server/FastCGI.py +517 -0
  65. Pyjo/Server/Hot/__init__.py +687 -0
  66. Pyjo/Server/Hot/__main__.py +5 -0
  67. Pyjo/Server/Lambda.py +641 -0
  68. Pyjo/Server/Prefork.py +917 -0
  69. Pyjo/Server/Prethread.py +394 -0
  70. Pyjo/Server/Reload/Backend/Poll.py +114 -0
  71. Pyjo/Server/Reload/Backend/__init__.py +126 -0
  72. Pyjo/Server/Reload/__init__.py +482 -0
  73. Pyjo/Server/Reload/__main__.py +5 -0
  74. Pyjo/Server/Stdio.py +228 -0
  75. Pyjo/Server/WSGI.py +349 -0
  76. Pyjo/Server/__init__.py +396 -0
  77. Pyjo/String/Bytes.py +532 -0
  78. Pyjo/String/HTML.py +210 -0
  79. Pyjo/String/Mixin.py +333 -0
  80. Pyjo/String/Template.py +470 -0
  81. Pyjo/String/Unicode.py +553 -0
  82. Pyjo/String/__init__.py +0 -0
  83. Pyjo/TOML.py +232 -0
  84. Pyjo/Template.py +1290 -0
  85. Pyjo/Test/Pyjo.py +1454 -0
  86. Pyjo/Test/__init__.py +1233 -0
  87. Pyjo/Test/pytest_plugin.py +305 -0
  88. Pyjo/Transaction/HTTP.py +374 -0
  89. Pyjo/Transaction/WebSocket.py +709 -0
  90. Pyjo/Transaction/__init__.py +429 -0
  91. Pyjo/URL/Template.py +459 -0
  92. Pyjo/URL/__init__.py +651 -0
  93. Pyjo/Upload.py +139 -0
  94. Pyjo/UserAgent/CookieJar.py +391 -0
  95. Pyjo/UserAgent/Proxy.py +138 -0
  96. Pyjo/UserAgent/Server.py +239 -0
  97. Pyjo/UserAgent/Transactor.py +766 -0
  98. Pyjo/UserAgent/__init__.py +1243 -0
  99. Pyjo/Util.py +3567 -0
  100. Pyjo/WebSocket.py +332 -0
  101. Pyjo/YAML.py +1167 -0
  102. Pyjo/__init__.py +441 -0
  103. Pyjo/__main__.py +35 -0
  104. Pyjo/py.typed +0 -0
  105. Pyjoyment/Command/Author/__init__.py +20 -0
  106. Pyjoyment/Command/Author/generate/__init__.py +106 -0
  107. Pyjoyment/Command/Author/generate/app.py +299 -0
  108. Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
  109. Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
  110. Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
  111. Pyjoyment/Command/Author/generate/lite_app.py +113 -0
  112. Pyjoyment/Command/Author/generate/plugin.py +267 -0
  113. Pyjoyment/Command/Author/generate/pyproject.py +123 -0
  114. Pyjoyment/Command/Author/inflate.py +101 -0
  115. Pyjoyment/Command/__init__.py +332 -0
  116. Pyjoyment/Command/asgi.py +92 -0
  117. Pyjoyment/Command/cgi.py +90 -0
  118. Pyjoyment/Command/daemon.py +160 -0
  119. Pyjoyment/Command/eval.py +128 -0
  120. Pyjoyment/Command/fastcgi.py +99 -0
  121. Pyjoyment/Command/get.py +305 -0
  122. Pyjoyment/Command/lambda.py +117 -0
  123. Pyjoyment/Command/prefork.py +181 -0
  124. Pyjoyment/Command/prethread.py +126 -0
  125. Pyjoyment/Command/routes.py +133 -0
  126. Pyjoyment/Command/stdio.py +92 -0
  127. Pyjoyment/Command/version.py +141 -0
  128. Pyjoyment/Command/wsgi.py +93 -0
  129. Pyjoyment/Commands.py +546 -0
  130. Pyjoyment/Controller.py +1307 -0
  131. Pyjoyment/HelloWorld.py +56 -0
  132. Pyjoyment/Lite.py +494 -0
  133. Pyjoyment/Plugin/AllowedHosts.py +205 -0
  134. Pyjoyment/Plugin/Config.py +248 -0
  135. Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
  136. Pyjoyment/Plugin/EPLRenderer.py +134 -0
  137. Pyjoyment/Plugin/EPRenderer.py +212 -0
  138. Pyjoyment/Plugin/HeaderCondition.py +98 -0
  139. Pyjoyment/Plugin/Health.py +325 -0
  140. Pyjoyment/Plugin/JSONConfig.py +172 -0
  141. Pyjoyment/Plugin/Mount.py +223 -0
  142. Pyjoyment/Plugin/PyRenderer.py +209 -0
  143. Pyjoyment/Plugin/TOMLConfig.py +141 -0
  144. Pyjoyment/Plugin/TagHelpers.py +959 -0
  145. Pyjoyment/Plugin/YAMLConfig.py +178 -0
  146. Pyjoyment/Plugin/__init__.py +80 -0
  147. Pyjoyment/Plugins.py +215 -0
  148. Pyjoyment/Renderer.py +673 -0
  149. Pyjoyment/Routes/Match.py +251 -0
  150. Pyjoyment/Routes/Pattern.py +443 -0
  151. Pyjoyment/Routes/Route.py +827 -0
  152. Pyjoyment/Routes/__init__.py +419 -0
  153. Pyjoyment/Sessions.py +255 -0
  154. Pyjoyment/Static.py +421 -0
  155. Pyjoyment/Types.py +238 -0
  156. Pyjoyment/Validator/Validation.py +395 -0
  157. Pyjoyment/Validator/__init__.py +304 -0
  158. Pyjoyment/__init__.py +1083 -0
  159. Pyjoyment/__main__.py +5 -0
  160. Pyjoyment/py.typed +0 -0
  161. Pyjoyment/resources/public/favicon.ico +0 -0
  162. Pyjoyment/resources/public/favicon.svg +345 -0
  163. Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
  164. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
  165. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
  166. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
  167. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
  168. Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
  169. Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
  170. Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
  171. Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
  172. Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
  173. Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
  174. Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
  175. Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
  176. Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
  177. Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
  178. Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
  179. Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
  180. Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
  181. Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
  182. Pyjoyment/resources/public/pyjo/logo.png +0 -0
  183. Pyjoyment/resources/public/pyjo/logo.svg +346 -0
  184. Pyjoyment/resources/public/pyjo/notfound.png +0 -0
  185. Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
  186. Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
  187. Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
  188. Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
  189. Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
  190. Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
  191. Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
  192. Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
  193. Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
  194. Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
  195. Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
  196. pyjoyment-0.1.0.dist-info/METADATA +376 -0
  197. pyjoyment-0.1.0.dist-info/RECORD +200 -0
  198. pyjoyment-0.1.0.dist-info/WHEEL +4 -0
  199. pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
  200. pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,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