pyview-web 0.0.25__tar.gz → 0.2.0__tar.gz
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.
Potentially problematic release.
This version of pyview-web might be problematic. Click here for more details.
- {pyview_web-0.0.25 → pyview_web-0.2.0}/PKG-INFO +8 -9
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyproject.toml +9 -8
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/__init__.py +10 -3
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/changesets/changesets.py +4 -4
- pyview_web-0.2.0/pyview/js.py +120 -0
- pyview_web-0.2.0/pyview/live_routes.py +47 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/live_socket.py +26 -5
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/live_view.py +9 -9
- pyview_web-0.2.0/pyview/meta.py +6 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/pyview.py +16 -9
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/template/__init__.py +2 -0
- pyview_web-0.2.0/pyview/template/context_processor.py +17 -0
- pyview_web-0.2.0/pyview/template/live_template.py +83 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/vendor/ibis/template.py +1 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/ws_handler.py +27 -8
- pyview_web-0.0.25/pyview/js.py +0 -34
- pyview_web-0.0.25/pyview/live_routes.py +0 -20
- pyview_web-0.0.25/pyview/template/live_template.py +0 -75
- {pyview_web-0.0.25 → pyview_web-0.2.0}/LICENSE +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/assets/js/app.js +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/assets/package-lock.json +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/assets/package.json +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/auth/__init__.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/auth/provider.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/auth/required.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/changesets/__init__.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/csrf.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/events.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/phx_message.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/secret.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/session.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/static/assets/app.js +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/template/render_diff.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/template/root_template.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/template/serializer.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/template/utils.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/test_csrf.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/uploads.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/vendor/__init__.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/vendor/flet/pubsub/__init__.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/vendor/flet/pubsub/pub_sub.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/vendor/ibis/__init__.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/vendor/ibis/compiler.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/vendor/ibis/context.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/vendor/ibis/errors.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/vendor/ibis/filters.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/vendor/ibis/loaders.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/vendor/ibis/nodes.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/vendor/ibis/tree.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/pyview/vendor/ibis/utils.py +0 -0
- {pyview_web-0.0.25 → pyview_web-0.2.0}/readme.md +0 -0
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: pyview-web
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: LiveView in Python
|
|
5
|
-
Home-page: https://pyview.rocks
|
|
6
5
|
License: MIT
|
|
7
6
|
Keywords: web,api,LiveView
|
|
8
7
|
Author: Larry Ogrodnek
|
|
9
8
|
Author-email: ogrodnek@gmail.com
|
|
10
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.10,<3.13
|
|
11
10
|
Classifier: Development Status :: 4 - Beta
|
|
12
11
|
Classifier: Environment :: Web Environment
|
|
13
12
|
Classifier: Framework :: AsyncIO
|
|
14
|
-
Classifier: Framework :: FastAPI
|
|
15
13
|
Classifier: Framework :: Pydantic
|
|
16
14
|
Classifier: Intended Audience :: Developers
|
|
17
15
|
Classifier: Intended Audience :: Information Technology
|
|
@@ -20,9 +18,9 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
20
18
|
Classifier: Operating System :: OS Independent
|
|
21
19
|
Classifier: Programming Language :: Python
|
|
22
20
|
Classifier: Programming Language :: Python :: 3
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
24
21
|
Classifier: Programming Language :: Python :: 3.10
|
|
25
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
24
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
27
25
|
Classifier: Topic :: Internet
|
|
28
26
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
@@ -36,10 +34,11 @@ Requires-Dist: APScheduler (==3.9.1.post1)
|
|
|
36
34
|
Requires-Dist: itsdangerous (>=2.1.2,<3.0.0)
|
|
37
35
|
Requires-Dist: markupsafe (>=2.1.2,<3.0.0)
|
|
38
36
|
Requires-Dist: psutil (>=5.9.4,<6.0.0)
|
|
39
|
-
Requires-Dist: pydantic (>=2.
|
|
40
|
-
Requires-Dist: starlette (==0.
|
|
41
|
-
Requires-Dist: uvicorn (==0.
|
|
37
|
+
Requires-Dist: pydantic (>=2.9.2,<3.0.0)
|
|
38
|
+
Requires-Dist: starlette (==0.40.0)
|
|
39
|
+
Requires-Dist: uvicorn (==0.30.6)
|
|
42
40
|
Requires-Dist: wsproto (==1.2.0)
|
|
41
|
+
Project-URL: Homepage, https://pyview.rocks
|
|
43
42
|
Project-URL: Repository, https://github.com/ogrodnek/pyview
|
|
44
43
|
Description-Content-Type: text/markdown
|
|
45
44
|
|
|
@@ -5,7 +5,7 @@ packages = [
|
|
|
5
5
|
{ include = "pyview" },
|
|
6
6
|
]
|
|
7
7
|
|
|
8
|
-
version = "0.0
|
|
8
|
+
version = "0.2.0"
|
|
9
9
|
description = "LiveView in Python"
|
|
10
10
|
authors = ["Larry Ogrodnek <ogrodnek@gmail.com>"]
|
|
11
11
|
license = "MIT"
|
|
@@ -29,34 +29,34 @@ classifiers = [
|
|
|
29
29
|
"Development Status :: 4 - Beta",
|
|
30
30
|
"Environment :: Web Environment",
|
|
31
31
|
"Framework :: AsyncIO",
|
|
32
|
-
"Framework :: FastAPI",
|
|
33
32
|
"Framework :: Pydantic",
|
|
34
33
|
"Intended Audience :: Developers",
|
|
35
34
|
"License :: OSI Approved :: MIT License",
|
|
36
35
|
"Programming Language :: Python :: 3 :: Only",
|
|
37
|
-
"Programming Language :: Python :: 3.9",
|
|
38
36
|
"Programming Language :: Python :: 3.10",
|
|
39
37
|
"Programming Language :: Python :: 3.11",
|
|
38
|
+
"Programming Language :: Python :: 3.12",
|
|
40
39
|
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
|
41
40
|
"Topic :: Internet :: WWW/HTTP",
|
|
42
41
|
]
|
|
43
42
|
|
|
44
43
|
[tool.poetry.dependencies]
|
|
45
|
-
python = ">=3.
|
|
46
|
-
starlette = "0.
|
|
47
|
-
uvicorn = "0.
|
|
44
|
+
python = ">=3.10,<3.13"
|
|
45
|
+
starlette = "0.40.0"
|
|
46
|
+
uvicorn = "0.30.6"
|
|
48
47
|
wsproto = "1.2.0"
|
|
49
48
|
APScheduler = "3.9.1.post1"
|
|
50
49
|
psutil = "^5.9.4"
|
|
51
50
|
markupsafe = "^2.1.2"
|
|
52
51
|
itsdangerous = "^2.1.2"
|
|
53
|
-
pydantic = "^2.
|
|
52
|
+
pydantic = "^2.9.2"
|
|
54
53
|
|
|
55
54
|
[tool.poetry.group.dev.dependencies]
|
|
56
55
|
pytest = "^7.2.0"
|
|
57
56
|
black = "24.3.0"
|
|
58
|
-
pyright = "1.1.
|
|
57
|
+
pyright = "1.1.400"
|
|
59
58
|
aiohttp = "^3.8.4"
|
|
59
|
+
pytest-cov = "^6.1.1"
|
|
60
60
|
|
|
61
61
|
[tool.poetry.group.profiling.dependencies]
|
|
62
62
|
scalene = "^1.5.19"
|
|
@@ -73,6 +73,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
73
73
|
[tool.pyright]
|
|
74
74
|
exclude = [
|
|
75
75
|
".venv",
|
|
76
|
+
"examples",
|
|
76
77
|
"examples/.venv",
|
|
77
78
|
"**/vendor",
|
|
78
79
|
"**/node_modules",
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
from pyview.live_view import LiveView
|
|
2
|
-
from pyview.live_socket import
|
|
2
|
+
from pyview.live_socket import (
|
|
3
|
+
LiveViewSocket,
|
|
4
|
+
is_connected,
|
|
5
|
+
ConnectedLiveViewSocket,
|
|
6
|
+
UnconnectedSocket,
|
|
7
|
+
)
|
|
3
8
|
from pyview.pyview import PyView, defaultRootTemplate
|
|
4
|
-
from pyview.js import
|
|
9
|
+
from pyview.js import JsCommand
|
|
5
10
|
from pyview.pyview import RootTemplateContext, RootTemplate
|
|
6
11
|
|
|
7
12
|
__all__ = [
|
|
@@ -9,7 +14,9 @@ __all__ = [
|
|
|
9
14
|
"LiveViewSocket",
|
|
10
15
|
"PyView",
|
|
11
16
|
"defaultRootTemplate",
|
|
12
|
-
"
|
|
17
|
+
"JsCommand",
|
|
13
18
|
"RootTemplateContext",
|
|
14
19
|
"RootTemplate",
|
|
20
|
+
"is_connected",
|
|
21
|
+
"ConnectedLiveViewSocket",
|
|
15
22
|
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import TypeVar, Any, Generic, Optional
|
|
1
|
+
from typing import TypeVar, Any, Generic, Optional
|
|
2
2
|
from pydantic import BaseModel, ValidationError
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from types import SimpleNamespace
|
|
@@ -8,7 +8,7 @@ Base = TypeVar("Base", bound=BaseModel)
|
|
|
8
8
|
|
|
9
9
|
@dataclass
|
|
10
10
|
class ChangeSet(Generic[Base]):
|
|
11
|
-
cls:
|
|
11
|
+
cls: type[Base]
|
|
12
12
|
changes: dict[str, Any]
|
|
13
13
|
errors: dict[str, Any]
|
|
14
14
|
valid: bool
|
|
@@ -29,7 +29,7 @@ class ChangeSet(Generic[Base]):
|
|
|
29
29
|
|
|
30
30
|
@property
|
|
31
31
|
def fields(self) -> list[str]:
|
|
32
|
-
return self.cls.
|
|
32
|
+
return list(self.cls.model_fields)
|
|
33
33
|
|
|
34
34
|
def save(self, payload: dict[str, Any]) -> Optional[Base]:
|
|
35
35
|
self.errors = {}
|
|
@@ -58,5 +58,5 @@ class ChangeSet(Generic[Base]):
|
|
|
58
58
|
self.valid = False
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
def change_set(cls:
|
|
61
|
+
def change_set(cls: type[Base]) -> ChangeSet[Base]:
|
|
62
62
|
return ChangeSet(cls, {}, {}, False)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
from pyview.vendor.ibis import filters
|
|
4
|
+
from pyview.template.context_processor import context_processor
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@context_processor
|
|
9
|
+
def add_js(meta):
|
|
10
|
+
return {"js": JsCommands([])}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@filters.register("js.add_class")
|
|
14
|
+
def js_add_class(js: "JsCommands", selector: str, *classes):
|
|
15
|
+
return js.add_class(selector, *classes)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@filters.register("js.remove_class")
|
|
19
|
+
def js_remove_class(js: "JsCommands", selector: str, *classes):
|
|
20
|
+
return js.remove_class(selector, *classes)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@filters.register("js.show")
|
|
24
|
+
def js_show(js: "JsCommands", selector: str):
|
|
25
|
+
return js.show(selector)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@filters.register("js.hide")
|
|
29
|
+
def js_hide(js: "JsCommands", selector: str):
|
|
30
|
+
return js.hide(selector)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@filters.register("js.toggle")
|
|
34
|
+
def js_toggle(js: "JsCommands", selector: str):
|
|
35
|
+
return js.toggle(selector)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@filters.register("js.dispatch")
|
|
39
|
+
def js_dispatch(js: "JsCommands", event: str, selector: str):
|
|
40
|
+
return js.dispatch(event, selector)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@filters.register("js.push")
|
|
44
|
+
def js_push(js: "JsCommands", event: str, payload: Optional[dict[str, Any]] = None):
|
|
45
|
+
return js.push(event, payload)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@filters.register("js.focus")
|
|
49
|
+
def js_focus(js: "JsCommands", selector: str):
|
|
50
|
+
return js.focus(selector)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@filters.register("js.focus_first")
|
|
54
|
+
def js_focus_first(js: "JsCommands", selector: str):
|
|
55
|
+
return js.focus_first(selector)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@filters.register("js.transition")
|
|
59
|
+
def js_transition(js: "JsCommands", selector: str, transition: str, time: int = 200):
|
|
60
|
+
return js.transition(selector, transition, time)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class JsCommand:
|
|
65
|
+
cmd: str
|
|
66
|
+
opts: dict[str, Any]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class JsCommands:
|
|
71
|
+
commands: list[JsCommand]
|
|
72
|
+
|
|
73
|
+
def add(self, cmd: JsCommand) -> "JsCommands":
|
|
74
|
+
return JsCommands(self.commands + [cmd])
|
|
75
|
+
|
|
76
|
+
def show(self, selector: str) -> "JsCommands":
|
|
77
|
+
return self.add(JsCommand("show", {"to": selector}))
|
|
78
|
+
|
|
79
|
+
def hide(self, selector: str) -> "JsCommands":
|
|
80
|
+
return self.add(JsCommand("hide", {"to": selector}))
|
|
81
|
+
|
|
82
|
+
def toggle(self, selector: str) -> "JsCommands":
|
|
83
|
+
return self.add(JsCommand("toggle", {"to": selector}))
|
|
84
|
+
|
|
85
|
+
def add_class(self, selector: str, *classes: str) -> "JsCommands":
|
|
86
|
+
return self.add(JsCommand("add_class", {"to": selector, "names": classes}))
|
|
87
|
+
|
|
88
|
+
def remove_class(self, selector: str, *classes: str) -> "JsCommands":
|
|
89
|
+
return self.add(JsCommand("remove_class", {"to": selector, "names": classes}))
|
|
90
|
+
|
|
91
|
+
def dispatch(self, event: str, selector: str) -> "JsCommands":
|
|
92
|
+
return self.add(JsCommand("dispatch", {"to": selector, "event": event}))
|
|
93
|
+
|
|
94
|
+
def push(
|
|
95
|
+
self, event: str, payload: Optional[dict[str, Any]] = None
|
|
96
|
+
) -> "JsCommands":
|
|
97
|
+
return self.add(
|
|
98
|
+
JsCommand(
|
|
99
|
+
"push", {"event": event} | ({"value": payload} if payload else {})
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def focus(self, selector: str) -> "JsCommands":
|
|
104
|
+
return self.add(JsCommand("focus", {"to": selector}))
|
|
105
|
+
|
|
106
|
+
def focus_first(self, selector: str) -> "JsCommands":
|
|
107
|
+
return self.add(JsCommand("focus_first", {"to": selector}))
|
|
108
|
+
|
|
109
|
+
def transition(
|
|
110
|
+
self, selector: str, transition: str, time: int = 200
|
|
111
|
+
) -> "JsCommands":
|
|
112
|
+
return self.add(
|
|
113
|
+
JsCommand(
|
|
114
|
+
"transition",
|
|
115
|
+
{"to": selector, "time": time, "transition": [[transition], [], []]},
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def __str__(self):
|
|
120
|
+
return json.dumps([(c.cmd, c.opts) for c in self.commands])
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from pyview.live_view import LiveView
|
|
2
|
+
from typing import Callable, Any
|
|
3
|
+
from starlette.routing import compile_path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LiveViewLookup:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.routes = [] # [(path_format, path_regex, param_convertors, lv)]
|
|
9
|
+
|
|
10
|
+
def add(self, path: str, lv: Callable[[], LiveView]):
|
|
11
|
+
path_regex, path_format, param_convertors = compile_path(path)
|
|
12
|
+
self.routes.append((path_format, path_regex, param_convertors, lv))
|
|
13
|
+
|
|
14
|
+
def get(self, path: str) -> tuple[LiveView, dict[str, Any]]:
|
|
15
|
+
# Find all matching routes
|
|
16
|
+
matches = []
|
|
17
|
+
|
|
18
|
+
for path_format, path_regex, param_convertors, lv in self.routes:
|
|
19
|
+
match_obj = path_regex.match(path)
|
|
20
|
+
if match_obj is not None:
|
|
21
|
+
params = match_obj.groupdict()
|
|
22
|
+
|
|
23
|
+
# Convert path params using Starlette's convertors
|
|
24
|
+
for param_name, convertor in param_convertors.items():
|
|
25
|
+
if param_name in params:
|
|
26
|
+
params[param_name] = convertor.convert(params[param_name])
|
|
27
|
+
|
|
28
|
+
# Store the match with its priority information
|
|
29
|
+
has_params = bool(param_convertors)
|
|
30
|
+
matches.append((lv, params, has_params))
|
|
31
|
+
|
|
32
|
+
# Sort matches by priority: static routes (has_params=False) come first
|
|
33
|
+
matches.sort(key=lambda x: x[2]) # Sort by has_params (False comes before True)
|
|
34
|
+
|
|
35
|
+
if matches:
|
|
36
|
+
lv, params, _ = matches[0]
|
|
37
|
+
return lv(), params
|
|
38
|
+
|
|
39
|
+
# Check for trailing slash
|
|
40
|
+
if path.endswith("/"):
|
|
41
|
+
try:
|
|
42
|
+
return self.get(path[:-1])
|
|
43
|
+
except ValueError:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
# No matches found
|
|
47
|
+
raise ValueError(f"No LiveView found for path: {path}")
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from starlette.websockets import WebSocket
|
|
3
3
|
import json
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import (
|
|
5
|
+
Any,
|
|
6
|
+
TypeVar,
|
|
7
|
+
Generic,
|
|
8
|
+
TYPE_CHECKING,
|
|
9
|
+
Optional,
|
|
10
|
+
Union,
|
|
11
|
+
TypeAlias,
|
|
12
|
+
TypeGuard,
|
|
13
|
+
)
|
|
5
14
|
from urllib.parse import urlencode
|
|
6
15
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
7
16
|
from pyview.vendor.flet.pubsub import PubSubHub, PubSub
|
|
8
17
|
from pyview.events import InfoEvent
|
|
9
18
|
from pyview.uploads import UploadConstraints, UploadConfig, UploadManager
|
|
19
|
+
from pyview.meta import PyViewMeta
|
|
10
20
|
import datetime
|
|
11
21
|
|
|
12
22
|
|
|
@@ -21,10 +31,14 @@ pub_sub_hub = PubSubHub()
|
|
|
21
31
|
T = TypeVar("T")
|
|
22
32
|
|
|
23
33
|
|
|
34
|
+
def is_connected(socket: LiveViewSocket[T]) -> TypeGuard["ConnectedLiveViewSocket[T]"]:
|
|
35
|
+
return socket.connected
|
|
36
|
+
|
|
37
|
+
|
|
24
38
|
class UnconnectedSocket(Generic[T]):
|
|
25
39
|
context: T
|
|
26
|
-
connected: bool = False
|
|
27
40
|
live_title: Optional[str] = None
|
|
41
|
+
connected: bool = False
|
|
28
42
|
|
|
29
43
|
def allow_upload(
|
|
30
44
|
self, upload_name: str, constraints: UploadConstraints
|
|
@@ -32,7 +46,7 @@ class UnconnectedSocket(Generic[T]):
|
|
|
32
46
|
return UploadConfig(name=upload_name, constraints=constraints)
|
|
33
47
|
|
|
34
48
|
|
|
35
|
-
class
|
|
49
|
+
class ConnectedLiveViewSocket(Generic[T]):
|
|
36
50
|
context: T
|
|
37
51
|
live_title: Optional[str] = None
|
|
38
52
|
pending_events: list[tuple[str, Any]]
|
|
@@ -49,6 +63,10 @@ class LiveViewSocket(Generic[T]):
|
|
|
49
63
|
self.pending_events = []
|
|
50
64
|
self.upload_manager = UploadManager()
|
|
51
65
|
|
|
66
|
+
@property
|
|
67
|
+
def meta(self) -> PyViewMeta:
|
|
68
|
+
return PyViewMeta()
|
|
69
|
+
|
|
52
70
|
async def subscribe(self, topic: str):
|
|
53
71
|
await self.pub_sub.subscribe_topic_async(topic, self._topic_callback_internal)
|
|
54
72
|
|
|
@@ -81,12 +99,12 @@ class LiveViewSocket(Generic[T]):
|
|
|
81
99
|
|
|
82
100
|
async def send_info(self, event: InfoEvent):
|
|
83
101
|
await self.liveview.handle_info(event, self)
|
|
84
|
-
r = await self.liveview.render(self.context)
|
|
102
|
+
r = await self.liveview.render(self.context, self.meta)
|
|
85
103
|
resp = [None, None, self.topic, "diff", self.diff(r.tree())]
|
|
86
104
|
|
|
87
105
|
try:
|
|
88
106
|
await self.websocket.send_text(json.dumps(resp))
|
|
89
|
-
except Exception
|
|
107
|
+
except Exception:
|
|
90
108
|
for id in self.scheduled_jobs:
|
|
91
109
|
print("Removing job", id)
|
|
92
110
|
scheduler.remove_job(id)
|
|
@@ -143,3 +161,6 @@ class LiveViewSocket(Generic[T]):
|
|
|
143
161
|
await self.liveview.disconnect(self)
|
|
144
162
|
except Exception:
|
|
145
163
|
pass
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
LiveViewSocket: TypeAlias = Union[ConnectedLiveViewSocket[T], UnconnectedSocket[T]]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from typing import TypeVar, Generic, Optional, Union, Any
|
|
2
|
-
from .live_socket import LiveViewSocket,
|
|
2
|
+
from .live_socket import LiveViewSocket, ConnectedLiveViewSocket
|
|
3
3
|
from pyview.template import (
|
|
4
4
|
LiveTemplate,
|
|
5
5
|
template_file,
|
|
@@ -8,11 +8,11 @@ from pyview.template import (
|
|
|
8
8
|
find_associated_file,
|
|
9
9
|
)
|
|
10
10
|
from pyview.events import InfoEvent
|
|
11
|
+
from pyview.meta import PyViewMeta
|
|
11
12
|
from urllib.parse import ParseResult
|
|
12
13
|
|
|
13
14
|
T = TypeVar("T")
|
|
14
15
|
|
|
15
|
-
AnySocket = Union[LiveViewSocket[T], UnconnectedSocket[T]]
|
|
16
16
|
Session = dict[str, Any]
|
|
17
17
|
|
|
18
18
|
# TODO: ideally this would always be a ParseResult, but we need to update push_patch
|
|
@@ -23,26 +23,26 @@ class LiveView(Generic[T]):
|
|
|
23
23
|
def __init__(self):
|
|
24
24
|
pass
|
|
25
25
|
|
|
26
|
-
async def mount(self, socket:
|
|
26
|
+
async def mount(self, socket: LiveViewSocket[T], session: Session):
|
|
27
27
|
pass
|
|
28
28
|
|
|
29
|
-
async def handle_event(self, event, payload, socket:
|
|
29
|
+
async def handle_event(self, event, payload, socket: ConnectedLiveViewSocket[T]):
|
|
30
30
|
pass
|
|
31
31
|
|
|
32
|
-
async def handle_info(self, event: InfoEvent, socket:
|
|
32
|
+
async def handle_info(self, event: InfoEvent, socket: ConnectedLiveViewSocket[T]):
|
|
33
33
|
pass
|
|
34
34
|
|
|
35
|
-
async def handle_params(self, url: URL, params, socket:
|
|
35
|
+
async def handle_params(self, url: URL, params, socket: LiveViewSocket[T]):
|
|
36
36
|
pass
|
|
37
37
|
|
|
38
|
-
async def disconnect(self, socket:
|
|
38
|
+
async def disconnect(self, socket: ConnectedLiveViewSocket[T]):
|
|
39
39
|
pass
|
|
40
40
|
|
|
41
|
-
async def render(self, assigns: T) -> RenderedContent:
|
|
41
|
+
async def render(self, assigns: T, meta: PyViewMeta) -> RenderedContent:
|
|
42
42
|
html_render = _find_render(self)
|
|
43
43
|
|
|
44
44
|
if html_render:
|
|
45
|
-
return LiveRender(html_render, assigns)
|
|
45
|
+
return LiveRender(html_render, assigns, meta)
|
|
46
46
|
|
|
47
47
|
raise NotImplementedError()
|
|
48
48
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from starlette.applications import Starlette
|
|
2
|
-
from starlette.websockets import WebSocket
|
|
3
2
|
from starlette.responses import HTMLResponse
|
|
4
3
|
from starlette.middleware.gzip import GZipMiddleware
|
|
5
|
-
from starlette.routing import Route
|
|
4
|
+
from starlette.routing import Route, WebSocketRoute
|
|
6
5
|
from starlette.requests import Request
|
|
7
6
|
import uuid
|
|
8
7
|
from urllib.parse import parse_qs, urlparse
|
|
@@ -11,6 +10,7 @@ from pyview.live_socket import UnconnectedSocket
|
|
|
11
10
|
from pyview.csrf import generate_csrf_token
|
|
12
11
|
from pyview.session import serialize_session
|
|
13
12
|
from pyview.auth import AuthProviderFactory
|
|
13
|
+
from pyview.meta import PyViewMeta
|
|
14
14
|
from .ws_handler import LiveSocketHandler
|
|
15
15
|
from .live_view import LiveView
|
|
16
16
|
from .live_routes import LiveViewLookup
|
|
@@ -31,10 +31,7 @@ class PyView(Starlette):
|
|
|
31
31
|
self.view_lookup = LiveViewLookup()
|
|
32
32
|
self.live_handler = LiveSocketHandler(self.view_lookup)
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
await self.live_handler.handle(websocket)
|
|
36
|
-
|
|
37
|
-
self.add_websocket_route("/live/websocket", live_websocket_endpoint)
|
|
34
|
+
self.routes.append(WebSocketRoute("/live/websocket", self.live_handler.handle))
|
|
38
35
|
self.add_middleware(GZipMiddleware)
|
|
39
36
|
|
|
40
37
|
def add_live_view(self, path: str, view: type[LiveView]):
|
|
@@ -53,14 +50,24 @@ async def liveview_container(
|
|
|
53
50
|
):
|
|
54
51
|
url = request.url
|
|
55
52
|
path = url.path
|
|
56
|
-
lv
|
|
53
|
+
lv, path_params = view_lookup.get(path)
|
|
57
54
|
s = UnconnectedSocket()
|
|
58
55
|
|
|
59
56
|
session = request.session if "session" in request.scope else {}
|
|
60
57
|
|
|
61
58
|
await lv.mount(s, session)
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
|
|
60
|
+
# Parse query parameters
|
|
61
|
+
query_params = parse_qs(url.query)
|
|
62
|
+
|
|
63
|
+
# Merge path parameters with query parameters
|
|
64
|
+
# Path parameters take precedence in case of conflict
|
|
65
|
+
merged_params = {**query_params, **path_params}
|
|
66
|
+
|
|
67
|
+
# Pass merged parameters to handle_params
|
|
68
|
+
await lv.handle_params(urlparse(url._url), merged_params, s)
|
|
69
|
+
|
|
70
|
+
r = await lv.render(s.context, PyViewMeta())
|
|
64
71
|
|
|
65
72
|
liveview_css = find_associated_css(lv)
|
|
66
73
|
|
|
@@ -2,6 +2,7 @@ from pyview.vendor.ibis import Template
|
|
|
2
2
|
from .live_template import LiveTemplate, template_file, RenderedContent, LiveRender
|
|
3
3
|
from .root_template import RootTemplate, RootTemplateContext, defaultRootTemplate
|
|
4
4
|
from .utils import find_associated_css, find_associated_file
|
|
5
|
+
from .context_processor import context_processor
|
|
5
6
|
|
|
6
7
|
__all__ = [
|
|
7
8
|
"Template",
|
|
@@ -14,4 +15,5 @@ __all__ = [
|
|
|
14
15
|
"defaultRootTemplate",
|
|
15
16
|
"find_associated_css",
|
|
16
17
|
"find_associated_file",
|
|
18
|
+
"context_processor",
|
|
17
19
|
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from pyview.meta import PyViewMeta
|
|
2
|
+
|
|
3
|
+
context_processors = []
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def context_processor(func):
|
|
7
|
+
context_processors.append(func)
|
|
8
|
+
return func
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def apply_context_processors(meta: PyViewMeta):
|
|
12
|
+
context = {}
|
|
13
|
+
|
|
14
|
+
for processor in context_processors:
|
|
15
|
+
context.update(processor(meta))
|
|
16
|
+
|
|
17
|
+
return context
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from pyview.vendor.ibis import Template
|
|
2
|
+
from typing import Any, Union, Protocol, Optional, ClassVar
|
|
3
|
+
from dataclasses import asdict, Field
|
|
4
|
+
from .serializer import serialize
|
|
5
|
+
import os.path
|
|
6
|
+
from pyview.template.context_processor import apply_context_processors
|
|
7
|
+
from pyview.meta import PyViewMeta
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DataclassInstance(Protocol):
|
|
11
|
+
__dataclass_fields__: ClassVar[dict[str, Field[Any]]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Assigns = Union[dict[str, Any], DataclassInstance]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# TODO: should we still support this?
|
|
18
|
+
class DictConvertable(Protocol):
|
|
19
|
+
def asdict(self) -> dict[str, Any]: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LiveTemplate:
|
|
23
|
+
t: Template
|
|
24
|
+
|
|
25
|
+
def __init__(self, template: Template):
|
|
26
|
+
self.t = template
|
|
27
|
+
|
|
28
|
+
def tree(self, assigns: Assigns, meta: PyViewMeta) -> dict[str, Any]:
|
|
29
|
+
if not isinstance(assigns, dict):
|
|
30
|
+
assigns = serialize(assigns)
|
|
31
|
+
additional_context = apply_context_processors(meta)
|
|
32
|
+
return self.t.tree(additional_context | assigns)
|
|
33
|
+
|
|
34
|
+
def render(self, assigns: Assigns, meta: PyViewMeta) -> str:
|
|
35
|
+
if not isinstance(assigns, dict):
|
|
36
|
+
assigns = asdict(assigns)
|
|
37
|
+
additional_context = apply_context_processors(meta)
|
|
38
|
+
return self.t.render(additional_context | assigns)
|
|
39
|
+
|
|
40
|
+
def text(self, assigns: Assigns, meta: PyViewMeta) -> str:
|
|
41
|
+
return self.render(assigns, meta)
|
|
42
|
+
|
|
43
|
+
def debug(self) -> str:
|
|
44
|
+
return self.t.root_node.to_str()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RenderedContent(Protocol):
|
|
48
|
+
def tree(self) -> dict[str, Any]: ...
|
|
49
|
+
|
|
50
|
+
def text(self) -> str: ...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class LiveRender:
|
|
54
|
+
def __init__(self, template: LiveTemplate, assigns: Any, meta: PyViewMeta):
|
|
55
|
+
self.template = template
|
|
56
|
+
self.assigns = assigns
|
|
57
|
+
self.meta = meta
|
|
58
|
+
|
|
59
|
+
def tree(self) -> dict[str, Any]:
|
|
60
|
+
return self.template.tree(self.assigns, self.meta)
|
|
61
|
+
|
|
62
|
+
def text(self) -> str:
|
|
63
|
+
return self.template.text(self.assigns, self.meta)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_cache = {}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def template_file(filename: str) -> Optional[LiveTemplate]:
|
|
70
|
+
"""Renders a template file with the given assigns."""
|
|
71
|
+
if not os.path.isfile(filename):
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
mtime = os.path.getmtime(filename)
|
|
75
|
+
if filename in _cache:
|
|
76
|
+
cached_mtime, cached_template = _cache[filename]
|
|
77
|
+
if cached_mtime == mtime:
|
|
78
|
+
return cached_template
|
|
79
|
+
|
|
80
|
+
with open(filename, "r") as f:
|
|
81
|
+
t = LiveTemplate(Template(f.read(), template_id=filename))
|
|
82
|
+
_cache[filename] = (mtime, t)
|
|
83
|
+
return t
|
|
@@ -10,6 +10,7 @@ class Template:
|
|
|
10
10
|
def __init__(self, template_string, template_id="UNIDENTIFIED"):
|
|
11
11
|
self.root_node = ibis.compiler.compile(template_string, template_id)
|
|
12
12
|
self.blocks = self._register_blocks(self.root_node, {})
|
|
13
|
+
self.template_id = template_id
|
|
13
14
|
|
|
14
15
|
def __str__(self):
|
|
15
16
|
return str(self.root_node)
|
|
@@ -2,7 +2,7 @@ from typing import Optional, Any
|
|
|
2
2
|
import json
|
|
3
3
|
from starlette.websockets import WebSocket, WebSocketDisconnect
|
|
4
4
|
from urllib.parse import urlparse, parse_qs
|
|
5
|
-
from pyview.live_socket import LiveViewSocket
|
|
5
|
+
from pyview.live_socket import ConnectedLiveViewSocket, LiveViewSocket
|
|
6
6
|
from pyview.live_routes import LiveViewLookup
|
|
7
7
|
from pyview.csrf import validate_csrf_token
|
|
8
8
|
from pyview.session import deserialize_session
|
|
@@ -42,16 +42,22 @@ class LiveSocketHandler:
|
|
|
42
42
|
self.myJoinId = topic
|
|
43
43
|
|
|
44
44
|
url = urlparse(payload["url"])
|
|
45
|
-
lv = self.routes.get(url.path)
|
|
45
|
+
lv, path_params = self.routes.get(url.path)
|
|
46
46
|
await self.check_auth(websocket, lv)
|
|
47
|
-
socket =
|
|
47
|
+
socket = ConnectedLiveViewSocket(websocket, topic, lv)
|
|
48
48
|
|
|
49
49
|
session = {}
|
|
50
50
|
if "session" in payload:
|
|
51
51
|
session = deserialize_session(payload["session"])
|
|
52
52
|
|
|
53
53
|
await lv.mount(socket, session)
|
|
54
|
-
|
|
54
|
+
|
|
55
|
+
# Parse query parameters and merge with path parameters
|
|
56
|
+
query_params = parse_qs(url.query)
|
|
57
|
+
merged_params = {**query_params, **path_params}
|
|
58
|
+
|
|
59
|
+
# Pass merged parameters to handle_params
|
|
60
|
+
await lv.handle_params(url, merged_params, socket)
|
|
55
61
|
|
|
56
62
|
rendered = await _render(socket)
|
|
57
63
|
|
|
@@ -75,7 +81,7 @@ class LiveSocketHandler:
|
|
|
75
81
|
self.sessions -= 1
|
|
76
82
|
|
|
77
83
|
async def handle_connected(
|
|
78
|
-
self, myJoinId, socket:
|
|
84
|
+
self, myJoinId, socket: ConnectedLiveViewSocket, prev_rendered: dict[str, Any]
|
|
79
85
|
):
|
|
80
86
|
while True:
|
|
81
87
|
message = await socket.websocket.receive()
|
|
@@ -129,7 +135,20 @@ class LiveSocketHandler:
|
|
|
129
135
|
lv = socket.liveview
|
|
130
136
|
url = urlparse(payload["url"])
|
|
131
137
|
|
|
132
|
-
|
|
138
|
+
# Extract and merge parameters
|
|
139
|
+
query_params = parse_qs(url.query)
|
|
140
|
+
path_params = {}
|
|
141
|
+
|
|
142
|
+
# We need to get path params for the new URL
|
|
143
|
+
try:
|
|
144
|
+
# TODO: I don't think this is actually going to work...
|
|
145
|
+
_, path_params = self.routes.get(url.path)
|
|
146
|
+
except ValueError:
|
|
147
|
+
pass # Handle case where the path doesn't match any route
|
|
148
|
+
|
|
149
|
+
merged_params = {**query_params, **path_params}
|
|
150
|
+
|
|
151
|
+
await lv.handle_params(url, merged_params, socket)
|
|
133
152
|
rendered = await _render(socket)
|
|
134
153
|
diff = calc_diff(prev_rendered, rendered)
|
|
135
154
|
prev_rendered = rendered
|
|
@@ -235,8 +254,8 @@ class LiveSocketHandler:
|
|
|
235
254
|
)
|
|
236
255
|
|
|
237
256
|
|
|
238
|
-
async def _render(socket:
|
|
239
|
-
rendered = (await socket.liveview.render(socket.context)).tree()
|
|
257
|
+
async def _render(socket: ConnectedLiveViewSocket):
|
|
258
|
+
rendered = (await socket.liveview.render(socket.context, socket.meta)).tree()
|
|
240
259
|
|
|
241
260
|
if socket.live_title:
|
|
242
261
|
rendered["t"] = socket.live_title
|
pyview_web-0.0.25/pyview/js.py
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from typing import Union
|
|
3
|
-
from pyview.vendor.ibis import filters
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
JsArgs = Union[tuple[str, str], tuple[str, str, list[str]]]
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@filters.register
|
|
10
|
-
def js(args: JsArgs):
|
|
11
|
-
if len(args) > 2:
|
|
12
|
-
cmd, id, names = args # type: ignore
|
|
13
|
-
return Js(cmd, id, names)
|
|
14
|
-
cmd, id = args # type: ignore
|
|
15
|
-
return Js(cmd, id)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class Js:
|
|
19
|
-
def __init__(self, cmd: str, id: str, names: list[str] = []):
|
|
20
|
-
self.cmd = cmd
|
|
21
|
-
self.id = id
|
|
22
|
-
self.names = names
|
|
23
|
-
|
|
24
|
-
def __str__(self):
|
|
25
|
-
opts = {
|
|
26
|
-
"to": self.id,
|
|
27
|
-
"time": 200,
|
|
28
|
-
"transition": [[], [], []],
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if len(self.names) > 0:
|
|
32
|
-
opts["names"] = self.names
|
|
33
|
-
|
|
34
|
-
return json.dumps([[self.cmd, opts]])
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
from pyview.live_view import LiveView
|
|
2
|
-
from typing import Callable
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class LiveViewLookup:
|
|
6
|
-
def __init__(self):
|
|
7
|
-
self.routes = {}
|
|
8
|
-
|
|
9
|
-
def add(self, path: str, lv: Callable[[], LiveView]):
|
|
10
|
-
self.routes[path] = lv
|
|
11
|
-
|
|
12
|
-
def get(self, path: str) -> LiveView:
|
|
13
|
-
lv = self.routes.get(path)
|
|
14
|
-
if not lv and path.endswith("/"):
|
|
15
|
-
lv = self.routes[path[:-1]]
|
|
16
|
-
|
|
17
|
-
if not lv:
|
|
18
|
-
raise ValueError("No LiveView found for path: " + path)
|
|
19
|
-
|
|
20
|
-
return lv()
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
from pyview.vendor.ibis import Template
|
|
2
|
-
from typing import Any, Union, Protocol, Optional
|
|
3
|
-
from dataclasses import asdict
|
|
4
|
-
from .serializer import serialize
|
|
5
|
-
import os.path
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class DictConvertable(Protocol):
|
|
9
|
-
def asdict(self) -> dict[str, Any]:
|
|
10
|
-
...
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Assigns = Union[dict[str, Any], DictConvertable]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class LiveTemplate:
|
|
17
|
-
t: Template
|
|
18
|
-
|
|
19
|
-
def __init__(self, template: Template):
|
|
20
|
-
self.t = template
|
|
21
|
-
|
|
22
|
-
def tree(self, assigns: Assigns) -> dict[str, Any]:
|
|
23
|
-
if not isinstance(assigns, dict):
|
|
24
|
-
assigns = serialize(assigns)
|
|
25
|
-
return self.t.tree(assigns)
|
|
26
|
-
|
|
27
|
-
def render(self, assigns: Assigns) -> str:
|
|
28
|
-
if not isinstance(assigns, dict):
|
|
29
|
-
assigns = asdict(assigns)
|
|
30
|
-
return self.t.render(assigns)
|
|
31
|
-
|
|
32
|
-
def text(self, assigns: Assigns) -> str:
|
|
33
|
-
return self.render(assigns)
|
|
34
|
-
|
|
35
|
-
def debug(self) -> str:
|
|
36
|
-
return self.t.root_node.to_str()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class RenderedContent(Protocol):
|
|
40
|
-
def tree(self) -> dict[str, Any]:
|
|
41
|
-
...
|
|
42
|
-
|
|
43
|
-
def text(self) -> str:
|
|
44
|
-
...
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class LiveRender:
|
|
48
|
-
def __init__(self, template: LiveTemplate, assigns: Any):
|
|
49
|
-
self.template = template
|
|
50
|
-
self.assigns = assigns
|
|
51
|
-
|
|
52
|
-
def tree(self) -> dict[str, Any]:
|
|
53
|
-
return self.template.tree(self.assigns)
|
|
54
|
-
|
|
55
|
-
def text(self) -> str:
|
|
56
|
-
return self.template.text(self.assigns)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
_cache = {}
|
|
60
|
-
|
|
61
|
-
def template_file(filename: str) -> Optional[LiveTemplate]:
|
|
62
|
-
"""Renders a template file with the given assigns."""
|
|
63
|
-
if not os.path.isfile(filename):
|
|
64
|
-
return None
|
|
65
|
-
|
|
66
|
-
mtime = os.path.getmtime(filename)
|
|
67
|
-
if filename in _cache:
|
|
68
|
-
cached_mtime, cached_template = _cache[filename]
|
|
69
|
-
if cached_mtime == mtime:
|
|
70
|
-
return cached_template
|
|
71
|
-
|
|
72
|
-
with open(filename, "r") as f:
|
|
73
|
-
t = LiveTemplate(Template(f.read()))
|
|
74
|
-
_cache[filename] = (mtime, t)
|
|
75
|
-
return t
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|