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