quart-assets 0.1.0.dev9__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.
- quart_assets/__init__.py +32 -0
- quart_assets/extension.py +498 -0
- quart_assets-0.1.0.dev9.dist-info/LICENSE +28 -0
- quart_assets-0.1.0.dev9.dist-info/METADATA +65 -0
- quart_assets-0.1.0.dev9.dist-info/RECORD +7 -0
- quart_assets-0.1.0.dev9.dist-info/WHEEL +4 -0
- quart_assets-0.1.0.dev9.dist-info/entry_points.txt +3 -0
quart_assets/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import print_function
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from quart.globals import app_ctx, request_ctx
|
|
5
|
+
except ImportError:
|
|
6
|
+
from quart import _app_ctx_stack, _request_ctx_stack # type: ignore
|
|
7
|
+
|
|
8
|
+
request_ctx = _request_ctx_stack.top
|
|
9
|
+
app_ctx = _app_ctx_stack.top
|
|
10
|
+
|
|
11
|
+
from webassets import Bundle # type: ignore[import-untyped] # noqa F401
|
|
12
|
+
|
|
13
|
+
from .extension import (
|
|
14
|
+
assets,
|
|
15
|
+
AsyncAssetsExtension,
|
|
16
|
+
Jinja2Filter,
|
|
17
|
+
QuartAssets,
|
|
18
|
+
QuartConfigStorage,
|
|
19
|
+
QuartResolver,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0.dev9"
|
|
23
|
+
|
|
24
|
+
__all__ = (
|
|
25
|
+
"assets",
|
|
26
|
+
"Bundle",
|
|
27
|
+
"QuartAssets",
|
|
28
|
+
"QuartConfigStorage",
|
|
29
|
+
"QuartResolver",
|
|
30
|
+
"Jinja2Filter",
|
|
31
|
+
"AsyncAssetsExtension",
|
|
32
|
+
)
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
"""Integration of the ``webassets`` library with Quart."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
from os import path
|
|
7
|
+
from typing import Any, Dict, Optional, Tuple, Union
|
|
8
|
+
|
|
9
|
+
from quart import current_app, has_app_context, has_request_context
|
|
10
|
+
from quart.app import Quart
|
|
11
|
+
from quart.cli import pass_script_info, ScriptInfo
|
|
12
|
+
from quart.globals import app_ctx, request_ctx
|
|
13
|
+
from quart.templating import render_template_string
|
|
14
|
+
from webassets.env import ( # type: ignore[import-untyped]
|
|
15
|
+
BaseEnvironment,
|
|
16
|
+
ConfigStorage,
|
|
17
|
+
env_options,
|
|
18
|
+
Resolver,
|
|
19
|
+
)
|
|
20
|
+
from webassets.ext.jinja2 import AssetsExtension # type: ignore[import-untyped]
|
|
21
|
+
from webassets.filter import Filter, register_filter # type: ignore[import-untyped]
|
|
22
|
+
from webassets.loaders import PythonLoader, YAMLLoader # type: ignore[import-untyped]
|
|
23
|
+
from webassets.script import CommandLineEnvironment # type: ignore[import-untyped]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_static_folder(app_or_blueprint: Any) -> str:
|
|
27
|
+
"""Return the static folder of the given Quart app
|
|
28
|
+
instance, or module/blueprint.
|
|
29
|
+
"""
|
|
30
|
+
if not app_or_blueprint.has_static_folder:
|
|
31
|
+
# Use an exception type here that is not hidden by spit_prefix.
|
|
32
|
+
raise TypeError(f"The referenced blueprint {app_or_blueprint} has no static " "folder.")
|
|
33
|
+
return app_or_blueprint.static_folder
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AsyncAssetsExtension(AssetsExtension): # type: ignore[misc]
|
|
37
|
+
"""An async-aware version of the webassets Jinja2 extension that supports async coroutines."""
|
|
38
|
+
|
|
39
|
+
def _render_assets(
|
|
40
|
+
self, filter: Any, output: Any, dbg: Any, depends: Any, files: Any, caller: Any = None
|
|
41
|
+
) -> str:
|
|
42
|
+
env = self.environment.assets_environment # pyright: ignore[reportAttributeAccessIssue]
|
|
43
|
+
if env is None:
|
|
44
|
+
raise RuntimeError("No assets environment configured in " + "Jinja2 environment")
|
|
45
|
+
|
|
46
|
+
# Construct a bundle with the given options
|
|
47
|
+
bundle_kwargs = {
|
|
48
|
+
"output": output,
|
|
49
|
+
"filters": filter,
|
|
50
|
+
"debug": dbg,
|
|
51
|
+
"depends": depends,
|
|
52
|
+
}
|
|
53
|
+
bundle = self.BundleClass(*self.resolve_contents(files, env), **bundle_kwargs)
|
|
54
|
+
|
|
55
|
+
# Retrieve urls (this may or may not cause a build)
|
|
56
|
+
with bundle.bind(env):
|
|
57
|
+
urls = bundle.urls(calculate_sri=True)
|
|
58
|
+
|
|
59
|
+
# For each url, execute the content of this template tag (represented
|
|
60
|
+
# by the macro ```caller`` given to use by Jinja2).
|
|
61
|
+
result = ""
|
|
62
|
+
for entry in urls:
|
|
63
|
+
if isinstance(entry, dict):
|
|
64
|
+
caller_result = caller(entry["uri"], entry.get("sri", None), bundle.extra)
|
|
65
|
+
else:
|
|
66
|
+
caller_result = caller(entry, None, bundle.extra)
|
|
67
|
+
|
|
68
|
+
# Check if the caller returned a coroutine (async context)
|
|
69
|
+
if inspect.iscoroutine(caller_result):
|
|
70
|
+
# The caller returned a coroutine, which means we're in an
|
|
71
|
+
# async template context
|
|
72
|
+
# We need to run this coroutine to completion
|
|
73
|
+
try:
|
|
74
|
+
# Use a different approach: run the coroutine in a new
|
|
75
|
+
# thread with its own event loop
|
|
76
|
+
import queue
|
|
77
|
+
import threading
|
|
78
|
+
|
|
79
|
+
result_queue: "queue.Queue[Any]" = queue.Queue()
|
|
80
|
+
exception_queue: "queue.Queue[Exception]" = queue.Queue()
|
|
81
|
+
|
|
82
|
+
def run_coroutine() -> None:
|
|
83
|
+
"""Run the coroutine in a separate thread with a new event loop."""
|
|
84
|
+
new_loop = None
|
|
85
|
+
try:
|
|
86
|
+
# Create a new event loop for this thread
|
|
87
|
+
new_loop = asyncio.new_event_loop()
|
|
88
|
+
asyncio.set_event_loop(new_loop)
|
|
89
|
+
|
|
90
|
+
# Run the coroutine
|
|
91
|
+
result = new_loop.run_until_complete(caller_result)
|
|
92
|
+
result_queue.put(result)
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
exception_queue.put(e)
|
|
96
|
+
finally:
|
|
97
|
+
if new_loop is not None:
|
|
98
|
+
try:
|
|
99
|
+
new_loop.close()
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
# Start the thread
|
|
104
|
+
thread = threading.Thread(target=run_coroutine, daemon=True)
|
|
105
|
+
thread.start()
|
|
106
|
+
|
|
107
|
+
# Wait for the result with a timeout
|
|
108
|
+
thread.join(timeout=5.0)
|
|
109
|
+
|
|
110
|
+
if thread.is_alive():
|
|
111
|
+
# Thread is still running, this is a timeout
|
|
112
|
+
raise RuntimeError("Timeout waiting for async template rendering")
|
|
113
|
+
|
|
114
|
+
# Check for exceptions
|
|
115
|
+
if not exception_queue.empty():
|
|
116
|
+
raise exception_queue.get()
|
|
117
|
+
|
|
118
|
+
# Get the result
|
|
119
|
+
if not result_queue.empty():
|
|
120
|
+
caller_result = result_queue.get()
|
|
121
|
+
else:
|
|
122
|
+
raise RuntimeError("No result received from async template rendering")
|
|
123
|
+
|
|
124
|
+
except RuntimeError as e:
|
|
125
|
+
if "no running event loop" in str(e):
|
|
126
|
+
# No event loop running, close the coroutine
|
|
127
|
+
caller_result.close()
|
|
128
|
+
raise RuntimeError(
|
|
129
|
+
"Cannot handle async template rendering without an event loop"
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
raise
|
|
133
|
+
except Exception as e:
|
|
134
|
+
# Clean up the coroutine
|
|
135
|
+
if inspect.iscoroutine(caller_result):
|
|
136
|
+
caller_result.close()
|
|
137
|
+
raise RuntimeError(f"Error handling async template rendering: {e}")
|
|
138
|
+
|
|
139
|
+
result += caller_result
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
__all__ = ["Jinja2Filter"]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class Jinja2Filter(Filter): # type: ignore[misc]
|
|
147
|
+
"""Compiles all source files as Jinja2 templates using Quart contexts."""
|
|
148
|
+
|
|
149
|
+
name = "jinja2"
|
|
150
|
+
max_debug_level = None
|
|
151
|
+
|
|
152
|
+
def __init__(self, context: Optional[Dict[str, Any]] = None) -> None:
|
|
153
|
+
super().__init__()
|
|
154
|
+
self.context = context or {}
|
|
155
|
+
|
|
156
|
+
def input(self, _in: Any, out: Any, **kw: Any) -> None:
|
|
157
|
+
out.write(render_template_string(_in.read(), **self.context))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class QuartConfigStorage(ConfigStorage): # type: ignore[misc]
|
|
161
|
+
"""Uses the config object of a Quart app as the backend: either the app
|
|
162
|
+
instance bound to the extension directly, or the current Quart app on
|
|
163
|
+
the stack. Also provides per-application defaults for some values.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def __init__(self, *a: Any, **kw: Any) -> None:
|
|
167
|
+
self._defaults: Dict[str, Any] = {}
|
|
168
|
+
ConfigStorage.__init__(self, *a, **kw)
|
|
169
|
+
|
|
170
|
+
def _transform_key(self, key: str) -> str:
|
|
171
|
+
if key.lower() in env_options:
|
|
172
|
+
return f"ASSETS_{key.upper()}"
|
|
173
|
+
|
|
174
|
+
return key.upper()
|
|
175
|
+
|
|
176
|
+
def setdefault(self, key: str, value: Any) -> None:
|
|
177
|
+
"""We may not always be connected to an app, but we still need
|
|
178
|
+
to provide a way to the base environment to set its defaults.
|
|
179
|
+
"""
|
|
180
|
+
try:
|
|
181
|
+
super().setdefault(key, value)
|
|
182
|
+
except RuntimeError:
|
|
183
|
+
self._defaults[key] = value
|
|
184
|
+
|
|
185
|
+
def __contains__(self, key: str) -> bool:
|
|
186
|
+
return self._transform_key(key) in self.env._app.config
|
|
187
|
+
|
|
188
|
+
def __getitem__(self, key: str) -> Any:
|
|
189
|
+
value = self._get_deprecated(key)
|
|
190
|
+
if value:
|
|
191
|
+
return value
|
|
192
|
+
|
|
193
|
+
# First try the current app's config
|
|
194
|
+
public_key = self._transform_key(key)
|
|
195
|
+
if self.env._app:
|
|
196
|
+
if public_key in self.env._app.config:
|
|
197
|
+
return self.env._app.config[public_key]
|
|
198
|
+
|
|
199
|
+
# Try a non-app specific default value
|
|
200
|
+
if key in self._defaults:
|
|
201
|
+
return self._defaults.__getitem__(key)
|
|
202
|
+
|
|
203
|
+
# Finally try to use a default based on the current app
|
|
204
|
+
deffunc = getattr(self, f"_app_default_{key}", None)
|
|
205
|
+
if deffunc and callable(deffunc):
|
|
206
|
+
return deffunc()
|
|
207
|
+
|
|
208
|
+
# We've run out of options
|
|
209
|
+
raise KeyError()
|
|
210
|
+
|
|
211
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
212
|
+
if not self._set_deprecated(key, value):
|
|
213
|
+
self.env._app.config[self._transform_key(key)] = value
|
|
214
|
+
|
|
215
|
+
def __delitem__(self, key: str) -> None:
|
|
216
|
+
del self.env._app.config[self._transform_key(key)]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class QuartResolver(Resolver): # type: ignore[misc]
|
|
220
|
+
"""Adds support for Quart blueprints.
|
|
221
|
+
|
|
222
|
+
This resolver is designed to use the Quart staticfile system to
|
|
223
|
+
locate files, by looking at directory prefixes. (``foo/bar.png``
|
|
224
|
+
looks in the static folder of the ``foo`` blueprint. ``url_for``
|
|
225
|
+
is used to generate urls to these files.)
|
|
226
|
+
|
|
227
|
+
This default behaviour changes when you start setting certain
|
|
228
|
+
standard *webassets* path and url configuration values:
|
|
229
|
+
|
|
230
|
+
If a :attr:`Environment.directory` is set, output files will
|
|
231
|
+
always be written there, while source files still use the Quart
|
|
232
|
+
system.
|
|
233
|
+
|
|
234
|
+
If a :attr:`Environment.load_path` is set, it is used to look
|
|
235
|
+
up source files, replacing the Quart system. Blueprint prefixes
|
|
236
|
+
are no longer resolved.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
def split_prefix(self, ctx: Any, item: str) -> Tuple[str, str, str]:
|
|
240
|
+
"""See if ``item`` has blueprint prefix, return (directory, rel_path)."""
|
|
241
|
+
app = ctx._app
|
|
242
|
+
directory = ""
|
|
243
|
+
endpoint = ""
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
if hasattr(app, "blueprints"):
|
|
247
|
+
blueprint, name = item.split("/", 1)
|
|
248
|
+
directory = get_static_folder(app.blueprints[blueprint])
|
|
249
|
+
endpoint = "%s.static" % blueprint
|
|
250
|
+
item = name
|
|
251
|
+
else:
|
|
252
|
+
# No blueprints support, use app static
|
|
253
|
+
directory = get_static_folder(app)
|
|
254
|
+
endpoint = "static"
|
|
255
|
+
except (ValueError, KeyError):
|
|
256
|
+
directory = get_static_folder(app)
|
|
257
|
+
endpoint = "static"
|
|
258
|
+
|
|
259
|
+
return directory, item, endpoint
|
|
260
|
+
|
|
261
|
+
def use_webassets_system_for_output(self, ctx: Any) -> bool:
|
|
262
|
+
return ctx.config.get("directory") is not None or ctx.config.get("url") is not None
|
|
263
|
+
|
|
264
|
+
def use_webassets_system_for_sources(self, ctx: Any) -> bool:
|
|
265
|
+
return bool(ctx.load_path)
|
|
266
|
+
|
|
267
|
+
def search_for_source(self, ctx: Any, item: str) -> Any:
|
|
268
|
+
# If a load_path is set, use it instead of the Quart static system.
|
|
269
|
+
#
|
|
270
|
+
# Note: With only env.directory set, we don't go to default;
|
|
271
|
+
# Setting env.directory only makes the output directory fixed.
|
|
272
|
+
if self.use_webassets_system_for_sources(ctx):
|
|
273
|
+
return Resolver.search_for_source(self, ctx, item)
|
|
274
|
+
|
|
275
|
+
# Look in correct blueprint's directory
|
|
276
|
+
directory, item, endpoint = self.split_prefix(ctx, item)
|
|
277
|
+
try:
|
|
278
|
+
return self.consider_single_directory(directory, item)
|
|
279
|
+
except IOError:
|
|
280
|
+
# XXX: Hack to make the tests pass, which are written to not
|
|
281
|
+
# expect an IOError upon missing files. They need to be rewritten.
|
|
282
|
+
return path.normpath(path.join(directory, item))
|
|
283
|
+
|
|
284
|
+
def resolve_output_to_path(self, ctx: Any, target: str, bundle: Any) -> Any:
|
|
285
|
+
# If a directory/url pair is set, always use it for output files
|
|
286
|
+
if self.use_webassets_system_for_output(ctx):
|
|
287
|
+
return Resolver.resolve_output_to_path(self, ctx, target, bundle)
|
|
288
|
+
|
|
289
|
+
# Allow targeting blueprint static folders
|
|
290
|
+
directory, rel_path, endpoint = self.split_prefix(ctx, target)
|
|
291
|
+
return path.normpath(path.join(directory, rel_path))
|
|
292
|
+
|
|
293
|
+
def resolve_source_to_url(self, ctx: Any, filepath: str, item: str) -> str:
|
|
294
|
+
# If a load path is set, use it instead of the Quart static system.
|
|
295
|
+
if self.use_webassets_system_for_sources(ctx):
|
|
296
|
+
return super().resolve_source_to_url(ctx, filepath, item)
|
|
297
|
+
|
|
298
|
+
return self.convert_item_to_quart_url(ctx, item, filepath)
|
|
299
|
+
|
|
300
|
+
def resolve_output_to_url(self, ctx: Any, target: str) -> str:
|
|
301
|
+
# With a directory/url pair set, use it for output files.
|
|
302
|
+
if self.use_webassets_system_for_output(ctx):
|
|
303
|
+
return Resolver.resolve_output_to_url(self, ctx, target)
|
|
304
|
+
|
|
305
|
+
# Otherwise, behaves like all other Quart URLs.
|
|
306
|
+
return self.convert_item_to_quart_url(ctx, target)
|
|
307
|
+
|
|
308
|
+
def convert_item_to_quart_url(self, ctx: Any, item: str, filepath: Optional[str] = None) -> str:
|
|
309
|
+
"""Given a relative reference like `foo/bar.css`, returns
|
|
310
|
+
the Quart static url. By doing so it takes into account
|
|
311
|
+
blueprints, i.e. in the aformentioned example,
|
|
312
|
+
``foo`` may reference a blueprint.
|
|
313
|
+
|
|
314
|
+
If an absolute path is given via ``filepath``, it will be
|
|
315
|
+
used instead. This is needed because ``item`` may be a
|
|
316
|
+
glob instruction that was resolved to multiple files.
|
|
317
|
+
"""
|
|
318
|
+
from quart import has_request_context, url_for
|
|
319
|
+
|
|
320
|
+
directory, rel_path, endpoint = self.split_prefix(ctx, item)
|
|
321
|
+
|
|
322
|
+
if filepath is not None:
|
|
323
|
+
filename = filepath[len(directory) + 1 :]
|
|
324
|
+
else:
|
|
325
|
+
filename = rel_path
|
|
326
|
+
|
|
327
|
+
# Windows compatibility
|
|
328
|
+
filename = filename.replace("\\", "/")
|
|
329
|
+
|
|
330
|
+
if has_request_context():
|
|
331
|
+
# We're already in a request context, use it directly
|
|
332
|
+
url = url_for(endpoint, filename=filename)
|
|
333
|
+
else:
|
|
334
|
+
# Fallback to manual URL construction when no request context
|
|
335
|
+
# This handles both sync and async contexts
|
|
336
|
+
app = ctx.environment._app
|
|
337
|
+
|
|
338
|
+
# Handle blueprint URLs
|
|
339
|
+
if endpoint.endswith(".static"):
|
|
340
|
+
# This is a blueprint static endpoint
|
|
341
|
+
bp_name = endpoint[:-7] # Remove '.static' suffix
|
|
342
|
+
if hasattr(app, "blueprints") and bp_name in app.blueprints:
|
|
343
|
+
bp = app.blueprints[bp_name]
|
|
344
|
+
static_url_path = getattr(bp, "static_url_path", None)
|
|
345
|
+
if static_url_path:
|
|
346
|
+
url = f"{static_url_path}/{filename}"
|
|
347
|
+
else:
|
|
348
|
+
# Use blueprint name as prefix
|
|
349
|
+
url = f"/{bp_name}/{filename}"
|
|
350
|
+
else:
|
|
351
|
+
# Fallback to app static path
|
|
352
|
+
url = f"{app.static_url_path}/{filename}"
|
|
353
|
+
else:
|
|
354
|
+
# Regular app static endpoint
|
|
355
|
+
url = f"{app.static_url_path}/{filename}"
|
|
356
|
+
|
|
357
|
+
# In some cases, url will be an absolute url with a scheme and
|
|
358
|
+
# hostname. (for example, when using werkzeug's host matching).
|
|
359
|
+
# In general, url_for() will return a http url. During assets build,
|
|
360
|
+
# we don't know yet if the assets will be served over http, https
|
|
361
|
+
# or both. Let's use // instead. url_for takes a _scheme argument,
|
|
362
|
+
# but only together with external=True, which we do not want to
|
|
363
|
+
# force every time. Further, # this _scheme argument is not able to
|
|
364
|
+
# render // - it always forces a colon.
|
|
365
|
+
if url and url.startswith("http:"):
|
|
366
|
+
url = url[5:]
|
|
367
|
+
return url
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class QuartAssets(BaseEnvironment): # type: ignore[misc]
|
|
371
|
+
"""This object is used to hold a collection of bundles and configuration.
|
|
372
|
+
|
|
373
|
+
If initialized with a Quart app instance then a webassets Jinja2 extension
|
|
374
|
+
is automatically registered.
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
config_storage_class = QuartConfigStorage
|
|
378
|
+
resolver_class = QuartResolver
|
|
379
|
+
|
|
380
|
+
def __init__(self, app: Optional[Quart] = None) -> None:
|
|
381
|
+
self.app = app
|
|
382
|
+
super().__init__()
|
|
383
|
+
if app:
|
|
384
|
+
self.init_app(app)
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def _app(self) -> Quart:
|
|
388
|
+
"""The application object; this is either the app that has been bound
|
|
389
|
+
to, or the current application.
|
|
390
|
+
"""
|
|
391
|
+
if self.app is not None:
|
|
392
|
+
return self.app
|
|
393
|
+
|
|
394
|
+
if has_request_context():
|
|
395
|
+
return request_ctx.app
|
|
396
|
+
|
|
397
|
+
if has_app_context():
|
|
398
|
+
return app_ctx.app
|
|
399
|
+
|
|
400
|
+
raise RuntimeError(
|
|
401
|
+
"Assets instance not bound to an application, "
|
|
402
|
+
+ "and no application in current context"
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
def set_directory(self, directory: str) -> None:
|
|
406
|
+
self.config["directory"] = directory
|
|
407
|
+
|
|
408
|
+
def get_directory(self) -> str:
|
|
409
|
+
if self.config.get("directory") is not None:
|
|
410
|
+
return self.config["directory"]
|
|
411
|
+
return get_static_folder(self._app)
|
|
412
|
+
|
|
413
|
+
directory = property(
|
|
414
|
+
get_directory,
|
|
415
|
+
set_directory,
|
|
416
|
+
doc="""The base directory to which all paths will be relative to.
|
|
417
|
+
""",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
def set_url(self, url: str) -> None:
|
|
421
|
+
self.config["url"] = url
|
|
422
|
+
|
|
423
|
+
def get_url(self) -> Optional[str]:
|
|
424
|
+
if self.config.get("url") is not None:
|
|
425
|
+
return self.config["url"]
|
|
426
|
+
return self._app.static_url_path
|
|
427
|
+
|
|
428
|
+
url = property(
|
|
429
|
+
get_url,
|
|
430
|
+
set_url,
|
|
431
|
+
doc="""The base url to which all static urls will be relative to.""",
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
def init_app(self, app: Quart) -> None:
|
|
435
|
+
# Use our custom async-aware extension instead of the default webassets
|
|
436
|
+
# extension
|
|
437
|
+
app.jinja_env.add_extension(AsyncAssetsExtension)
|
|
438
|
+
app.jinja_env.assets_environment = self # type: ignore[attr-defined]
|
|
439
|
+
|
|
440
|
+
def from_yaml(self, path: str) -> None:
|
|
441
|
+
"""Register bundles from a YAML configuration file"""
|
|
442
|
+
bundles = YAMLLoader(path).load_bundles()
|
|
443
|
+
for name, bundle in bundles.items():
|
|
444
|
+
self.register(name, bundle)
|
|
445
|
+
|
|
446
|
+
def from_module(self, path: Union[str, Any]) -> None:
|
|
447
|
+
"""Register bundles from a Python module"""
|
|
448
|
+
bundles = PythonLoader(path).load_bundles()
|
|
449
|
+
for name, bundle in bundles.items():
|
|
450
|
+
self.register(name, bundle)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# Override the built-in ``jinja2`` filter that ships with ``webassets``. This
|
|
454
|
+
# custom filter uses Quart's ``render_template_string`` function to provide all
|
|
455
|
+
# the standard Quart template context variables.
|
|
456
|
+
register_filter(Jinja2Filter)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
import click
|
|
461
|
+
except ImportError:
|
|
462
|
+
pass
|
|
463
|
+
else:
|
|
464
|
+
|
|
465
|
+
def _webassets_cmd(cmd: str) -> None:
|
|
466
|
+
"""Helper to run a webassets command."""
|
|
467
|
+
|
|
468
|
+
logger = logging.getLogger("webassets")
|
|
469
|
+
logger.addHandler(logging.StreamHandler())
|
|
470
|
+
logger.setLevel(logging.DEBUG)
|
|
471
|
+
cmdenv = CommandLineEnvironment(
|
|
472
|
+
current_app.jinja_env.assets_environment, logger # type: ignore[attr-defined]
|
|
473
|
+
)
|
|
474
|
+
getattr(cmdenv, cmd)()
|
|
475
|
+
|
|
476
|
+
@click.group()
|
|
477
|
+
def assets(info: ScriptInfo) -> None:
|
|
478
|
+
"""Quart Assets commands."""
|
|
479
|
+
|
|
480
|
+
@assets.command()
|
|
481
|
+
@pass_script_info
|
|
482
|
+
def build(info: ScriptInfo) -> None:
|
|
483
|
+
"""Build bundles."""
|
|
484
|
+
_webassets_cmd("build")
|
|
485
|
+
|
|
486
|
+
@assets.command()
|
|
487
|
+
@pass_script_info
|
|
488
|
+
def clean(info: ScriptInfo) -> None:
|
|
489
|
+
"""Clean bundles."""
|
|
490
|
+
_webassets_cmd("clean")
|
|
491
|
+
|
|
492
|
+
@assets.command()
|
|
493
|
+
@pass_script_info
|
|
494
|
+
def watch(info: ScriptInfo) -> None:
|
|
495
|
+
"""Watch bundles for file changes."""
|
|
496
|
+
_webassets_cmd("watch")
|
|
497
|
+
|
|
498
|
+
__all__.extend(["assets", "build", "clean", "watch"])
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Copyright (c) 2010, Michael Elsdörfer <http://elsdoerfer.name>
|
|
2
|
+
Copyright (c) 2025, Sasha Gerrand <https://sgerrand.com>
|
|
3
|
+
All rights reserved.
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions
|
|
7
|
+
are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright
|
|
10
|
+
notice, this list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above
|
|
13
|
+
copyright notice, this list of conditions and the following
|
|
14
|
+
disclaimer in the documentation and/or other materials
|
|
15
|
+
provided with the distribution.
|
|
16
|
+
|
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
18
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
19
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
20
|
+
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
21
|
+
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
22
|
+
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
23
|
+
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
24
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
25
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
26
|
+
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
27
|
+
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
28
|
+
POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: quart-assets
|
|
3
|
+
Version: 0.1.0.dev9
|
|
4
|
+
Summary: Asset management for Quart apps
|
|
5
|
+
License: BSD-2-Clause
|
|
6
|
+
Author: Sasha Gerrand
|
|
7
|
+
Author-email: quart-assets@sgerrand.dev
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Classifier: Environment :: Web Environment
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Dist: pyscss
|
|
22
|
+
Requires-Dist: pyyaml
|
|
23
|
+
Requires-Dist: quart (>=0.20.0,<0.21.0)
|
|
24
|
+
Requires-Dist: webassets (>=2.0)
|
|
25
|
+
Project-URL: Bug Tracker, https://github.com/sgerrand/quart-assets/issues
|
|
26
|
+
Project-URL: Documentation, https://quart-assets.readthedocs.io
|
|
27
|
+
Project-URL: Homepage, https://github.com/sgerrand/quart-assets
|
|
28
|
+
Project-URL: Repository, https://github.com/sgerrand/quart-assets
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Quart-Assets
|
|
32
|
+
|
|
33
|
+
[](https://github.com/sgerrand/quart-assets/actions/workflows/tests.yml)
|
|
34
|
+
[](https://opensource.org/licenses/BSD-2-Clause)
|
|
35
|
+
|
|
36
|
+
Quart-Assets is an extension for [Quart][quart] that supports merging,
|
|
37
|
+
minifying and compiling CSS and Javascript files via the
|
|
38
|
+
[`webassets`][webassets] library.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
To use Quart-Assets with a Quart app, you have to create a QuartAssets
|
|
43
|
+
instance and initialise it with the application:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from quart import Quart
|
|
47
|
+
from quart_assets import Bundle, QuartAssets
|
|
48
|
+
|
|
49
|
+
app = Quart(__name__)
|
|
50
|
+
assets = QuartAssets(app)
|
|
51
|
+
|
|
52
|
+
js_bundle = Bundle('alpine.js', 'main.js', 'utils.js',
|
|
53
|
+
filters='jsmin', output='dist/all.min.js')
|
|
54
|
+
assets.register('js_all', js_bundle)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
A bundle consists of any number of source files (it may also contain other
|
|
58
|
+
nested bundles), an output target, and a list of filters to apply.
|
|
59
|
+
|
|
60
|
+
All paths are relative to your app’s static directory, or the static directory of a Quart blueprint.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
[quart]: https://quart.palletprojects.com
|
|
64
|
+
[webassets]: https://webassets.readthedocs.io
|
|
65
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
quart_assets/__init__.py,sha256=aWpAJn8weubcT1E572HqZQ4QuKTtM2wN9SGdgPhuS-I,667
|
|
2
|
+
quart_assets/extension.py,sha256=sGs40CGRwRcJEdD_NSx54C-SGcbaoHO2p-HEFCcaANo,19060
|
|
3
|
+
quart_assets-0.1.0.dev9.dist-info/LICENSE,sha256=wQfQfoPc1OCxxvjJBE2w63iloAZo0GttMGhUNKybSCQ,1414
|
|
4
|
+
quart_assets-0.1.0.dev9.dist-info/METADATA,sha256=CeeyrtLGAaz28WrclHD3WgbRU99jqtEbn9G8MxMxPTQ,2494
|
|
5
|
+
quart_assets-0.1.0.dev9.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
6
|
+
quart_assets-0.1.0.dev9.dist-info/entry_points.txt,sha256=6wVE4T5YCpyG8cGnj5pO2NEj9eqUH1Yx_k5pKjZHjA0,45
|
|
7
|
+
quart_assets-0.1.0.dev9.dist-info/RECORD,,
|