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,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