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
Pyjoyment/Static.py ADDED
@@ -0,0 +1,421 @@
1
+ """
2
+ Pyjoyment.Static - Serve static files
3
+ =====================================
4
+ ::
5
+
6
+ from Pyjoyment.Static import Static
7
+
8
+ static = Static()
9
+ static.classes.append('MyApp.Controller.Foo')
10
+ static.paths.append('/home/sri/public')
11
+
12
+ # Or import the module, as in Google Python Style Guide
13
+
14
+ import Pyjoyment.Static
15
+
16
+ static = Pyjoyment.Static.new()
17
+ static.classes.append('MyApp.Controller.Foo')
18
+ static.paths.append('/home/sri/public')
19
+
20
+ :mod:`Pyjoyment.Static` is a static file server with ``Range``,
21
+ ``If-Modified-Since`` and ``If-None-Match`` support, based on
22
+ `RFC 7232 <https://tools.ietf.org/html/rfc7232>`_ and
23
+ `RFC 7233 <https://tools.ietf.org/html/rfc7233>`_.
24
+
25
+ See also
26
+ --------
27
+
28
+ :mod:`Pyjoyment`, https://mojolicious.org.
29
+
30
+ Ported from `Mojolicious::Static <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojolicious/Static.pm>`_
31
+ of Mojolicious 9.49.
32
+ """
33
+
34
+ import Pyjo.Asset
35
+ import Pyjo.Asset.File
36
+ import Pyjo.Asset.Memory
37
+ import Pyjo.Base
38
+ import Pyjo.Date
39
+ import Pyjo.File
40
+ import Pyjo.Loader
41
+
42
+ from Pyjo.Util import encode, md5_sum, trim
43
+
44
+ import os
45
+ import re
46
+
47
+ from typing import TYPE_CHECKING, Any, cast
48
+
49
+
50
+ if TYPE_CHECKING:
51
+ import Pyjoyment.Controller
52
+
53
+
54
+ # Bundled files
55
+ _PUBLIC = Pyjo.File.path(__file__).sibling('resources', 'public')
56
+ _EXTRA = {'/'.join(file.to_rel(_PUBLIC).to_list()): file.realpath().to_str() for file in _PUBLIC.list_tree()}
57
+
58
+
59
+ class Pyjoyment_Static(Pyjo.Base.object):
60
+ """
61
+ :mod:`Pyjoyment.Static` inherits all methods from :mod:`Pyjo.Base` and
62
+ implements the following new ones.
63
+ """
64
+
65
+ def __init__(self) -> None:
66
+ """::
67
+
68
+ static = Pyjoyment.Static.new()
69
+
70
+ Construct a new :mod:`Pyjoyment.Static` object.
71
+ """
72
+ self.asset_dir = 'assets'
73
+ """::
74
+
75
+ dir = static.asset_dir
76
+ static.asset_dir = 'assets'
77
+
78
+ Subdirectory used for all static assets, defaults to ``assets``.
79
+ """
80
+
81
+ self.classes = ['__main__']
82
+ """::
83
+
84
+ classes = static.classes
85
+ static.classes = ['__main__']
86
+
87
+ Classes to use for finding files in ``DATA`` sections with
88
+ :mod:`Pyjo.Loader`, first one has the highest precedence, defaults to
89
+ ``__main__``. Only files with exactly one extension will be used, like
90
+ ``index.html``. Note that for files to be detected, these classes need
91
+ to have already been loaded and added before :meth:`warmup` is called,
92
+ which usually happens automatically during application startup. ::
93
+
94
+ # Add another class with static files in DATA section
95
+ static.classes.append('Pyjoyment.Plugin.Fun')
96
+
97
+ # Add another class with static files in DATA section and higher precedence
98
+ static.classes.insert(0, 'Pyjoyment.Plugin.MoreFun')
99
+ """
100
+
101
+ self.extra = dict(_EXTRA)
102
+ """::
103
+
104
+ extra = static.extra
105
+ static.extra = {'foo/bar.txt': '/home/sri/myapp/bar.txt'}
106
+
107
+ Paths for extra files to be served from locations other than
108
+ :attr:`paths`, such as the images used by the built-in exception and
109
+ not found pages. Note that extra files are only served if no better
110
+ alternative could be found in :attr:`paths` and :attr:`classes`. ::
111
+
112
+ # Remove built-in favicon
113
+ del static.extra['favicon.ico']
114
+ """
115
+
116
+ self.paths: list[str] = []
117
+ """::
118
+
119
+ paths = static.paths
120
+ static.paths = ['/home/sri/public']
121
+
122
+ Directories to serve static files from, first one has the highest
123
+ precedence. ::
124
+
125
+ # Add another "public" directory
126
+ static.paths.append('/home/sri/public')
127
+
128
+ # Add another "public" directory with higher precedence
129
+ static.paths.insert(0, '/home/sri/themes/blue/public')
130
+ """
131
+
132
+ self.prefix: str | None = None
133
+ """::
134
+
135
+ prefix = static.prefix
136
+ static.prefix = '/static'
137
+
138
+ Prefix to use for all static files, defaults to ``None``. This can be
139
+ very useful for production deployments where the reverse proxy server
140
+ should take over serving static files.
141
+ """
142
+
143
+ self._assets: dict[str, str] | None = None
144
+ self._index: dict[str, str] | None = None
145
+
146
+ def asset_path(self, asset: str) -> str:
147
+ """::
148
+
149
+ path = static.asset_path('/app.js')
150
+
151
+ Get static asset path.
152
+ """
153
+ if not asset.startswith('/'):
154
+ asset = f'/{asset}'
155
+ if self._assets is None:
156
+ self._assets = {}
157
+ return self.file_path(f'/{self.asset_dir}{self._assets.get(asset, asset)}')
158
+
159
+ def dispatch(self, c: 'Pyjoyment.Controller.object') -> bool | None:
160
+ """::
161
+
162
+ boolean = static.dispatch(Pyjoyment.Controller.new())
163
+
164
+ Serve static file for :mod:`Pyjoyment.Controller` object.
165
+ """
166
+ # Method (GET or HEAD)
167
+ req = c.req
168
+ method = req.method
169
+ if method != 'GET' and method != 'HEAD':
170
+ return None
171
+
172
+ # Canonical path
173
+ stash = c.stash()
174
+ path = req.url.path
175
+ path = path.new(stash['path']) if stash.get('path') else path.clone()
176
+ if not (parts := path.canonicalize().parts):
177
+ return None
178
+
179
+ # Serve static file and prevent path traversal
180
+ canon_path = '/'.join(parts)
181
+ if re.search(r'^\.\./|\\', canon_path) or not self.serve(c, canon_path):
182
+ return None
183
+ stash['mojo.static'] = True
184
+
185
+ # Development assets will be rebuilt a lot, do not let browsers cache them
186
+ if c.app.mode == 'development' and canon_path.startswith(self.asset_dir):
187
+ c.res.headers.cache_control = 'no-cache'
188
+
189
+ return bool(c.rendered())
190
+
191
+ def file(self, rel: str) -> Pyjo.Asset.File.object | Pyjo.Asset.Memory.object | None:
192
+ """::
193
+
194
+ asset = static.file('images/logo.png')
195
+ asset = static.file('../lib/MyApp.py')
196
+
197
+ Build :mod:`Pyjo.Asset.File` or :mod:`Pyjo.Asset.Memory` object for a
198
+ file, relative to :attr:`paths` or from :attr:`classes`, or return
199
+ ``None`` if it doesn't exist. Note that this method uses a relative
200
+ path, but does not protect from traversing to parent directories. ::
201
+
202
+ content = static.file('foo/bar.html').slurp()
203
+ """
204
+ if self._index is None:
205
+ self.warmup()
206
+
207
+ # Search all paths
208
+ parts = rel.split('/')
209
+ for path in self.paths:
210
+ if asset := _get_file(Pyjo.File.path(path, *parts).to_str()):
211
+ return asset
212
+
213
+ # Search DATA
214
+ if asset := self._get_data_file(rel):
215
+ return asset
216
+
217
+ # Search extra files
218
+ extra = self.extra
219
+ return _get_file(extra[rel]) if rel in extra else None
220
+
221
+ def file_path(self, file: str) -> str:
222
+ """::
223
+
224
+ path = static.file_path('/index.html')
225
+
226
+ Get static file path with :attr:`prefix` if it has been configured.
227
+ """
228
+ if not file.startswith('/'):
229
+ file = f'/{file}'
230
+ if not (prefix := self.prefix):
231
+ return file
232
+ return f'{prefix}{file}'
233
+
234
+ def is_fresh(
235
+ self, c: 'Pyjoyment.Controller.object', *, etag: str | None = None, last_modified: float | None = None
236
+ ) -> bool:
237
+ """::
238
+
239
+ boolean = static.is_fresh(Pyjoyment.Controller.new(), etag='abc')
240
+ boolean = static.is_fresh(Pyjoyment.Controller.new(), etag='W/"def"')
241
+
242
+ Check freshness of request by comparing the ``If-None-Match`` and
243
+ ``If-Modified-Since`` request headers to the ``ETag`` and
244
+ ``Last-Modified`` response headers.
245
+
246
+ These options are currently available:
247
+
248
+ etag ::
249
+
250
+ etag='abc'
251
+ etag='W/"abc"'
252
+
253
+ Add ``ETag`` header before comparing.
254
+
255
+ last_modified ::
256
+
257
+ last_modified=epoch
258
+
259
+ Add ``Last-Modified`` header before comparing.
260
+ """
261
+ res_headers = c.res.headers
262
+ if last_modified:
263
+ res_headers.last_modified = Pyjo.Date.new(last_modified).to_str()
264
+ if etag:
265
+ etag = etag if etag.startswith('W/"') else f'"{etag}"'
266
+ res_headers.etag = etag
267
+
268
+ # Unconditional
269
+ req_headers = c.req.headers
270
+ match = req_headers.if_none_match
271
+ if not (since := req_headers.if_modified_since) and not match:
272
+ return False
273
+
274
+ # If-None-Match
275
+ if etag is None:
276
+ etag = res_headers.etag or ''
277
+ if match and not any(e == etag or f'W/{e}' == etag for e in (trim(m) for m in match.split(','))):
278
+ return False
279
+
280
+ # If-Modified-Since
281
+ last = last_modified if last_modified else res_headers.last_modified
282
+ if not last or not since:
283
+ return bool(match)
284
+ return (_epoch(last) or 0) <= (_epoch(since) or 0)
285
+
286
+ def serve(self, c: 'Pyjoyment.Controller.object', rel: str) -> bool | None:
287
+ """::
288
+
289
+ boolean = static.serve(Pyjoyment.Controller.new(), 'images/logo.png')
290
+ boolean = static.serve(Pyjoyment.Controller.new(), '../lib/MyApp.py')
291
+
292
+ Serve a specific file, relative to :attr:`paths` or from
293
+ :attr:`classes`. Note that this method uses a relative path, but does
294
+ not protect from traversing to parent directories.
295
+ """
296
+ # Prefix
297
+ if prefix := self.prefix:
298
+ rel = f'/{rel}'
299
+ if not rel.startswith(f'{prefix}/'):
300
+ return None
301
+ rel = rel[len(prefix) + 1 :]
302
+
303
+ if not (asset := self.file(rel)):
304
+ return None
305
+ c.app.types.content_type(c, file=rel)
306
+ return bool(self.serve_asset(c, asset))
307
+
308
+ def serve_asset(self, c: 'Pyjoyment.Controller.object', asset: Pyjo.Asset.object) -> Any:
309
+ """::
310
+
311
+ static.serve_asset(Pyjoyment.Controller.new(), Pyjo.Asset.File.new())
312
+
313
+ Serve a :mod:`Pyjo.Asset.File` or :mod:`Pyjo.Asset.Memory` object with
314
+ ``Range``, ``If-Modified-Since`` and ``If-None-Match`` support.
315
+ """
316
+ # Content-Type
317
+ if asset.is_file:
318
+ c.app.types.content_type(c, file=cast(Pyjo.Asset.File.object, asset).path)
319
+
320
+ # Last-Modified and ETag
321
+ res = c.res
322
+ res.code = 200
323
+ res.headers.accept_ranges = 'bytes'
324
+ # Whole seconds, as Perl gives the time of a file, and as an HTTP date has it
325
+ mtime = int(asset.mtime)
326
+ if self.is_fresh(c, etag=md5_sum(str(mtime).encode()), last_modified=mtime):
327
+ res.code = 304
328
+ return res
329
+
330
+ # Range
331
+ content: Any = res.content
332
+ if not (range := c.req.headers.range):
333
+ content.asset = asset
334
+ return content
335
+
336
+ # Not satisfiable
337
+ if not (size := asset.size):
338
+ res.code = 416
339
+ return res
340
+ if not (m := re.match(r'^bytes=(\d+)?-(\d+)?', range)):
341
+ res.code = 416
342
+ return res
343
+ start = int(m[1]) if m[1] is not None else 0
344
+ end = int(m[2]) if m[2] is not None and int(m[2]) < size else size - 1
345
+ if start > end:
346
+ res.code = 416
347
+ return res
348
+
349
+ # Satisfiable
350
+ res.code = 206
351
+ res.headers.content_length = end - start + 1
352
+ res.headers.content_range = f'bytes {start}-{end}/{size}'
353
+ asset.start_range = start
354
+ asset.end_range = end
355
+ content.asset = asset
356
+ return content
357
+
358
+ def warmup(self) -> None:
359
+ """::
360
+
361
+ static.warmup()
362
+
363
+ Prepare static files from :attr:`classes` and static assets for future
364
+ use.
365
+ """
366
+ # DATA sections
367
+ index: dict[str, str] = {}
368
+ self._index = index
369
+ for cls in reversed(self.classes):
370
+ for key in cast(dict[str, Any], Pyjo.Loader.data_section(cls) or {}):
371
+ index[key] = cls
372
+
373
+ # Static assets
374
+ assets: dict[str, str] = {}
375
+ self._assets = assets
376
+ asset_dir = self.asset_dir
377
+ for path in self.paths:
378
+ asset_path = Pyjo.File.path(path, asset_dir)
379
+ if not os.path.isdir(asset_path):
380
+ continue
381
+
382
+ for asset_file in asset_path.list_tree():
383
+ parts = asset_file.to_rel(asset_path).to_list()
384
+ filename = parts.pop()
385
+ prefix = '/'.join(parts)
386
+
387
+ if not (m := re.match(r'^([^.]+)\.([^.]+)\.(.+)$', filename)):
388
+ continue
389
+ checksum = m[2]
390
+ short = f'/{m[1]}.{m[3]}' if prefix == '' else f'/{prefix}/{m[1]}.{m[3]}'
391
+ long = '/' + '/'.join([*parts, filename])
392
+
393
+ if short not in assets or checksum == 'development':
394
+ assets[short] = long
395
+
396
+ def _get_data_file(self, rel: str) -> Pyjo.Asset.Memory.object | None:
397
+ # Protect files without extensions and templates with two extensions
398
+ if not re.search(r'\.\w+$', rel) or re.search(r'\.\w+\.\w+$', rel):
399
+ return None
400
+
401
+ # Find file
402
+ cls = (self._index or {}).get(rel)
403
+ if cls is None or (data := Pyjo.Loader.data_section(cls, rel)) is None:
404
+ return None
405
+ data = cast(str | bytes, data)
406
+ asset = Pyjo.Asset.Memory.new()
407
+ asset.add_chunk(data if isinstance(data, bytes) else encode('UTF-8', data))
408
+ return asset
409
+
410
+
411
+ def _epoch(date: Any) -> float | None:
412
+ return Pyjo.Date.new(date).epoch
413
+
414
+
415
+ def _get_file(path: str) -> Pyjo.Asset.File.object | None:
416
+ return Pyjo.Asset.File.new(path=path) if os.path.isfile(path) and os.access(path, os.R_OK) else None
417
+
418
+
419
+ new = Pyjoyment_Static.new
420
+ object = Pyjoyment_Static
421
+ Static = Pyjoyment_Static
Pyjoyment/Types.py ADDED
@@ -0,0 +1,238 @@
1
+ """
2
+ Pyjoyment.Types - MIME types
3
+ ============================
4
+ ::
5
+
6
+ from Pyjoyment.Types import Types
7
+
8
+ types = Types()
9
+ types.type('foo', 'text/foo')
10
+ print(types.type('foo'))
11
+
12
+ # Or import the module, as in Google Python Style Guide
13
+
14
+ import Pyjoyment.Types
15
+
16
+ types = Pyjoyment.Types.new()
17
+ types.type('foo', 'text/foo')
18
+ print(types.type('foo'))
19
+
20
+ :mod:`Pyjoyment.Types` manages MIME types for :mod:`Pyjoyment`. ::
21
+
22
+ appcache -> text/cache-manifest
23
+ atom -> application/atom+xml
24
+ bin -> application/octet-stream
25
+ css -> text/css
26
+ gif -> image/gif
27
+ gz -> application/x-gzip
28
+ htm -> text/html
29
+ html -> text/html;charset=UTF-8
30
+ ico -> image/x-icon
31
+ jpeg -> image/jpeg
32
+ jpg -> image/jpeg
33
+ js -> application/javascript
34
+ json -> application/json;charset=UTF-8
35
+ mp3 -> audio/mpeg
36
+ mp4 -> video/mp4
37
+ ogg -> audio/ogg
38
+ ogv -> video/ogg
39
+ pdf -> application/pdf
40
+ png -> image/png
41
+ rss -> application/rss+xml
42
+ sse -> text/event-stream
43
+ svg -> image/svg+xml
44
+ toml -> application/toml;charset=UTF-8
45
+ ttf -> font/ttf
46
+ txt -> text/plain;charset=UTF-8
47
+ webm -> video/webm
48
+ woff -> font/woff
49
+ woff2 -> font/woff2
50
+ xml -> application/xml,text/xml
51
+ yaml -> application/yaml;charset=UTF-8,text/yaml,application/x-yaml,text/x-yaml
52
+ zip -> application/zip
53
+
54
+ The most common ones are already defined.
55
+
56
+ See also
57
+ --------
58
+
59
+ :mod:`Pyjoyment`, https://mojolicious.org.
60
+
61
+ Ported from `Mojolicious::Types <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojolicious/Types.pm>`_
62
+ of Mojolicious 9.49.
63
+ """
64
+
65
+ import Pyjo.Base
66
+ import Pyjo.File
67
+
68
+ import os
69
+ import re
70
+
71
+ from typing import TYPE_CHECKING, Self, overload
72
+
73
+
74
+ if TYPE_CHECKING:
75
+ import Pyjo.Headers
76
+ import Pyjoyment.Controller
77
+
78
+
79
+ _ACCEPT_RE = re.compile(r'^\s*([^,; ]+)(?:\s*\;\s*q\s*=\s*(\d+(?:\.\d+)?))?\s*$', re.IGNORECASE)
80
+
81
+
82
+ class Pyjoyment_Types(Pyjo.Base.object):
83
+ """
84
+ :mod:`Pyjoyment.Types` inherits all methods from :mod:`Pyjo.Base` and
85
+ implements the following new ones.
86
+ """
87
+
88
+ def __init__(self, mapping: dict[str, list[str]] | None = None) -> None:
89
+ """::
90
+
91
+ types = Pyjoyment.Types.new()
92
+ types = Pyjoyment.Types.new(mapping={'png': ['image/png']})
93
+
94
+ Construct a new :mod:`Pyjoyment.Types` object.
95
+ """
96
+ self.mapping: dict[str, list[str]] = (
97
+ mapping
98
+ if mapping is not None
99
+ else {
100
+ 'appcache': ['text/cache-manifest'],
101
+ 'atom': ['application/atom+xml'],
102
+ 'bin': ['application/octet-stream'],
103
+ 'css': ['text/css'],
104
+ 'gif': ['image/gif'],
105
+ 'gz': ['application/x-gzip'],
106
+ 'htm': ['text/html'],
107
+ 'html': ['text/html;charset=UTF-8'],
108
+ 'ico': ['image/x-icon'],
109
+ 'jpeg': ['image/jpeg'],
110
+ 'jpg': ['image/jpeg'],
111
+ 'js': ['application/javascript'],
112
+ 'json': ['application/json;charset=UTF-8'],
113
+ 'mp3': ['audio/mpeg'],
114
+ 'mp4': ['video/mp4'],
115
+ 'ogg': ['audio/ogg'],
116
+ 'ogv': ['video/ogg'],
117
+ 'pdf': ['application/pdf'],
118
+ 'png': ['image/png'],
119
+ 'rss': ['application/rss+xml'],
120
+ 'sse': ['text/event-stream'],
121
+ 'svg': ['image/svg+xml'],
122
+ # Not in Mojolicious: TOML
123
+ 'toml': ['application/toml;charset=UTF-8'],
124
+ 'ttf': ['font/ttf'],
125
+ 'txt': ['text/plain;charset=UTF-8'],
126
+ 'webm': ['video/webm'],
127
+ 'woff': ['font/woff'],
128
+ 'woff2': ['font/woff2'],
129
+ 'xml': ['application/xml', 'text/xml'],
130
+ # Not in Mojolicious: YAML, of RFC 9512, and the types that it had before
131
+ 'yaml': ['application/yaml;charset=UTF-8', 'text/yaml', 'application/x-yaml', 'text/x-yaml'],
132
+ 'zip': ['application/zip'],
133
+ }
134
+ )
135
+ """::
136
+
137
+ mapping = types.mapping
138
+ types.mapping = {'png': ['image/png']}
139
+
140
+ MIME type mapping.
141
+ """
142
+
143
+ def content_type(
144
+ self, c: 'Pyjoyment.Controller.object', *, ext: str | None = None, file: str | os.PathLike[str] | None = None
145
+ ) -> 'Pyjo.Headers.object | None':
146
+ """::
147
+
148
+ types.content_type(Pyjoyment.Controller.new(), ext='json')
149
+
150
+ Detect MIME type for :mod:`Pyjoyment.Controller` object unless a
151
+ ``Content-Type`` response header has already been set, defaults to using
152
+ ``application/octet-stream`` if no better alternative could be found.
153
+ These options are currently available:
154
+
155
+ ext
156
+ ::
157
+
158
+ ext='json'
159
+
160
+ File extension to get MIME type for.
161
+
162
+ file
163
+ ::
164
+
165
+ file='foo/bar.png'
166
+
167
+ File path to get MIME type for.
168
+ """
169
+ headers = c.res.headers
170
+ if headers.content_type:
171
+ return None
172
+
173
+ type = self.file_type(file) if file else self.type(ext or '')
174
+ headers.content_type = type if type is not None else 'application/octet-stream'
175
+ return headers
176
+
177
+ def detect(self, accept: str | None) -> list[str]:
178
+ """::
179
+
180
+ exts = types.detect('text/html, application/json;q=9')
181
+
182
+ Detect file extensions from ``Accept`` header value. ::
183
+
184
+ # List detected extensions prioritized
185
+ for ext in types.detect('application/json, text/xml;q=0.1'):
186
+ print(ext)
187
+ """
188
+ # Extract and prioritize MIME types
189
+ types: dict[str, float] = {}
190
+ for part in (accept or '').split(','):
191
+ if m := _ACCEPT_RE.match(part):
192
+ types[m[1].lower()] = float(m[2]) if m[2] is not None else 1
193
+ detected = sorted(sorted(types), key=lambda type: types[type], reverse=True)
194
+
195
+ # Detect extensions from MIME types
196
+ reverse: dict[str, list[str]] = {}
197
+ mapping = self.mapping
198
+ for ext in sorted(mapping):
199
+ for type in mapping[ext]:
200
+ reverse.setdefault(type.partition(';')[0].lower(), []).append(ext)
201
+
202
+ return [ext for type in detected for ext in reverse.get(type, [])]
203
+
204
+ def file_type(self, file: str | os.PathLike[str]) -> str | None:
205
+ """::
206
+
207
+ type = types.file_type('foo/bar.png')
208
+
209
+ Get MIME type for file path.
210
+ """
211
+ return self.type(Pyjo.File.path(file).extname)
212
+
213
+ @overload
214
+ def type(self, ext: str) -> str | None: ...
215
+
216
+ @overload
217
+ def type(self, ext: str, type: str | list[str]) -> Self: ...
218
+
219
+ def type(self, ext: str, type: str | list[str] | None = None) -> str | Self | None:
220
+ """::
221
+
222
+ type = types.type('png')
223
+ types = types.type('png', 'image/png')
224
+ types = types.type('json', ['application/json', 'text/x-json'])
225
+
226
+ Get or set MIME types for file extension, alternatives are only used for
227
+ detection.
228
+ """
229
+ if type is None:
230
+ types = self.mapping.get(ext.lower())
231
+ return types[0] if types else None
232
+ self.mapping[ext.lower()] = type if isinstance(type, list) else [type]
233
+ return self
234
+
235
+
236
+ new = Pyjoyment_Types.new
237
+ object = Pyjoyment_Types
238
+ Types = Pyjoyment_Types