instaui 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.
- instaui/__init__.py +9 -0
- instaui/_helper/observable_helper.py +35 -0
- instaui/boot_info.py +43 -0
- instaui/common/jsonable.py +37 -0
- instaui/components/__init__.py +0 -0
- instaui/components/column.py +18 -0
- instaui/components/component.py +47 -0
- instaui/components/content.py +34 -0
- instaui/components/directive.py +55 -0
- instaui/components/element.py +462 -0
- instaui/components/grid.py +80 -0
- instaui/components/html/__init__.py +36 -0
- instaui/components/html/_mixins.py +34 -0
- instaui/components/html/button.py +38 -0
- instaui/components/html/checkbox.py +42 -0
- instaui/components/html/date.py +28 -0
- instaui/components/html/div.py +7 -0
- instaui/components/html/form.py +7 -0
- instaui/components/html/input.py +28 -0
- instaui/components/html/label.py +21 -0
- instaui/components/html/li.py +17 -0
- instaui/components/html/link.py +31 -0
- instaui/components/html/number.py +34 -0
- instaui/components/html/paragraph.py +19 -0
- instaui/components/html/range.py +45 -0
- instaui/components/html/select.py +93 -0
- instaui/components/html/span.py +19 -0
- instaui/components/html/ul.py +20 -0
- instaui/components/match.py +106 -0
- instaui/components/row.py +19 -0
- instaui/components/slot.py +82 -0
- instaui/components/transition_group.py +9 -0
- instaui/components/value_element.py +48 -0
- instaui/components/vfor.py +140 -0
- instaui/components/vif.py +38 -0
- instaui/consts.py +18 -0
- instaui/dependencies/__init__.py +15 -0
- instaui/dependencies/component_registrar.py +82 -0
- instaui/dependencies/installer.py +5 -0
- instaui/event/event_mixin.py +12 -0
- instaui/event/js_event.py +57 -0
- instaui/event/web_event.py +108 -0
- instaui/experimental/__init__.py +4 -0
- instaui/experimental/debug.py +48 -0
- instaui/fastapi_server/_utils.py +42 -0
- instaui/fastapi_server/_uvicorn.py +37 -0
- instaui/fastapi_server/config_router.py +60 -0
- instaui/fastapi_server/debug_mode_router.py +61 -0
- instaui/fastapi_server/event_router.py +58 -0
- instaui/fastapi_server/middlewares.py +19 -0
- instaui/fastapi_server/request_context.py +19 -0
- instaui/fastapi_server/server.py +246 -0
- instaui/fastapi_server/watch_router.py +53 -0
- instaui/handlers/_utils.py +66 -0
- instaui/handlers/computed_handler.py +42 -0
- instaui/handlers/config_handler.py +13 -0
- instaui/handlers/event_handler.py +58 -0
- instaui/handlers/watch_handler.py +57 -0
- instaui/html_tools.py +139 -0
- instaui/inject.py +33 -0
- instaui/js/__init__.py +4 -0
- instaui/js/js_output.py +15 -0
- instaui/js/lambda_func.py +35 -0
- instaui/launch_collector.py +52 -0
- instaui/page_info.py +23 -0
- instaui/runtime/__init__.py +29 -0
- instaui/runtime/_app.py +206 -0
- instaui/runtime/_inner_helper.py +9 -0
- instaui/runtime/context.py +47 -0
- instaui/runtime/dataclass.py +30 -0
- instaui/runtime/resource.py +87 -0
- instaui/runtime/scope.py +107 -0
- instaui/runtime/ui_state_scope.py +15 -0
- instaui/settings/__init__.py +4 -0
- instaui/settings/__settings.py +13 -0
- instaui/skip.py +12 -0
- instaui/spa_router/__init__.py +26 -0
- instaui/spa_router/_components.py +35 -0
- instaui/spa_router/_file_base_utils.py +264 -0
- instaui/spa_router/_functions.py +122 -0
- instaui/spa_router/_install.py +11 -0
- instaui/spa_router/_route_model.py +139 -0
- instaui/spa_router/_router_box.py +40 -0
- instaui/spa_router/_router_output.py +22 -0
- instaui/spa_router/_router_param_var.py +51 -0
- instaui/spa_router/_types.py +4 -0
- instaui/spa_router/templates/page_routes +59 -0
- instaui/static/insta-ui.css +1 -0
- instaui/static/insta-ui.esm-browser.prod.js +3663 -0
- instaui/static/insta-ui.iife.js +29 -0
- instaui/static/insta-ui.iife.js.map +1 -0
- instaui/static/insta-ui.js.map +1 -0
- instaui/static/tailwindcss.min.js +62 -0
- instaui/static/templates/debug/sse.html +117 -0
- instaui/static/templates/web.html +118 -0
- instaui/static/templates/zero.html +55 -0
- instaui/static/vue.esm-browser.prod.js +9 -0
- instaui/static/vue.global.prod.js +9 -0
- instaui/static/vue.runtime.esm-browser.prod.js +5 -0
- instaui/systems/file_system.py +17 -0
- instaui/systems/func_system.py +104 -0
- instaui/systems/js_system.py +22 -0
- instaui/systems/pydantic_system.py +27 -0
- instaui/systems/string_system.py +10 -0
- instaui/template/__init__.py +4 -0
- instaui/template/env.py +7 -0
- instaui/template/web_template.py +55 -0
- instaui/template/zero_template.py +24 -0
- instaui/ui/__init__.py +121 -0
- instaui/ui/events.py +25 -0
- instaui/ui_functions/input_slient_data.py +16 -0
- instaui/ui_functions/server.py +13 -0
- instaui/ui_functions/str_format.py +36 -0
- instaui/ui_functions/ui_page.py +31 -0
- instaui/ui_functions/ui_types.py +13 -0
- instaui/ui_functions/url_location.py +33 -0
- instaui/vars/__init__.py +13 -0
- instaui/vars/_types.py +8 -0
- instaui/vars/_utils.py +12 -0
- instaui/vars/data.py +68 -0
- instaui/vars/element_ref.py +42 -0
- instaui/vars/event_context.py +45 -0
- instaui/vars/event_extend.py +0 -0
- instaui/vars/js_computed.py +95 -0
- instaui/vars/mixin_types/common_type.py +5 -0
- instaui/vars/mixin_types/element_binding.py +10 -0
- instaui/vars/mixin_types/observable.py +7 -0
- instaui/vars/mixin_types/pathable.py +14 -0
- instaui/vars/mixin_types/py_binding.py +13 -0
- instaui/vars/mixin_types/str_format_binding.py +8 -0
- instaui/vars/mixin_types/var_type.py +5 -0
- instaui/vars/path_var.py +89 -0
- instaui/vars/ref.py +103 -0
- instaui/vars/slot_prop.py +46 -0
- instaui/vars/state.py +82 -0
- instaui/vars/types.py +24 -0
- instaui/vars/vfor_item.py +204 -0
- instaui/vars/vue_computed.py +82 -0
- instaui/vars/web_computed.py +157 -0
- instaui/vars/web_view_computed.py +1 -0
- instaui/version.py +3 -0
- instaui/watch/_types.py +4 -0
- instaui/watch/_utils.py +3 -0
- instaui/watch/js_watch.py +74 -0
- instaui/watch/vue_watch.py +61 -0
- instaui/watch/web_watch.py +123 -0
- instaui/zero/__init__.py +3 -0
- instaui/zero/scope.py +9 -0
- instaui-0.1.0.dist-info/LICENSE +21 -0
- instaui-0.1.0.dist-info/METADATA +154 -0
- instaui-0.1.0.dist-info/RECORD +152 -0
- instaui-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,87 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Any, ClassVar, Dict, List, Optional, Set, Union
|
4
|
+
from .dataclass import JsLink, VueAppUse, VueAppComponent
|
5
|
+
|
6
|
+
|
7
|
+
class HtmlResource:
|
8
|
+
_default_css_links: ClassVar[Dict[Union[str, Path], Any]] = {}
|
9
|
+
_default_style_tags: ClassVar[List[str]] = []
|
10
|
+
_default_js_links: ClassVar[List[JsLink]] = []
|
11
|
+
_default_script_tags: ClassVar[List[str]] = []
|
12
|
+
_default_vue_app_use: ClassVar[Set[VueAppUse]] = set()
|
13
|
+
_default_vue_app_components: ClassVar[Set[VueAppComponent]] = set()
|
14
|
+
use_tailwind: bool = False
|
15
|
+
_title: str = ""
|
16
|
+
|
17
|
+
def __init__(self) -> None:
|
18
|
+
self._css_links: Dict[Union[str, Path], Any] = self._default_css_links.copy()
|
19
|
+
self._style_tags: List[str] = self._default_style_tags.copy()
|
20
|
+
self._js_links: List[JsLink] = self._default_js_links.copy()
|
21
|
+
self._script_tags: List[str] = self._default_script_tags.copy()
|
22
|
+
self._vue_app_use: Set[VueAppUse] = self._default_vue_app_use.copy()
|
23
|
+
self._vue_app_components: Set[VueAppComponent] = (
|
24
|
+
self._default_vue_app_components.copy()
|
25
|
+
)
|
26
|
+
self._import_maps: Dict[str, str] = {}
|
27
|
+
self.title: str = self._title
|
28
|
+
self._appConfig = "{}"
|
29
|
+
|
30
|
+
def add_css_link(self, link: Union[str, Path]):
|
31
|
+
self._css_links[link] = None
|
32
|
+
|
33
|
+
def add_style_tag(self, content: str):
|
34
|
+
self._style_tags.append(content)
|
35
|
+
|
36
|
+
def add_js_link(
|
37
|
+
self,
|
38
|
+
link: Union[str, Path],
|
39
|
+
*,
|
40
|
+
attrs: Optional[Dict[str, Any]] = None,
|
41
|
+
insert_before: int = -1,
|
42
|
+
):
|
43
|
+
if insert_before == -1:
|
44
|
+
self._js_links.append(JsLink(link, attrs or {}))
|
45
|
+
return
|
46
|
+
self._js_links.insert(insert_before, JsLink(link, attrs or {}))
|
47
|
+
|
48
|
+
def add_script_tag(self, content: str):
|
49
|
+
self._script_tags.append(content)
|
50
|
+
|
51
|
+
def add_vue_app_use(self, name: str):
|
52
|
+
self._vue_app_use.add(VueAppUse(name))
|
53
|
+
|
54
|
+
def add_vue_app_component(self, name: str, url: str):
|
55
|
+
self._vue_app_components.add(VueAppComponent(name, url))
|
56
|
+
|
57
|
+
def add_import_map(self, name: str, link: str):
|
58
|
+
self._import_maps[name] = link
|
59
|
+
|
60
|
+
@classmethod
|
61
|
+
def default_css_link(cls, link: Union[str, Path]):
|
62
|
+
cls._default_css_links[link] = None
|
63
|
+
|
64
|
+
@classmethod
|
65
|
+
def default_style_tag(cls, content: str):
|
66
|
+
cls._default_style_tags.append(content)
|
67
|
+
|
68
|
+
@classmethod
|
69
|
+
def default_js_link(
|
70
|
+
cls,
|
71
|
+
link: Union[str, Path],
|
72
|
+
*,
|
73
|
+
attrs: Optional[Dict[str, Any]] = None,
|
74
|
+
insert_before: int = -1,
|
75
|
+
):
|
76
|
+
if insert_before == -1:
|
77
|
+
cls._default_js_links.append(JsLink(link, attrs or {}))
|
78
|
+
return
|
79
|
+
cls._default_js_links.insert(insert_before, JsLink(link, attrs or {}))
|
80
|
+
|
81
|
+
@classmethod
|
82
|
+
def default_script_tag(cls, content: str):
|
83
|
+
cls._default_script_tags.append(content)
|
84
|
+
|
85
|
+
@classmethod
|
86
|
+
def default_vue_app_use(cls, name: str):
|
87
|
+
cls._default_vue_app_use.add(VueAppUse(name))
|
instaui/runtime/scope.py
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING, List
|
4
|
+
from instaui.common.jsonable import Jsonable
|
5
|
+
|
6
|
+
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from instaui.vars.mixin_types.var_type import VarMixin
|
9
|
+
from instaui.vars.mixin_types.py_binding import CanInputMixin
|
10
|
+
from instaui.vars.web_computed import WebComputed
|
11
|
+
from instaui.watch.web_watch import WebWatch
|
12
|
+
from instaui.watch.js_watch import JsWatch
|
13
|
+
from instaui.watch.vue_watch import VueWatch
|
14
|
+
from instaui.vars.element_ref import ElementRef
|
15
|
+
|
16
|
+
|
17
|
+
class Scope(Jsonable):
|
18
|
+
def __init__(self, id: str) -> None:
|
19
|
+
super().__init__()
|
20
|
+
self.id = id
|
21
|
+
self._vars_id_counter = 0
|
22
|
+
self._element_ref_id_counter = 0
|
23
|
+
self._vars: List[VarMixin] = []
|
24
|
+
self._web_computeds: List[WebComputed] = []
|
25
|
+
self._element_refs: List[ElementRef] = []
|
26
|
+
self._run_method_records: List = []
|
27
|
+
self._web_watchs: List[WebWatch] = []
|
28
|
+
self._js_watchs: List[JsWatch] = []
|
29
|
+
self._vue_watchs: List[VueWatch] = []
|
30
|
+
self._query = {}
|
31
|
+
|
32
|
+
def set_run_method_record(
|
33
|
+
self, scope_id: str, element_ref_id: str, method_name: str, args
|
34
|
+
):
|
35
|
+
self._run_method_records.append((scope_id, element_ref_id, method_name, args))
|
36
|
+
|
37
|
+
def generate_vars_id(self) -> str:
|
38
|
+
self._vars_id_counter += 1
|
39
|
+
return str(self._vars_id_counter)
|
40
|
+
|
41
|
+
def generate_element_ref_id(self) -> str:
|
42
|
+
self._element_ref_id_counter += 1
|
43
|
+
return str(self._element_ref_id_counter)
|
44
|
+
|
45
|
+
def register_element_ref(self, ref: ElementRef):
|
46
|
+
self._element_refs.append(ref)
|
47
|
+
|
48
|
+
def set_query(self, url: str, key: str, on: List[CanInputMixin]) -> None:
|
49
|
+
self._query = {
|
50
|
+
"url": url,
|
51
|
+
"key": key,
|
52
|
+
"on": [v._to_input_config() for v in on],
|
53
|
+
}
|
54
|
+
|
55
|
+
def register_var(self, var: VarMixin) -> None:
|
56
|
+
self._vars.append(var)
|
57
|
+
|
58
|
+
def register_web_computed(self, computed: WebComputed) -> None:
|
59
|
+
self._web_computeds.append(computed)
|
60
|
+
|
61
|
+
def register_web_watch(self, watch: WebWatch) -> None:
|
62
|
+
self._web_watchs.append(watch)
|
63
|
+
|
64
|
+
def register_js_watch(self, watch: JsWatch) -> None:
|
65
|
+
self._js_watchs.append(watch)
|
66
|
+
|
67
|
+
def register_vue_watch(self, watch: VueWatch) -> None:
|
68
|
+
self._vue_watchs.append(watch)
|
69
|
+
|
70
|
+
def _to_json_dict(self):
|
71
|
+
data = super()._to_json_dict()
|
72
|
+
if self._vars:
|
73
|
+
data["vars"] = self._vars
|
74
|
+
if self._query:
|
75
|
+
data["query"] = self._query
|
76
|
+
if self._web_watchs:
|
77
|
+
data["py_watch"] = self._web_watchs
|
78
|
+
if self._js_watchs:
|
79
|
+
data["js_watch"] = self._js_watchs
|
80
|
+
if self._vue_watchs:
|
81
|
+
data["vue_watch"] = self._vue_watchs
|
82
|
+
if self._element_refs:
|
83
|
+
data["eRefs"] = self._element_refs
|
84
|
+
if self._web_computeds:
|
85
|
+
data["web_computed"] = self._web_computeds
|
86
|
+
|
87
|
+
return data
|
88
|
+
|
89
|
+
|
90
|
+
class GlobalScope(Scope):
|
91
|
+
def __init__(self, id: str) -> None:
|
92
|
+
super().__init__(id)
|
93
|
+
|
94
|
+
def register_var(self, var: VarMixin) -> None:
|
95
|
+
raise ValueError("Can not register vars in global scope")
|
96
|
+
|
97
|
+
def register_web_computed(self, computed: WebComputed) -> None:
|
98
|
+
raise ValueError("Can not register web_computeds in global scope")
|
99
|
+
|
100
|
+
def register_web_watch(self, watch: WebWatch) -> None:
|
101
|
+
raise ValueError("Can not register web_watchs in global scope")
|
102
|
+
|
103
|
+
def register_js_watch(self, watch: JsWatch) -> None:
|
104
|
+
raise ValueError("Can not register js_watchs in global scope")
|
105
|
+
|
106
|
+
def register_vue_watch(self, watch: VueWatch) -> None:
|
107
|
+
raise ValueError("Can not register vue_watchs in global scope")
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import contextvars
|
2
|
+
from typing import Dict
|
3
|
+
|
4
|
+
|
5
|
+
_scope_var: contextvars.ContextVar[Dict[type, object]] = contextvars.ContextVar(
|
6
|
+
"_scope_var", default={}
|
7
|
+
)
|
8
|
+
|
9
|
+
|
10
|
+
def save_state(key, obj: object) -> None:
|
11
|
+
_scope_var.get()[key] = obj
|
12
|
+
|
13
|
+
|
14
|
+
def load_state(key) -> object:
|
15
|
+
return _scope_var.get()[key]
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from instaui.runtime import in_default_app_slot, HtmlResource
|
2
|
+
|
3
|
+
|
4
|
+
def use_tailwind(value=True):
|
5
|
+
"""Use Tailwind CSS framework.
|
6
|
+
|
7
|
+
Args:
|
8
|
+
value (bool, optional): whether to use Tailwind CSS. Defaults to True.
|
9
|
+
"""
|
10
|
+
|
11
|
+
if not in_default_app_slot():
|
12
|
+
raise ValueError("Cannot set use_tailwind outside of ui.page")
|
13
|
+
HtmlResource.use_tailwind = value
|
instaui/skip.py
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
from ._components import RouterLink as link, RouterView as view
|
2
|
+
from ._functions import (
|
3
|
+
add_route,
|
4
|
+
config_router,
|
5
|
+
get_params,
|
6
|
+
get_full_path,
|
7
|
+
get_path,
|
8
|
+
push,
|
9
|
+
output,
|
10
|
+
)
|
11
|
+
from ._route_model import RouteItem
|
12
|
+
from ._file_base_utils import build_routes_from_files
|
13
|
+
|
14
|
+
__all__ = [
|
15
|
+
"add_route",
|
16
|
+
"config_router",
|
17
|
+
"link",
|
18
|
+
"view",
|
19
|
+
"get_params",
|
20
|
+
"get_full_path",
|
21
|
+
"get_path",
|
22
|
+
"push",
|
23
|
+
"output",
|
24
|
+
"RouteItem",
|
25
|
+
"build_routes_from_files",
|
26
|
+
]
|
@@ -0,0 +1,35 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
import typing
|
3
|
+
from instaui import html
|
4
|
+
from instaui.components.element import Element
|
5
|
+
from instaui.vars.types import TMaybeRef
|
6
|
+
|
7
|
+
|
8
|
+
class RouterLink(Element):
|
9
|
+
def __init__(self, text: TMaybeRef[str], *, to: str):
|
10
|
+
super().__init__("router-link")
|
11
|
+
|
12
|
+
self.props({"to": to})
|
13
|
+
|
14
|
+
if text is not None:
|
15
|
+
with self.add_slot("default"):
|
16
|
+
html.span(text)
|
17
|
+
|
18
|
+
@classmethod
|
19
|
+
def by_name(
|
20
|
+
cls,
|
21
|
+
text: TMaybeRef[str],
|
22
|
+
*,
|
23
|
+
name: str,
|
24
|
+
params: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
25
|
+
) -> RouterLink:
|
26
|
+
to: typing.Dict = {"name": name}
|
27
|
+
if params:
|
28
|
+
to["params"] = params
|
29
|
+
|
30
|
+
return cls(text, to=to) # type: ignore
|
31
|
+
|
32
|
+
|
33
|
+
class RouterView(Element):
|
34
|
+
def __init__(self):
|
35
|
+
super().__init__("router-view")
|
@@ -0,0 +1,264 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from collections import deque
|
3
|
+
from datetime import datetime
|
4
|
+
import importlib.util
|
5
|
+
from pathlib import Path
|
6
|
+
import sys
|
7
|
+
import typing
|
8
|
+
from pydantic import BaseModel, Field
|
9
|
+
import jinja2
|
10
|
+
import inspect
|
11
|
+
|
12
|
+
|
13
|
+
def build_routes_from_files(
|
14
|
+
folder_path: typing.Union[str, Path] = "pages",
|
15
|
+
module_name: str = "_routes",
|
16
|
+
):
|
17
|
+
folder_path = _utils.get_caller_path().parent / Path(folder_path)
|
18
|
+
root = _model_utils.create_root(folder_path, module_name)
|
19
|
+
_code_gen.generate_router_file(root)
|
20
|
+
|
21
|
+
print(f"Build _routes from files in {folder_path}...")
|
22
|
+
# _module_utils.reload_module_from_folder(folder_path, module_name)
|
23
|
+
|
24
|
+
|
25
|
+
class _model_utils:
|
26
|
+
class FileRouteInfo(BaseModel):
|
27
|
+
file: Path
|
28
|
+
base_folder: Path
|
29
|
+
children: typing.List[_model_utils.FileRouteInfo] = []
|
30
|
+
|
31
|
+
path: str = Field(init=False, default="")
|
32
|
+
name: str = Field(init=False, default="")
|
33
|
+
fn_path: typing.Optional[str] = Field(init=False, default=None)
|
34
|
+
|
35
|
+
params: str = Field(init=False, default="")
|
36
|
+
|
37
|
+
def model_post_init(self, __context) -> None:
|
38
|
+
self.params = self._extract_params()
|
39
|
+
self.path, self.name = self._extra_path_name()
|
40
|
+
|
41
|
+
if self.file.is_file():
|
42
|
+
self.fn_path = ".".join(
|
43
|
+
self.file.relative_to(self.base_folder).with_suffix("").parts
|
44
|
+
)
|
45
|
+
|
46
|
+
def is_index_file(self):
|
47
|
+
return self.file.is_file() and self.file.stem == "index"
|
48
|
+
|
49
|
+
def change_index(self, index_info: _model_utils.FileRouteInfo):
|
50
|
+
self.fn_path = index_info.fn_path
|
51
|
+
self.path = self.path + index_info.params
|
52
|
+
|
53
|
+
def _extract_params(self):
|
54
|
+
if self.file.is_file():
|
55
|
+
route_config = _module_utils.get_module_getter(self.file)(
|
56
|
+
"_route_config"
|
57
|
+
)
|
58
|
+
if route_config:
|
59
|
+
if "params" in route_config:
|
60
|
+
return route_config["params"]
|
61
|
+
|
62
|
+
return ""
|
63
|
+
|
64
|
+
def _extra_path_name(self):
|
65
|
+
name_parts = list(
|
66
|
+
self.file.relative_to(self.base_folder).with_suffix("").parts
|
67
|
+
)
|
68
|
+
|
69
|
+
is_root = len(name_parts) == 1
|
70
|
+
|
71
|
+
path = self.file.stem
|
72
|
+
if path == "index":
|
73
|
+
path = ""
|
74
|
+
|
75
|
+
name = ".".join(name_parts)
|
76
|
+
|
77
|
+
if is_root:
|
78
|
+
path = "/" + path
|
79
|
+
|
80
|
+
if self.params:
|
81
|
+
path += self.params
|
82
|
+
|
83
|
+
return path, name
|
84
|
+
|
85
|
+
def import_code(self):
|
86
|
+
if not self.fn_path:
|
87
|
+
return ""
|
88
|
+
|
89
|
+
return f"from .{self.fn_path} import main as {self.main_fn_name()}"
|
90
|
+
|
91
|
+
def main_fn_name(self):
|
92
|
+
if not self.fn_path:
|
93
|
+
return ""
|
94
|
+
return self.name.replace(".", "_")
|
95
|
+
|
96
|
+
class FileRouteRoot(BaseModel):
|
97
|
+
folder: str
|
98
|
+
module_name: str
|
99
|
+
infos: list[_model_utils.FileRouteInfo] = []
|
100
|
+
|
101
|
+
@staticmethod
|
102
|
+
def create_root(folder: Path, module_name: str) -> FileRouteRoot:
|
103
|
+
base_folder = Path(folder)
|
104
|
+
infos = _model_utils._create_route_info(base_folder)
|
105
|
+
return _model_utils.FileRouteRoot(
|
106
|
+
folder=str(base_folder), module_name=module_name, infos=infos
|
107
|
+
)
|
108
|
+
|
109
|
+
@staticmethod
|
110
|
+
def _create_route_info(base_folder: Path) -> typing.List[FileRouteInfo]:
|
111
|
+
result: typing.List[_model_utils.FileRouteInfo] = []
|
112
|
+
|
113
|
+
stack: deque[
|
114
|
+
typing.Tuple[typing.Optional[_model_utils.FileRouteInfo], Path]
|
115
|
+
] = deque()
|
116
|
+
stack.extendleft((None, path) for path in base_folder.iterdir())
|
117
|
+
|
118
|
+
while stack:
|
119
|
+
parent_info, item = stack.pop()
|
120
|
+
is_dir = item.is_dir()
|
121
|
+
|
122
|
+
if item.stem.startswith("_"):
|
123
|
+
continue
|
124
|
+
|
125
|
+
if is_dir:
|
126
|
+
folder_info = _model_utils.FileRouteInfo(
|
127
|
+
file=item, base_folder=base_folder
|
128
|
+
)
|
129
|
+
infos = ((folder_info, path) for path in item.iterdir())
|
130
|
+
stack.extendleft(infos)
|
131
|
+
|
132
|
+
if parent_info is None:
|
133
|
+
result.append(folder_info)
|
134
|
+
else:
|
135
|
+
parent_info.children.append(folder_info)
|
136
|
+
continue
|
137
|
+
|
138
|
+
if item.suffix != ".py":
|
139
|
+
continue
|
140
|
+
|
141
|
+
file_info = _model_utils.FileRouteInfo(file=item, base_folder=base_folder)
|
142
|
+
|
143
|
+
if parent_info is None:
|
144
|
+
result.append(file_info)
|
145
|
+
else:
|
146
|
+
if file_info.is_index_file():
|
147
|
+
parent_info.change_index(file_info)
|
148
|
+
|
149
|
+
else:
|
150
|
+
parent_info.children.append(file_info)
|
151
|
+
|
152
|
+
return result
|
153
|
+
|
154
|
+
@staticmethod
|
155
|
+
def iter_route_info(infos: typing.List[FileRouteInfo]):
|
156
|
+
stack: typing.List[_model_utils.FileRouteInfo] = []
|
157
|
+
stack.extend(infos)
|
158
|
+
|
159
|
+
while stack:
|
160
|
+
info = stack.pop()
|
161
|
+
stack.extend(info.children)
|
162
|
+
yield info
|
163
|
+
|
164
|
+
|
165
|
+
class _code_gen:
|
166
|
+
_env = jinja2.Environment(
|
167
|
+
loader=jinja2.PackageLoader("instaui.spa_router", "templates"),
|
168
|
+
)
|
169
|
+
|
170
|
+
class TemplateModel(BaseModel):
|
171
|
+
update_time: datetime = Field(default_factory=datetime.now)
|
172
|
+
route_names: typing.List[str] = []
|
173
|
+
routes: typing.List[_model_utils.FileRouteInfo] = []
|
174
|
+
|
175
|
+
def get_all_main_import(self):
|
176
|
+
return [
|
177
|
+
info.import_code() for info in _model_utils.iter_route_info(self.routes)
|
178
|
+
]
|
179
|
+
|
180
|
+
@staticmethod
|
181
|
+
def generate_router_file(root: _model_utils.FileRouteRoot):
|
182
|
+
_template = _code_gen._env.get_template("page_routes")
|
183
|
+
|
184
|
+
template_model = _code_gen.TemplateModel(
|
185
|
+
route_names=_code_gen._extract_all_route_names(root), routes=root.infos
|
186
|
+
)
|
187
|
+
|
188
|
+
code = _template.render(model=template_model)
|
189
|
+
Path(root.folder).joinpath(f"{root.module_name}.py").write_text(
|
190
|
+
code, encoding="utf-8"
|
191
|
+
)
|
192
|
+
|
193
|
+
@staticmethod
|
194
|
+
def _extract_all_route_names(root: _model_utils.FileRouteRoot):
|
195
|
+
return [
|
196
|
+
info.name
|
197
|
+
for info in _model_utils.iter_route_info(root.infos)
|
198
|
+
if info.fn_path
|
199
|
+
]
|
200
|
+
|
201
|
+
|
202
|
+
class _module_utils:
|
203
|
+
@staticmethod
|
204
|
+
def reload_module_from_folder(folder: Path, module_name: str):
|
205
|
+
module_file_path = folder / f"{module_name}.py"
|
206
|
+
|
207
|
+
if not module_file_path.exists():
|
208
|
+
raise FileNotFoundError(f"No such file: '{module_file_path}'")
|
209
|
+
|
210
|
+
package_name = module_file_path.parent.name
|
211
|
+
spec = importlib.util.spec_from_file_location(
|
212
|
+
module_name, str(module_file_path)
|
213
|
+
)
|
214
|
+
if spec is None:
|
215
|
+
raise ImportError(f"Failed to load module: '{module_name}'")
|
216
|
+
|
217
|
+
module = importlib.util.module_from_spec(spec)
|
218
|
+
module.__package__ = package_name
|
219
|
+
|
220
|
+
if module_name in sys.modules:
|
221
|
+
sys.modules[module_name] = module
|
222
|
+
importlib.reload(module)
|
223
|
+
return
|
224
|
+
|
225
|
+
spec.loader.exec_module(module) # type: ignore
|
226
|
+
|
227
|
+
@staticmethod
|
228
|
+
def get_module_getter(path: Path):
|
229
|
+
if not isinstance(path, Path):
|
230
|
+
raise ValueError("Expected a Path object")
|
231
|
+
|
232
|
+
if not path.exists():
|
233
|
+
raise FileNotFoundError(f"The file {path} does not exist.")
|
234
|
+
|
235
|
+
module_name = path.stem
|
236
|
+
module_path = str(path.absolute())
|
237
|
+
|
238
|
+
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
239
|
+
if spec is None:
|
240
|
+
raise ImportError(f"Cannot create a module spec for {module_path}")
|
241
|
+
|
242
|
+
module = importlib.util.module_from_spec(spec)
|
243
|
+
|
244
|
+
try:
|
245
|
+
spec.loader.exec_module(module) # type: ignore
|
246
|
+
except Exception as e:
|
247
|
+
raise ImportError(f"Failed to import {module_path}: {e}")
|
248
|
+
|
249
|
+
def getter_fn(var_name: str):
|
250
|
+
return getattr(module, var_name, None)
|
251
|
+
|
252
|
+
return getter_fn
|
253
|
+
|
254
|
+
|
255
|
+
class _utils:
|
256
|
+
@staticmethod
|
257
|
+
def get_caller_path():
|
258
|
+
current_frame = inspect.currentframe()
|
259
|
+
try:
|
260
|
+
caller_frame = current_frame.f_back.f_back # type: ignore
|
261
|
+
filename = caller_frame.f_code.co_filename # type: ignore
|
262
|
+
return Path(filename)
|
263
|
+
finally:
|
264
|
+
del current_frame
|
@@ -0,0 +1,122 @@
|
|
1
|
+
import typing
|
2
|
+
from . import _types
|
3
|
+
from instaui.runtime._app import get_app_slot
|
4
|
+
from . import _install
|
5
|
+
from ._router_param_var import RouterParamsVar
|
6
|
+
from ._router_output import RouterOutput, RouterMethod
|
7
|
+
from ._route_model import RouteItem
|
8
|
+
|
9
|
+
_ASSERT_MSG = "Router is not initialized."
|
10
|
+
|
11
|
+
|
12
|
+
def add_route(
|
13
|
+
page_fn: typing.Callable,
|
14
|
+
*,
|
15
|
+
name: typing.Optional[str] = None,
|
16
|
+
path: typing.Optional[str] = None,
|
17
|
+
children: typing.Optional[typing.List[RouteItem]] = None,
|
18
|
+
lazy_loading: bool = False,
|
19
|
+
):
|
20
|
+
_install.try_register_router_collector()
|
21
|
+
route_collector = get_app_slot()._route_collector
|
22
|
+
assert route_collector is not None, _ASSERT_MSG
|
23
|
+
route_collector.add_route(
|
24
|
+
RouteItem.create(
|
25
|
+
path=path,
|
26
|
+
component_fn=page_fn,
|
27
|
+
name=name,
|
28
|
+
children=children,
|
29
|
+
)
|
30
|
+
)
|
31
|
+
|
32
|
+
|
33
|
+
def config_router(
|
34
|
+
routes: typing.Optional[typing.List[RouteItem]] = None,
|
35
|
+
*,
|
36
|
+
history: _types.TRouterHistoryMode = "hash",
|
37
|
+
keep_alive: bool = False,
|
38
|
+
):
|
39
|
+
"""Configure the router.
|
40
|
+
|
41
|
+
Example:
|
42
|
+
.. code-block:: python
|
43
|
+
routes = [
|
44
|
+
spa_router.RouteItem.create(path='/',component_fn=home),
|
45
|
+
spa_router.RouteItem.create(path='/user',component_fn=user_home),
|
46
|
+
]
|
47
|
+
|
48
|
+
spa_router.config_router(routes=routes)
|
49
|
+
|
50
|
+
Args:
|
51
|
+
routes (typing.Optional[typing.List[RouteItem]], optional): list of routes to be added to the router. Defaults to None.
|
52
|
+
history (_types.TRouterHistoryMode, optional): router history mode. Can be "web", "memory" or "hash". Defaults to "hash".
|
53
|
+
keep_alive (bool, optional): whether to keep the components alive when navigating to a new route.Defaults to False.
|
54
|
+
"""
|
55
|
+
|
56
|
+
_install.try_register_router_collector()
|
57
|
+
|
58
|
+
route_collector = get_app_slot()._route_collector
|
59
|
+
assert route_collector is not None, _ASSERT_MSG
|
60
|
+
|
61
|
+
route_collector.mode = history
|
62
|
+
route_collector.keep_alive = keep_alive
|
63
|
+
route_collector.routes = routes or []
|
64
|
+
|
65
|
+
|
66
|
+
def get_params(param_name: str) -> typing.Any:
|
67
|
+
return RouterParamsVar("params")[param_name]
|
68
|
+
|
69
|
+
|
70
|
+
def get_path():
|
71
|
+
return RouterParamsVar("path")
|
72
|
+
|
73
|
+
|
74
|
+
def get_full_path():
|
75
|
+
return RouterParamsVar("fullPath")
|
76
|
+
|
77
|
+
|
78
|
+
def push(
|
79
|
+
*,
|
80
|
+
path: typing.Optional[str] = None,
|
81
|
+
name: typing.Optional[str] = None,
|
82
|
+
params: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
83
|
+
query: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
84
|
+
hash: typing.Optional[str] = None,
|
85
|
+
):
|
86
|
+
method_params: typing.Dict = {}
|
87
|
+
|
88
|
+
if path is not None:
|
89
|
+
method_params["path"] = path
|
90
|
+
if name is not None:
|
91
|
+
method_params["name"] = name
|
92
|
+
|
93
|
+
if params is not None:
|
94
|
+
method_params["params"] = params
|
95
|
+
if query is not None:
|
96
|
+
method_params["query"] = query
|
97
|
+
if hash is not None:
|
98
|
+
method_params["hash"] = hash
|
99
|
+
|
100
|
+
return RouterMethod(
|
101
|
+
fn="push",
|
102
|
+
args=[method_params],
|
103
|
+
)
|
104
|
+
|
105
|
+
|
106
|
+
def go(n: int):
|
107
|
+
return RouterMethod(
|
108
|
+
fn="go",
|
109
|
+
args=[n],
|
110
|
+
)
|
111
|
+
|
112
|
+
|
113
|
+
def forward():
|
114
|
+
return go(1)
|
115
|
+
|
116
|
+
|
117
|
+
def back():
|
118
|
+
return go(-1)
|
119
|
+
|
120
|
+
|
121
|
+
def output():
|
122
|
+
return RouterOutput()
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from instaui.runtime._app import get_app_slot
|
2
|
+
from instaui.spa_router._route_model import RouteCollector
|
3
|
+
|
4
|
+
|
5
|
+
def try_register_router_collector():
|
6
|
+
app = get_app_slot()
|
7
|
+
if app._route_collector is not None:
|
8
|
+
return
|
9
|
+
|
10
|
+
rb = RouteCollector()
|
11
|
+
app.register_router(rb)
|