mplbed 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.
- mplbed/__init__.py +15 -0
- mplbed/_doc_helpers.py +52 -0
- mplbed/asgi.py +121 -0
- mplbed/consts.py +2 -0
- mplbed/html/__init__.py +0 -0
- mplbed/html/_impl.py +149 -0
- mplbed/html/raw.py +15 -0
- mplbed/html/safe.py +22 -0
- mplbed/integration/_common.py +291 -0
- mplbed/integration/quart.py +213 -0
- mplbed/integration/starlette.py +191 -0
- mplbed/server/__init__.py +3 -0
- mplbed/server/_impl.py +170 -0
- mplbed/server/_utils.py +33 -0
- mplbed/webaggext/__init__.py +16 -0
- mplbed/webaggext/_impl.py +153 -0
- mplbed/webaggext/webaggext.js +160 -0
- mplbed-0.1.0.dist-info/METADATA +74 -0
- mplbed-0.1.0.dist-info/RECORD +20 -0
- mplbed-0.1.0.dist-info/WHEEL +4 -0
mplbed/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .asgi import MplbedMiddleware
|
|
2
|
+
from .html import raw as raw_html
|
|
3
|
+
from .html import safe as safe_html
|
|
4
|
+
from .integration import quart as mplbed_quart
|
|
5
|
+
from .integration import starlette as mplbed_starlette
|
|
6
|
+
from .server import mplbed_app_factory
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"mplbed_quart",
|
|
10
|
+
"mplbed_starlette",
|
|
11
|
+
"mplbed_app_factory",
|
|
12
|
+
"raw_html",
|
|
13
|
+
"safe_html",
|
|
14
|
+
"MplbedMiddleware",
|
|
15
|
+
]
|
mplbed/_doc_helpers.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from .consts import DEFAULT_PREFIX
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def doc(s):
|
|
7
|
+
def decorator(func):
|
|
8
|
+
func.__doc__ = s
|
|
9
|
+
return func
|
|
10
|
+
|
|
11
|
+
return decorator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def fdf(s):
|
|
15
|
+
"""_F_ix _D_ocstring _F_ragment for inclusion in a docstring template."""
|
|
16
|
+
from inspect import cleandoc
|
|
17
|
+
from textwrap import indent
|
|
18
|
+
|
|
19
|
+
return indent(cleandoc(s), " ").strip()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DotAccessDict(dict[str, Any]):
|
|
23
|
+
__getattr__ = dict.get
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
PARAMS_DS: Any = DotAccessDict(
|
|
27
|
+
prefix=fdf(f"""
|
|
28
|
+
prefix : str, optional
|
|
29
|
+
The URL prefix for the routes handleded by `mplbed`. Default is '{DEFAULT_PREFIX}'.
|
|
30
|
+
"""),
|
|
31
|
+
mplbed_starlette_app=lambda name="mplbed_starlette_app": fdf(f"""
|
|
32
|
+
{name} : Starlette, optional
|
|
33
|
+
The Starlette app to use for the `mplbed` routes, as returned by `mplbed_starlette_app_factory`.
|
|
34
|
+
If not provided, a new app will be created using the provided `mplbed_starlette_app_kwargs`.
|
|
35
|
+
"""),
|
|
36
|
+
mplbed_starlette_app_kwargs=lambda name="mplbed_starlette_app_kwargs": fdf(f"""
|
|
37
|
+
{name} : dict, optional
|
|
38
|
+
Keyword arguments to pass to the Mplbed app factory if `mplbed_starlette_app` is not provided.
|
|
39
|
+
"""),
|
|
40
|
+
manage_routing=fdf("""
|
|
41
|
+
manage_routing : bool, optional
|
|
42
|
+
Whether the ASGI middleware should manage routing. Default is True.
|
|
43
|
+
If you set this to False, you are responsible for routing requests to
|
|
44
|
+
the Mplbed app under the given `prefix`.
|
|
45
|
+
"""),
|
|
46
|
+
native_app=fdf("""
|
|
47
|
+
native_app : Any, optional
|
|
48
|
+
The native app, e.g. the `Starlette` or `Quart` instance, which will
|
|
49
|
+
typically be saved in cased it is needed by the specific integration,
|
|
50
|
+
e.g. for rendering templates.
|
|
51
|
+
"""),
|
|
52
|
+
)
|
mplbed/asgi.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from contextvars import ContextVar
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from starlette.applications import Starlette
|
|
5
|
+
from starlette.routing import Match, Mount
|
|
6
|
+
|
|
7
|
+
from mplbed._doc_helpers import PARAMS_DS as D
|
|
8
|
+
from mplbed._doc_helpers import doc
|
|
9
|
+
|
|
10
|
+
_native_app: ContextVar[Any] = ContextVar("_native_app", default=None)
|
|
11
|
+
_prefix_and_app: ContextVar[tuple[str, Starlette] | None] = ContextVar("_prefix_and_app", default=None)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MplbedMiddleware:
|
|
15
|
+
"""
|
|
16
|
+
ASGI middleware to sets up routing and ensures context the correct context is
|
|
17
|
+
available so that the integrations work correctly.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@doc(
|
|
21
|
+
f"""
|
|
22
|
+
Initialize the MplbedMiddleware.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
default_app : ASGI3Application
|
|
27
|
+
The default ASGI app to use (i.e. the main app, typically yours).
|
|
28
|
+
{D.prefix}
|
|
29
|
+
{D.mplbed_starlette_app("app")}
|
|
30
|
+
{D.mplbed_starlette_app_kwargs("app_kwargs")}
|
|
31
|
+
{D.manage_routing}
|
|
32
|
+
{D.native_app}
|
|
33
|
+
"""
|
|
34
|
+
)
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
default_app,
|
|
38
|
+
*,
|
|
39
|
+
prefix: str,
|
|
40
|
+
app=None,
|
|
41
|
+
app_kwargs=None,
|
|
42
|
+
manage_routing=True,
|
|
43
|
+
native_app=None,
|
|
44
|
+
):
|
|
45
|
+
from mplbed.server import mplbed_app_factory
|
|
46
|
+
|
|
47
|
+
self.main_app = default_app
|
|
48
|
+
if app is None and not manage_routing:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
"If manage_routing is False, you must construct and provide the app yourself "
|
|
51
|
+
"(otherwise how do you plan to route to it?)"
|
|
52
|
+
)
|
|
53
|
+
if app is not None:
|
|
54
|
+
self.mplbed_app = app
|
|
55
|
+
elif app_kwargs is not None:
|
|
56
|
+
self.mplbed_app = mplbed_app_factory(**app_kwargs)
|
|
57
|
+
else:
|
|
58
|
+
self.mplbed_app = mplbed_app_factory()
|
|
59
|
+
self.prefix = prefix
|
|
60
|
+
self.manage_routing = manage_routing
|
|
61
|
+
self.native_app = native_app
|
|
62
|
+
if manage_routing:
|
|
63
|
+
self.mount = Mount(prefix, self.mplbed_app)
|
|
64
|
+
|
|
65
|
+
async def __call__(self, scope, receive, send):
|
|
66
|
+
"""ASGI entry point for the middleware."""
|
|
67
|
+
with (
|
|
68
|
+
_native_app.set(self.native_app),
|
|
69
|
+
_prefix_and_app.set((self.prefix, self.mplbed_app)),
|
|
70
|
+
):
|
|
71
|
+
if self.manage_routing:
|
|
72
|
+
assert self.mount
|
|
73
|
+
match, child_scope = self.mount.matches(scope)
|
|
74
|
+
if match != Match.NONE:
|
|
75
|
+
scope.update(child_scope)
|
|
76
|
+
await self.mplbed_app(scope, receive, send)
|
|
77
|
+
return
|
|
78
|
+
await self.main_app(scope, receive, send)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_native_app():
|
|
82
|
+
"""Get the native app from the current context.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
Any
|
|
87
|
+
The native app, or None if not set.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
return _native_app.get()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_asgi_app():
|
|
94
|
+
"""Get the ASGI app from the current context.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
ASGI3Application | None
|
|
99
|
+
The ASGI app, or None if not set.
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
val = _prefix_and_app.get()
|
|
103
|
+
if val is not None:
|
|
104
|
+
_, app = val
|
|
105
|
+
return app
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def url_path_for(name, **path_params):
|
|
110
|
+
"""Get the URL path for a given route name and parameters."""
|
|
111
|
+
prefix_and_app = path_params.pop("_prefix_and_app", None)
|
|
112
|
+
if prefix_and_app is None:
|
|
113
|
+
prefix_and_app = _prefix_and_app.get()
|
|
114
|
+
if prefix_and_app is None:
|
|
115
|
+
raise RuntimeError(
|
|
116
|
+
"Missing current prefix_and_app in context! "
|
|
117
|
+
"Did you install the MlpbedMiddleware? (_prefix_and_app was not passed)"
|
|
118
|
+
)
|
|
119
|
+
prefix, app = prefix_and_app
|
|
120
|
+
path = app.url_path_for(name, **path_params)
|
|
121
|
+
return prefix + path
|
mplbed/consts.py
ADDED
mplbed/html/__init__.py
ADDED
|
File without changes
|
mplbed/html/_impl.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from mplbed.asgi import url_path_for
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def figure_html_from_id(fig_id, *, target="inline", on_close="msg_discrete", prefix_and_app=None):
|
|
5
|
+
from json import dumps
|
|
6
|
+
|
|
7
|
+
ws_uri = url_path_for("websocket", fig_id=fig_id, _prefix_and_app=prefix_and_app)
|
|
8
|
+
ws_uri_str = dumps(ws_uri)
|
|
9
|
+
download_fig_uri = url_path_for("download_fig", fig_id=fig_id, fmt="{fmt}", _prefix_and_app=prefix_and_app)
|
|
10
|
+
download_fig_uri_str = dumps(download_fig_uri)
|
|
11
|
+
container = ""
|
|
12
|
+
setup_container = ""
|
|
13
|
+
if target == "inline":
|
|
14
|
+
container = "<div></div>"
|
|
15
|
+
target_js = "document.currentScript.previousElementSibling"
|
|
16
|
+
elif target == "body":
|
|
17
|
+
target_js = "document.body"
|
|
18
|
+
elif target == "modal":
|
|
19
|
+
container = """
|
|
20
|
+
<dialog closedby="any" style="padding: 1em; margin: 0 auto;"></dialog>
|
|
21
|
+
""".strip()
|
|
22
|
+
target_js = "document.currentScript.previousElementSibling"
|
|
23
|
+
setup_container = """
|
|
24
|
+
_mpl_webaggext.mk_modal(document.currentScript.previousElementSibling, fig);
|
|
25
|
+
""".strip()
|
|
26
|
+
else:
|
|
27
|
+
raise ValueError(f"Invalid target: {target}")
|
|
28
|
+
if on_close == "remove_dialog":
|
|
29
|
+
on_close = ["remove_parent", "dialog"]
|
|
30
|
+
on_close_js = dumps(on_close)
|
|
31
|
+
create_figure = f"""
|
|
32
|
+
let fig = _mpl_webaggext.new_fig(
|
|
33
|
+
{target_js},
|
|
34
|
+
{fig_id},
|
|
35
|
+
{ws_uri_str},
|
|
36
|
+
{download_fig_uri_str},
|
|
37
|
+
{on_close_js}
|
|
38
|
+
);
|
|
39
|
+
""".strip()
|
|
40
|
+
bits = (
|
|
41
|
+
container,
|
|
42
|
+
"""
|
|
43
|
+
<script>
|
|
44
|
+
(function() {
|
|
45
|
+
""".strip(),
|
|
46
|
+
create_figure,
|
|
47
|
+
setup_container,
|
|
48
|
+
"""
|
|
49
|
+
})();
|
|
50
|
+
</script>
|
|
51
|
+
""".strip(),
|
|
52
|
+
)
|
|
53
|
+
return "\n".join(bits)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def figure_html(figure, *retains, target="inline", on_close="msg_discrete"):
|
|
57
|
+
from matplotlib.pyplot import _get_backend_mod as get_backend_mod
|
|
58
|
+
|
|
59
|
+
from mplbed.server._impl import add_manager
|
|
60
|
+
|
|
61
|
+
manager = get_backend_mod().new_figure_manager_given_figure(id(figure), figure)
|
|
62
|
+
if hasattr(manager, "add_retains"):
|
|
63
|
+
manager.add_retains(*retains) # ty: ignore
|
|
64
|
+
else:
|
|
65
|
+
if not hasattr(figure, "_retains"):
|
|
66
|
+
figure._retains = []
|
|
67
|
+
figure._retains.extend(retains)
|
|
68
|
+
add_manager(manager)
|
|
69
|
+
return figure_html_from_id(manager.num, target=target, on_close=on_close)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def default_figure_page_template(*, head, fig, title):
|
|
73
|
+
return f"""<!DOCTYPE html>
|
|
74
|
+
<html lang="en">
|
|
75
|
+
<head>
|
|
76
|
+
{head}
|
|
77
|
+
<title>{title}</title>
|
|
78
|
+
</head>
|
|
79
|
+
<body>
|
|
80
|
+
{fig}
|
|
81
|
+
</body>
|
|
82
|
+
</html>
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_template_preview = default_figure_page_template(head="{head}", fig="{fig}", title="{title}")
|
|
87
|
+
_indented = "\n".join(" " + line for line in _template_preview.splitlines())
|
|
88
|
+
default_figure_page_template.__doc__ = (
|
|
89
|
+
"Applies the following template to the given ``head``, ``fig`` and ``title``:\n\n"
|
|
90
|
+
".. code-block:: html\n\n" + _indented + "\n"
|
|
91
|
+
)
|
|
92
|
+
del _template_preview, _indented
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def head_content(*, core=False, prefix_and_app=None):
|
|
96
|
+
css_files = []
|
|
97
|
+
if not core:
|
|
98
|
+
css_files.extend(["page", "boilerplate", "fbm"])
|
|
99
|
+
css_files.append("mpl")
|
|
100
|
+
head_bits = []
|
|
101
|
+
for css_file in css_files:
|
|
102
|
+
static_url = url_path_for("static", path=f"css/{css_file}.css", _prefix_and_app=prefix_and_app)
|
|
103
|
+
head_bits.append(
|
|
104
|
+
f"""
|
|
105
|
+
<link rel="stylesheet" href="{static_url}" type="text/css">
|
|
106
|
+
""".strip()
|
|
107
|
+
)
|
|
108
|
+
mpl_js_uri = url_path_for("mpl_js", _prefix_and_app=prefix_and_app)
|
|
109
|
+
head_bits.append(
|
|
110
|
+
f"""
|
|
111
|
+
<script src="{mpl_js_uri}"></script>
|
|
112
|
+
""".strip()
|
|
113
|
+
)
|
|
114
|
+
webaggext_js_uri = url_path_for("webaggext_js", _prefix_and_app=prefix_and_app)
|
|
115
|
+
head_bits.append(
|
|
116
|
+
f"""
|
|
117
|
+
<script src="{webaggext_js_uri}"></script>
|
|
118
|
+
""".strip()
|
|
119
|
+
)
|
|
120
|
+
head_bits.append(
|
|
121
|
+
"""
|
|
122
|
+
<style>
|
|
123
|
+
.mpl-toolbar {
|
|
124
|
+
position: relative;
|
|
125
|
+
padding-bottom: 2em;
|
|
126
|
+
}
|
|
127
|
+
.mpl-message {
|
|
128
|
+
position: absolute;
|
|
129
|
+
left: 0;
|
|
130
|
+
bottom: 0;
|
|
131
|
+
white-space: nowrap;
|
|
132
|
+
overflow-x: auto;
|
|
133
|
+
width: 100%;
|
|
134
|
+
}
|
|
135
|
+
.mpl-figure-root {
|
|
136
|
+
display: inline-flex !important;
|
|
137
|
+
flex-direction: column;
|
|
138
|
+
}
|
|
139
|
+
</style>
|
|
140
|
+
""".strip()
|
|
141
|
+
)
|
|
142
|
+
return "\n".join(head_bits)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def figure_page_html(fig, *, template=default_figure_page_template):
|
|
146
|
+
fig_html = figure_html(fig, target="body")
|
|
147
|
+
head = head_content(core=True)
|
|
148
|
+
resp_html = template(head=head, title="figure", fig=fig_html)
|
|
149
|
+
return resp_html
|
mplbed/html/raw.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from mplbed.html._impl import (
|
|
2
|
+
default_figure_page_template,
|
|
3
|
+
figure_html,
|
|
4
|
+
figure_html_from_id,
|
|
5
|
+
figure_page_html,
|
|
6
|
+
head_content,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"figure_html_from_id",
|
|
11
|
+
"figure_html",
|
|
12
|
+
"head_content",
|
|
13
|
+
"figure_page_html",
|
|
14
|
+
"default_figure_page_template",
|
|
15
|
+
]
|
mplbed/html/safe.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from mplbed.html import _impl
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _wrap_markupsafe(func):
|
|
5
|
+
def wrapper(*args, **kwargs):
|
|
6
|
+
from markupsafe import Markup
|
|
7
|
+
|
|
8
|
+
return Markup(func(*args, **kwargs))
|
|
9
|
+
|
|
10
|
+
return wrapper
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
figure_html_from_id = _wrap_markupsafe(_impl.figure_html_from_id)
|
|
14
|
+
head_content = _wrap_markupsafe(_impl.head_content)
|
|
15
|
+
figure_html = _wrap_markupsafe(_impl.figure_html)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"figure_html_from_id",
|
|
20
|
+
"figure_html",
|
|
21
|
+
"head_content",
|
|
22
|
+
]
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from frozendict import frozendict
|
|
6
|
+
|
|
7
|
+
EMPTY_DICT = frozendict()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def figure_standalone_docstring_factory(*, response_name, template_comment, is_async, params_extra):
|
|
11
|
+
async_str = "async " if is_async else ""
|
|
12
|
+
await_str = "await " if is_async else ""
|
|
13
|
+
return f"""
|
|
14
|
+
Create a {response_name} object containing the HTML for the given figure using {template_comment}.
|
|
15
|
+
|
|
16
|
+
Example
|
|
17
|
+
-------
|
|
18
|
+
|
|
19
|
+
{async_str}def my_handler(request):
|
|
20
|
+
fig = Figure()
|
|
21
|
+
return {await_str}figure_page(fig)
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
fig : matplotlib.figure.Figure or callable
|
|
26
|
+
A matplotlib Figure object or a callable that returns a Figure object.
|
|
27
|
+
{params_extra}
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def figure_page_docstring_factory(*, response_name, template_comment, is_generic, wraps, params_extra):
|
|
32
|
+
async_str = "(async) " if is_generic else ""
|
|
33
|
+
await_str = "(await) " if is_generic else ""
|
|
34
|
+
generic_note = "All usages are generic with regards to async/sync" if is_generic else ""
|
|
35
|
+
return f"""
|
|
36
|
+
Decorator to create a {response_name} object containing the HTML for the given figure using {template_comment}.
|
|
37
|
+
|
|
38
|
+
This function can be called in two ways:
|
|
39
|
+
|
|
40
|
+
1. As a decorator for a function taking any number of arguments and
|
|
41
|
+
returning a matplotlib Figure object, making it appropriate to use as a
|
|
42
|
+
decorator for view/handler function.
|
|
43
|
+
2. As a decorator for a function taking no arguments and returning a
|
|
44
|
+
matplotlib Figure object, making it appropriate to use as a decorator for a
|
|
45
|
+
figure closure within a view/handler.
|
|
46
|
+
|
|
47
|
+
{generic_note}
|
|
48
|
+
|
|
49
|
+
Internally this decorator uses the function {wraps} to generate the HTML and create the {response_name}.
|
|
50
|
+
|
|
51
|
+
Example
|
|
52
|
+
-------
|
|
53
|
+
|
|
54
|
+
# Usage 1
|
|
55
|
+
@figure_page
|
|
56
|
+
{async_str} def my_handler(request):
|
|
57
|
+
return Figure()
|
|
58
|
+
|
|
59
|
+
# Usage 2
|
|
60
|
+
{async_str} def my_handler(request):
|
|
61
|
+
# ...
|
|
62
|
+
@figure_page
|
|
63
|
+
{async_str} def create_figure():
|
|
64
|
+
return Figure()
|
|
65
|
+
return {await_str} create_figure()
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
{params_extra}
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _has_parameters(func):
|
|
74
|
+
return len(inspect.signature(func).parameters) > 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def figure_standalone_factory(
|
|
78
|
+
name,
|
|
79
|
+
generate_html,
|
|
80
|
+
response_factory,
|
|
81
|
+
*,
|
|
82
|
+
kwarg_defaults=None,
|
|
83
|
+
response_factory_kwarg_keys=(),
|
|
84
|
+
make_async=None,
|
|
85
|
+
ds_response_name="response",
|
|
86
|
+
ds_template_comment="a template",
|
|
87
|
+
ds_params_extra,
|
|
88
|
+
):
|
|
89
|
+
need_async = inspect.iscoroutinefunction(generate_html) or inspect.iscoroutinefunction(response_factory)
|
|
90
|
+
if make_async is None:
|
|
91
|
+
make_async = need_async
|
|
92
|
+
if need_async and not make_async:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
"generate_html or response_factory is async, but make_async is False. "
|
|
95
|
+
"Set make_async to True to allow async usage."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def process_kwargs(kwargs):
|
|
99
|
+
kwargs = {**(kwarg_defaults or {}), **kwargs}
|
|
100
|
+
response_factory_kwargs = {}
|
|
101
|
+
for k in response_factory_kwarg_keys:
|
|
102
|
+
if k in kwargs:
|
|
103
|
+
response_factory_kwargs[k] = kwargs.pop(k)
|
|
104
|
+
return kwargs, response_factory_kwargs
|
|
105
|
+
|
|
106
|
+
if make_async:
|
|
107
|
+
|
|
108
|
+
async def figure_standalone(fig, **kwargs):
|
|
109
|
+
generate_html_kwargs, response_factory_kwargs = process_kwargs(kwargs)
|
|
110
|
+
if inspect.iscoroutinefunction(generate_html):
|
|
111
|
+
html = await generate_html(fig, **generate_html_kwargs)
|
|
112
|
+
else:
|
|
113
|
+
html = generate_html(fig, **generate_html_kwargs)
|
|
114
|
+
if inspect.iscoroutinefunction(response_factory):
|
|
115
|
+
return await response_factory(html, **response_factory_kwargs)
|
|
116
|
+
else:
|
|
117
|
+
return response_factory(html, **response_factory_kwargs)
|
|
118
|
+
else:
|
|
119
|
+
|
|
120
|
+
def figure_standalone(fig, **kwargs):
|
|
121
|
+
generate_html_kwargs, response_factory_kwargs = process_kwargs(kwargs)
|
|
122
|
+
return response_factory(generate_html(fig, **generate_html_kwargs), **response_factory_kwargs)
|
|
123
|
+
|
|
124
|
+
figure_standalone.__name__ = name
|
|
125
|
+
figure_standalone.__doc__ = figure_standalone_docstring_factory(
|
|
126
|
+
response_name=ds_response_name,
|
|
127
|
+
template_comment=ds_template_comment,
|
|
128
|
+
is_async=make_async,
|
|
129
|
+
params_extra=ds_params_extra,
|
|
130
|
+
)
|
|
131
|
+
return figure_standalone
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def figure_page_factory(
|
|
135
|
+
name,
|
|
136
|
+
figure_standalone,
|
|
137
|
+
*,
|
|
138
|
+
ds_response_name="response",
|
|
139
|
+
ds_template_comment="a template",
|
|
140
|
+
ds_params_extra,
|
|
141
|
+
):
|
|
142
|
+
|
|
143
|
+
needs_async = inspect.iscoroutinefunction(figure_standalone)
|
|
144
|
+
|
|
145
|
+
def figure_page(inner, **figure_page_kwargs):
|
|
146
|
+
if inspect.iscoroutinefunction(inner):
|
|
147
|
+
if _has_parameters(inner):
|
|
148
|
+
# Async view style figure creating function
|
|
149
|
+
@wraps(inner)
|
|
150
|
+
async def async_view_wrapper(*args, **kwargs):
|
|
151
|
+
actual_fig = await inner(*args, **kwargs)
|
|
152
|
+
figure_standalone_kwargs: dict[str, Any] = kwargs.pop("figure_standalone_kwargs", {})
|
|
153
|
+
if needs_async:
|
|
154
|
+
return await figure_standalone(
|
|
155
|
+
actual_fig,
|
|
156
|
+
**{**figure_page_kwargs, **figure_standalone_kwargs},
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
return figure_standalone(
|
|
160
|
+
actual_fig,
|
|
161
|
+
**{**figure_page_kwargs, **figure_standalone_kwargs},
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
wrapper = async_view_wrapper
|
|
165
|
+
else:
|
|
166
|
+
# Async closure style figure creating function
|
|
167
|
+
@wraps(inner)
|
|
168
|
+
async def async_closure_wrapper(*, figure_standalone_kwargs=EMPTY_DICT):
|
|
169
|
+
actual_fig = await inner()
|
|
170
|
+
if needs_async:
|
|
171
|
+
return await figure_standalone(
|
|
172
|
+
actual_fig,
|
|
173
|
+
**{**figure_page_kwargs, **figure_standalone_kwargs},
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
return figure_standalone(
|
|
177
|
+
actual_fig,
|
|
178
|
+
**{**figure_page_kwargs, **figure_standalone_kwargs},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
wrapper = async_closure_wrapper
|
|
182
|
+
else:
|
|
183
|
+
if needs_async:
|
|
184
|
+
raise ValueError(f"Decorated function must be async for {name} (wrapping {figure_standalone.__name__})")
|
|
185
|
+
if _has_parameters(inner):
|
|
186
|
+
# View style figure creating function
|
|
187
|
+
@wraps(inner)
|
|
188
|
+
def view_wrapper(*args, **kwargs):
|
|
189
|
+
actual_fig = figure_standalone(*args, **kwargs)
|
|
190
|
+
figure_standalone_kwargs: dict[str, Any] = kwargs.pop("figure_standalone_kwargs", {})
|
|
191
|
+
return figure_page(actual_fig, **{**figure_page_kwargs, **figure_standalone_kwargs})
|
|
192
|
+
|
|
193
|
+
wrapper = view_wrapper
|
|
194
|
+
else:
|
|
195
|
+
# Closure style figure creating function
|
|
196
|
+
@wraps(inner)
|
|
197
|
+
def closure_wrapper(*, figure_standalone_kwargs=EMPTY_DICT):
|
|
198
|
+
actual_fig = figure_standalone()
|
|
199
|
+
return figure_page(actual_fig, **{**figure_page_kwargs, **figure_standalone_kwargs})
|
|
200
|
+
|
|
201
|
+
wrapper = closure_wrapper
|
|
202
|
+
wrapper.__name__ = name
|
|
203
|
+
return wrapper
|
|
204
|
+
|
|
205
|
+
figure_page.__doc__ = figure_page_docstring_factory(
|
|
206
|
+
response_name=ds_response_name,
|
|
207
|
+
template_comment=ds_template_comment,
|
|
208
|
+
wraps=figure_standalone.__name__,
|
|
209
|
+
is_generic=not needs_async,
|
|
210
|
+
params_extra=ds_params_extra,
|
|
211
|
+
)
|
|
212
|
+
return figure_page
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def mk_figure_page_variants(
|
|
216
|
+
suffix,
|
|
217
|
+
generate_html,
|
|
218
|
+
response_factory,
|
|
219
|
+
*,
|
|
220
|
+
kwarg_defaults=None,
|
|
221
|
+
response_factory_kwarg_keys=(),
|
|
222
|
+
ds_response_name="response",
|
|
223
|
+
ds_template_comment="a template",
|
|
224
|
+
ds_params_extra,
|
|
225
|
+
):
|
|
226
|
+
need_async = inspect.iscoroutinefunction(generate_html) or inspect.iscoroutinefunction(response_factory)
|
|
227
|
+
results = {}
|
|
228
|
+
standalone_args = dict(
|
|
229
|
+
generate_html=generate_html,
|
|
230
|
+
response_factory=response_factory,
|
|
231
|
+
kwarg_defaults=kwarg_defaults,
|
|
232
|
+
response_factory_kwarg_keys=response_factory_kwarg_keys,
|
|
233
|
+
ds_response_name=ds_response_name,
|
|
234
|
+
ds_template_comment=ds_template_comment,
|
|
235
|
+
ds_params_extra=ds_params_extra,
|
|
236
|
+
)
|
|
237
|
+
page_args = dict(
|
|
238
|
+
ds_response_name=ds_response_name,
|
|
239
|
+
ds_template_comment=ds_template_comment,
|
|
240
|
+
ds_params_extra=ds_params_extra,
|
|
241
|
+
)
|
|
242
|
+
if need_async:
|
|
243
|
+
figure_standalone = figure_standalone_factory(f"figure_standalone{suffix}", **standalone_args, make_async=True)
|
|
244
|
+
results[f"figure_standalone{suffix}"] = figure_standalone
|
|
245
|
+
results[f"figure_page{suffix}"] = figure_page_factory(f"figure_page{suffix}", figure_standalone, **page_args)
|
|
246
|
+
else:
|
|
247
|
+
figure_standalone = figure_standalone_factory(
|
|
248
|
+
f"figure_standalone{suffix}",
|
|
249
|
+
**standalone_args,
|
|
250
|
+
make_async=False,
|
|
251
|
+
)
|
|
252
|
+
results[f"figure_standalone{suffix}"] = figure_standalone
|
|
253
|
+
results[f"figure_page{suffix}"] = figure_page_factory(f"figure_page{suffix}", figure_standalone, **page_args)
|
|
254
|
+
figure_standalone_async = figure_standalone_factory(
|
|
255
|
+
f"figure_standalone{suffix}_async", **standalone_args, make_async=True
|
|
256
|
+
)
|
|
257
|
+
results[f"figure_standalone{suffix}_async"] = figure_standalone_async
|
|
258
|
+
results[f"figure_page{suffix}_async"] = figure_page_factory(
|
|
259
|
+
f"figure_page{suffix}_async", figure_standalone_async, **page_args
|
|
260
|
+
)
|
|
261
|
+
return results
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def setup_page_docstring(template_func):
|
|
265
|
+
from mplbed._doc_helpers import PARAMS_DS, DotAccessDict, fdf
|
|
266
|
+
|
|
267
|
+
def decorator(wrapped):
|
|
268
|
+
wrapped.__doc__ = template_func(
|
|
269
|
+
DotAccessDict(
|
|
270
|
+
**PARAMS_DS,
|
|
271
|
+
do_install_middleware=fdf("""
|
|
272
|
+
do_install_middleware : bool, optional
|
|
273
|
+
Whether to install the mplbed middleware on the given app. Default is True.
|
|
274
|
+
"""),
|
|
275
|
+
do_register_context_processor=fdf("""
|
|
276
|
+
do_register_context_processor : bool, optional
|
|
277
|
+
Whether to register the mplbed context processor on the given app. Default is True.
|
|
278
|
+
"""),
|
|
279
|
+
do_use_mpl_backend=fdf("""
|
|
280
|
+
do_use_mpl_backend : bool, optional
|
|
281
|
+
Whether to setup the matplotlib backend for rendering figures. Default is True.
|
|
282
|
+
"""),
|
|
283
|
+
use_webaggext_backend=fdf("""
|
|
284
|
+
use_webaggext_backend : bool, optional
|
|
285
|
+
Whether to use webaggext rather than the basic webagg backend for rendering figures. Default is True.
|
|
286
|
+
"""),
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
return wrapped
|
|
290
|
+
|
|
291
|
+
return decorator
|