Pyjoyment 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
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
|