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.
Files changed (152) hide show
  1. instaui/__init__.py +9 -0
  2. instaui/_helper/observable_helper.py +35 -0
  3. instaui/boot_info.py +43 -0
  4. instaui/common/jsonable.py +37 -0
  5. instaui/components/__init__.py +0 -0
  6. instaui/components/column.py +18 -0
  7. instaui/components/component.py +47 -0
  8. instaui/components/content.py +34 -0
  9. instaui/components/directive.py +55 -0
  10. instaui/components/element.py +462 -0
  11. instaui/components/grid.py +80 -0
  12. instaui/components/html/__init__.py +36 -0
  13. instaui/components/html/_mixins.py +34 -0
  14. instaui/components/html/button.py +38 -0
  15. instaui/components/html/checkbox.py +42 -0
  16. instaui/components/html/date.py +28 -0
  17. instaui/components/html/div.py +7 -0
  18. instaui/components/html/form.py +7 -0
  19. instaui/components/html/input.py +28 -0
  20. instaui/components/html/label.py +21 -0
  21. instaui/components/html/li.py +17 -0
  22. instaui/components/html/link.py +31 -0
  23. instaui/components/html/number.py +34 -0
  24. instaui/components/html/paragraph.py +19 -0
  25. instaui/components/html/range.py +45 -0
  26. instaui/components/html/select.py +93 -0
  27. instaui/components/html/span.py +19 -0
  28. instaui/components/html/ul.py +20 -0
  29. instaui/components/match.py +106 -0
  30. instaui/components/row.py +19 -0
  31. instaui/components/slot.py +82 -0
  32. instaui/components/transition_group.py +9 -0
  33. instaui/components/value_element.py +48 -0
  34. instaui/components/vfor.py +140 -0
  35. instaui/components/vif.py +38 -0
  36. instaui/consts.py +18 -0
  37. instaui/dependencies/__init__.py +15 -0
  38. instaui/dependencies/component_registrar.py +82 -0
  39. instaui/dependencies/installer.py +5 -0
  40. instaui/event/event_mixin.py +12 -0
  41. instaui/event/js_event.py +57 -0
  42. instaui/event/web_event.py +108 -0
  43. instaui/experimental/__init__.py +4 -0
  44. instaui/experimental/debug.py +48 -0
  45. instaui/fastapi_server/_utils.py +42 -0
  46. instaui/fastapi_server/_uvicorn.py +37 -0
  47. instaui/fastapi_server/config_router.py +60 -0
  48. instaui/fastapi_server/debug_mode_router.py +61 -0
  49. instaui/fastapi_server/event_router.py +58 -0
  50. instaui/fastapi_server/middlewares.py +19 -0
  51. instaui/fastapi_server/request_context.py +19 -0
  52. instaui/fastapi_server/server.py +246 -0
  53. instaui/fastapi_server/watch_router.py +53 -0
  54. instaui/handlers/_utils.py +66 -0
  55. instaui/handlers/computed_handler.py +42 -0
  56. instaui/handlers/config_handler.py +13 -0
  57. instaui/handlers/event_handler.py +58 -0
  58. instaui/handlers/watch_handler.py +57 -0
  59. instaui/html_tools.py +139 -0
  60. instaui/inject.py +33 -0
  61. instaui/js/__init__.py +4 -0
  62. instaui/js/js_output.py +15 -0
  63. instaui/js/lambda_func.py +35 -0
  64. instaui/launch_collector.py +52 -0
  65. instaui/page_info.py +23 -0
  66. instaui/runtime/__init__.py +29 -0
  67. instaui/runtime/_app.py +206 -0
  68. instaui/runtime/_inner_helper.py +9 -0
  69. instaui/runtime/context.py +47 -0
  70. instaui/runtime/dataclass.py +30 -0
  71. instaui/runtime/resource.py +87 -0
  72. instaui/runtime/scope.py +107 -0
  73. instaui/runtime/ui_state_scope.py +15 -0
  74. instaui/settings/__init__.py +4 -0
  75. instaui/settings/__settings.py +13 -0
  76. instaui/skip.py +12 -0
  77. instaui/spa_router/__init__.py +26 -0
  78. instaui/spa_router/_components.py +35 -0
  79. instaui/spa_router/_file_base_utils.py +264 -0
  80. instaui/spa_router/_functions.py +122 -0
  81. instaui/spa_router/_install.py +11 -0
  82. instaui/spa_router/_route_model.py +139 -0
  83. instaui/spa_router/_router_box.py +40 -0
  84. instaui/spa_router/_router_output.py +22 -0
  85. instaui/spa_router/_router_param_var.py +51 -0
  86. instaui/spa_router/_types.py +4 -0
  87. instaui/spa_router/templates/page_routes +59 -0
  88. instaui/static/insta-ui.css +1 -0
  89. instaui/static/insta-ui.esm-browser.prod.js +3663 -0
  90. instaui/static/insta-ui.iife.js +29 -0
  91. instaui/static/insta-ui.iife.js.map +1 -0
  92. instaui/static/insta-ui.js.map +1 -0
  93. instaui/static/tailwindcss.min.js +62 -0
  94. instaui/static/templates/debug/sse.html +117 -0
  95. instaui/static/templates/web.html +118 -0
  96. instaui/static/templates/zero.html +55 -0
  97. instaui/static/vue.esm-browser.prod.js +9 -0
  98. instaui/static/vue.global.prod.js +9 -0
  99. instaui/static/vue.runtime.esm-browser.prod.js +5 -0
  100. instaui/systems/file_system.py +17 -0
  101. instaui/systems/func_system.py +104 -0
  102. instaui/systems/js_system.py +22 -0
  103. instaui/systems/pydantic_system.py +27 -0
  104. instaui/systems/string_system.py +10 -0
  105. instaui/template/__init__.py +4 -0
  106. instaui/template/env.py +7 -0
  107. instaui/template/web_template.py +55 -0
  108. instaui/template/zero_template.py +24 -0
  109. instaui/ui/__init__.py +121 -0
  110. instaui/ui/events.py +25 -0
  111. instaui/ui_functions/input_slient_data.py +16 -0
  112. instaui/ui_functions/server.py +13 -0
  113. instaui/ui_functions/str_format.py +36 -0
  114. instaui/ui_functions/ui_page.py +31 -0
  115. instaui/ui_functions/ui_types.py +13 -0
  116. instaui/ui_functions/url_location.py +33 -0
  117. instaui/vars/__init__.py +13 -0
  118. instaui/vars/_types.py +8 -0
  119. instaui/vars/_utils.py +12 -0
  120. instaui/vars/data.py +68 -0
  121. instaui/vars/element_ref.py +42 -0
  122. instaui/vars/event_context.py +45 -0
  123. instaui/vars/event_extend.py +0 -0
  124. instaui/vars/js_computed.py +95 -0
  125. instaui/vars/mixin_types/common_type.py +5 -0
  126. instaui/vars/mixin_types/element_binding.py +10 -0
  127. instaui/vars/mixin_types/observable.py +7 -0
  128. instaui/vars/mixin_types/pathable.py +14 -0
  129. instaui/vars/mixin_types/py_binding.py +13 -0
  130. instaui/vars/mixin_types/str_format_binding.py +8 -0
  131. instaui/vars/mixin_types/var_type.py +5 -0
  132. instaui/vars/path_var.py +89 -0
  133. instaui/vars/ref.py +103 -0
  134. instaui/vars/slot_prop.py +46 -0
  135. instaui/vars/state.py +82 -0
  136. instaui/vars/types.py +24 -0
  137. instaui/vars/vfor_item.py +204 -0
  138. instaui/vars/vue_computed.py +82 -0
  139. instaui/vars/web_computed.py +157 -0
  140. instaui/vars/web_view_computed.py +1 -0
  141. instaui/version.py +3 -0
  142. instaui/watch/_types.py +4 -0
  143. instaui/watch/_utils.py +3 -0
  144. instaui/watch/js_watch.py +74 -0
  145. instaui/watch/vue_watch.py +61 -0
  146. instaui/watch/web_watch.py +123 -0
  147. instaui/zero/__init__.py +3 -0
  148. instaui/zero/scope.py +9 -0
  149. instaui-0.1.0.dist-info/LICENSE +21 -0
  150. instaui-0.1.0.dist-info/METADATA +154 -0
  151. instaui-0.1.0.dist-info/RECORD +152 -0
  152. 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))
@@ -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,4 @@
1
+ from .__settings import use_tailwind
2
+
3
+
4
+ __all__ = ["use_tailwind"]
@@ -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,12 @@
1
+ from typing import Any
2
+
3
+
4
+ class _Skip:
5
+ pass
6
+
7
+
8
+ skip_output = _Skip()
9
+
10
+
11
+ def is_skip_output(value: Any):
12
+ return value is skip_output
@@ -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)