instaui 0.1.1__py3-none-any.whl → 0.1.2__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 (34) hide show
  1. instaui/components/component.py +1 -1
  2. instaui/components/element.py +37 -17
  3. instaui/components/html/checkbox.py +0 -7
  4. instaui/components/vfor.py +1 -1
  5. instaui/dependencies/component_dependency.py +16 -0
  6. instaui/dependencies/plugin_dependency.py +28 -0
  7. instaui/fastapi_server/debug_mode_router.py +0 -1
  8. instaui/fastapi_server/dependency_router.py +21 -0
  9. instaui/fastapi_server/resource.py +35 -0
  10. instaui/fastapi_server/server.py +124 -100
  11. instaui/html_tools.py +12 -106
  12. instaui/page_info.py +1 -11
  13. instaui/runtime/_app.py +9 -20
  14. instaui/runtime/resource.py +11 -46
  15. instaui/static/insta-ui.esm-browser.prod.js +3663 -3663
  16. instaui/static/insta-ui.iife.js +29 -29
  17. instaui/static/templates/web.html +32 -76
  18. instaui/static/templates/zero.html +48 -32
  19. instaui/systems/file_system.py +0 -9
  20. instaui/template/web_template.py +40 -46
  21. instaui/template/zero_template.py +100 -15
  22. instaui/ui/__init__.py +2 -5
  23. instaui/ui_functions/ui_page.py +3 -18
  24. instaui/vars/path_var.py +2 -1
  25. instaui/zero/func.py +111 -0
  26. instaui/zero/scope.py +12 -1
  27. {instaui-0.1.1.dist-info → instaui-0.1.2.dist-info}/METADATA +1 -1
  28. {instaui-0.1.1.dist-info → instaui-0.1.2.dist-info}/RECORD +30 -29
  29. instaui/dependencies/__init__.py +0 -15
  30. instaui/dependencies/component_registrar.py +0 -82
  31. instaui/dependencies/installer.py +0 -5
  32. instaui/fastapi_server/config_router.py +0 -60
  33. {instaui-0.1.1.dist-info → instaui-0.1.2.dist-info}/LICENSE +0 -0
  34. {instaui-0.1.1.dist-info → instaui-0.1.2.dist-info}/WHEEL +0 -0
@@ -22,7 +22,7 @@ class Component(Jsonable):
22
22
 
23
23
  self.tag = (
24
24
  "div"
25
- if tag is None
25
+ if tag is None or tag == ""
26
26
  else (
27
27
  tag._to_element_binding_config()
28
28
  if isinstance(tag, ElementBindingMixin)
@@ -13,6 +13,7 @@ from typing import (
13
13
  Optional,
14
14
  Tuple,
15
15
  Union,
16
+ cast,
16
17
  overload,
17
18
  TYPE_CHECKING,
18
19
  )
@@ -21,11 +22,11 @@ from collections import defaultdict
21
22
  from instaui.runtime._app import get_app_slot
22
23
  from instaui.runtime._app import get_current_scope
23
24
  from instaui.vars.element_ref import ElementRef
24
-
25
-
26
25
  from instaui.vars.vfor_item import VForItem
27
26
  from instaui.components.directive import Directive
28
- from instaui.dependencies import ComponentRegistrationInfo, register_component
27
+ from instaui.dependencies.component_dependency import (
28
+ ComponentDependencyInfo,
29
+ )
29
30
  from .slot import SlotManager, Slot
30
31
  from instaui import consts
31
32
  from instaui.components.component import Component
@@ -69,14 +70,14 @@ PROPS_PATTERN = re.compile(
69
70
 
70
71
 
71
72
  class Element(Component):
72
- component: ClassVar[Optional[ComponentRegistrationInfo]] = None
73
+ dependency: ClassVar[Optional[ComponentDependencyInfo]] = None
73
74
  _default_props: ClassVar[Dict[str, Any]] = {}
74
75
  _default_classes: ClassVar[List[str]] = []
75
76
  _default_style: ClassVar[Dict[str, str]] = {}
76
77
 
77
78
  def __init__(self, tag: Optional[Union[str, ElementBindingMixin]] = None):
78
- if self.component:
79
- tag = self.component.name
79
+ if self.dependency:
80
+ tag = self.dependency.tag_name or ""
80
81
 
81
82
  super().__init__(tag)
82
83
 
@@ -101,19 +102,28 @@ class Element(Component):
101
102
  def __init_subclass__(
102
103
  cls,
103
104
  *,
104
- component: Union[str, Path, None] = None,
105
+ esm: Union[str, Path, None] = None,
106
+ externals: Optional[List[Union[str, Path]]] = None,
107
+ css: Union[List[Union[str, Path]], None] = None,
105
108
  ) -> None:
106
109
  super().__init_subclass__()
107
110
 
108
- if component:
109
- if isinstance(component, str):
110
- component = Path(component)
111
- if not component.is_absolute():
112
- component = Path(inspect.getfile(cls)).parent / component
111
+ if esm:
112
+ esm = _make_dependency_path(esm, cls)
113
+
114
+ if externals:
115
+ externals = [_make_dependency_path(e, cls) for e in externals]
116
+
117
+ if css:
118
+ css = [_make_dependency_path(c, cls) for c in css]
113
119
 
114
- # TODO: Lazy load component registration
115
- cls.component = register_component(
116
- component.stem, esm=component, shared=True
120
+ tag_name = f"instaui-{esm.stem}"
121
+
122
+ cls.dependency = ComponentDependencyInfo(
123
+ tag_name=tag_name,
124
+ esm=esm,
125
+ externals=cast(List[Path], externals or []),
126
+ css=cast(List[Path], css or []),
117
127
  )
118
128
 
119
129
  cls._default_props = copy(cls._default_props)
@@ -365,8 +375,8 @@ class Element(Component):
365
375
  if self._directives:
366
376
  data["dir"] = list(self._directives.keys())
367
377
 
368
- if self.component:
369
- get_app_slot().register_component(self.component)
378
+ if self.dependency:
379
+ get_app_slot().use_component_dependency(self.dependency)
370
380
 
371
381
  if self._element_ref:
372
382
  scope = get_current_scope()
@@ -460,3 +470,13 @@ def _classifyBindableDict(
460
470
  value_data[key] = value
461
471
 
462
472
  return value_data, bind_data
473
+
474
+
475
+ def _make_dependency_path(path: Union[str, Path], cls: type):
476
+ if isinstance(path, str):
477
+ path = Path(path)
478
+
479
+ if not path.is_absolute():
480
+ path = Path(inspect.getfile(cls)).parent / path
481
+
482
+ return path
@@ -31,12 +31,5 @@ class Checkbox(InputEventMixin, ValueElement[Union[bool, str]]):
31
31
  if model_value is not None:
32
32
  self.props({"value": model_value})
33
33
 
34
- # def vmodel(
35
- # self,
36
- # value: Any,
37
- # *modifiers: Literal["trim"] | Literal["number"] | Literal["lazy"],
38
- # ):
39
- # return super().vmodel(value, *modifiers) # type: ignore
40
-
41
34
  def _input_event_mixin_element(self) -> Element:
42
35
  return self
@@ -60,7 +60,7 @@ class VFor(Component, Generic[_T]):
60
60
  def __enter__(self) -> _T:
61
61
  self.__scope = self.__scope_manager.__enter__()
62
62
  super().__enter__()
63
- return VForItem(self).proxy
63
+ return VForItem(self).proxy # type: ignore
64
64
 
65
65
  def __exit__(self, *_) -> None:
66
66
  self.__scope_manager.__exit__(*_)
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, field
3
+ from pathlib import Path
4
+ from typing import List, Optional
5
+
6
+ _TTagName = str
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class ComponentDependencyInfo:
11
+ tag_name: _TTagName = field(hash=True)
12
+ esm: Path = field(hash=False)
13
+ externals: Optional[List[Path]] = field(
14
+ default_factory=list, compare=False, hash=False
15
+ )
16
+ css: List[Path] = field(default_factory=list, compare=False, hash=False)
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, field
3
+ from pathlib import Path
4
+ from typing import List, Optional
5
+ from instaui.runtime._app import get_app_slot
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class PluginDependencyInfo:
10
+ name: str = field(hash=True)
11
+ esm: Path = field(hash=False)
12
+ externals: Optional[List[Path]] = field(
13
+ default_factory=list, compare=False, hash=False
14
+ )
15
+ css: List[Path] = field(default_factory=list, compare=False, hash=False)
16
+
17
+
18
+ def register_plugin(
19
+ name: str,
20
+ esm: Path,
21
+ *,
22
+ externals: Optional[List[Path]] = None,
23
+ css: Optional[List[Path]] = None,
24
+ ):
25
+ info = PluginDependencyInfo(f"plugin/{name}", esm, externals or [], css or [])
26
+
27
+ get_app_slot().use_plugin_dependency(info)
28
+ return info
@@ -24,7 +24,6 @@ async def event_generator(
24
24
  task_event = asyncio.Event()
25
25
  _task_events[connection_id] = task_event
26
26
 
27
- # yield "event: task\ndata: 0 \n\n\n"
28
27
  try:
29
28
  while not task_event.is_set():
30
29
  if await request.is_disconnected():
@@ -0,0 +1,21 @@
1
+ from fastapi import FastAPI
2
+ from fastapi.responses import FileResponse
3
+
4
+ from instaui.fastapi_server import resource
5
+
6
+
7
+ URL = f"{resource.URL}/{{hash_part:path}}"
8
+
9
+
10
+ def create_router(app: FastAPI):
11
+ _dependency_handler(app)
12
+
13
+
14
+ def _dependency_handler(app: FastAPI):
15
+ @app.get(URL)
16
+ def _(hash_part: str) -> FileResponse:
17
+ local_file = resource.get_by_hash(hash_part)
18
+
19
+ return FileResponse(
20
+ local_file, headers={"Cache-Control": "public, max-age=3600"}
21
+ )
@@ -0,0 +1,35 @@
1
+ from pathlib import Path
2
+ from typing import Dict
3
+ from urllib.parse import quote as urllib_quote
4
+ from instaui.systems import file_system
5
+ from instaui.version import __version__ as _INSTA_VERSION
6
+
7
+ URL = f"/_instaui_{_INSTA_VERSION}/resource"
8
+ _THashPart = str
9
+ _HASH_PART_MAP: Dict[_THashPart, Path] = {}
10
+ _FILE_URL_MAP: Dict[Path, _THashPart] = {}
11
+
12
+
13
+ def get_by_hash(hash_part: str) -> Path:
14
+ return _HASH_PART_MAP[hash_part]
15
+
16
+
17
+ def record_resource(path: Path):
18
+ if path in _FILE_URL_MAP:
19
+ return _FILE_URL_MAP[path]
20
+
21
+ hash_part = _generate_hash_part(path)
22
+ _HASH_PART_MAP[hash_part] = path
23
+ url = f"{URL}/{hash_part}"
24
+ _FILE_URL_MAP[path] = url
25
+ return url
26
+
27
+
28
+ def generate_static_url(file_path: Path):
29
+ return urllib_quote(f"{URL}/{_generate_hash_part(file_path)}")
30
+
31
+
32
+ def _generate_hash_part(file_path: Path):
33
+ file_path = Path(file_path).resolve()
34
+ hash_name = file_system.generate_hash_name_from_path(file_path)
35
+ return f"{hash_name}/{file_path.name}"
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+ from contextlib import contextmanager
2
3
  import inspect
3
4
  import os
4
5
  import multiprocessing
@@ -8,29 +9,36 @@ import __main__
8
9
 
9
10
  from fastapi import FastAPI
10
11
  from fastapi import Request
11
- from fastapi.responses import FileResponse, HTMLResponse
12
- from fastapi.staticfiles import StaticFiles
12
+ from fastapi.responses import HTMLResponse
13
13
  import uvicorn
14
14
  from uvicorn.supervisors import ChangeReload
15
+ import itertools
15
16
 
16
- from instaui.html_tools import to_json
17
+ from instaui.html_tools import to_config_data
17
18
  from instaui.launch_collector import get_launch_collector
18
19
  from instaui.page_info import PageInfo
19
- from instaui.handlers import config_handler
20
- from instaui.systems import file_system
20
+
21
21
  from instaui import consts
22
- from instaui.runtime._app import get_app_slot
22
+ from instaui.runtime._app import get_app_slot, get_default_app_slot
23
+ from instaui.runtime.dataclass import JsLink, VueAppComponent
23
24
  from instaui.template import web_template
24
- from . import config_router
25
+
26
+
27
+ from . import dependency_router
25
28
  from . import event_router
26
29
  from . import watch_router
27
30
  from . import debug_mode_router
28
31
  from .middlewares import RequestContextMiddleware
29
32
  from ._uvicorn import UvicornServer
30
-
33
+ from . import resource
31
34
 
32
35
  APP_IMPORT_STRING = "instaui.fastapi_server.server:Server._instance.app"
33
36
 
37
+ VUE_JS_HASH_LINK = resource.record_resource(consts.VUE_ES_JS_PATH)
38
+ INSTAUI_JS_HASH_LINK = resource.record_resource(consts.APP_ES_JS_PATH)
39
+ APP_CSS_LINK = resource.record_resource(consts.APP_CSS_PATH)
40
+ TAILWIND_JS_HASH_LINK = resource.record_resource(consts.TAILWIND_JS_PATH)
41
+
34
42
 
35
43
  class Server:
36
44
  _instance: Optional[Server] = None
@@ -44,13 +52,11 @@ class Server:
44
52
  def __init__(self):
45
53
  self.app = FastAPI()
46
54
  self.app.add_middleware(RequestContextMiddleware)
47
- config_router.create_router(self.app)
55
+ dependency_router.create_router(self.app)
48
56
  event_router.create_router(self.app)
49
57
  watch_router.create_router(self.app)
50
58
  debug_mode_router.create_router(self.app)
51
59
 
52
- self._static_dir_url = _add_static_dir(self.app)
53
-
54
60
  for page_info in get_launch_collector()._page_router.values():
55
61
  self.register_page(page_info)
56
62
 
@@ -61,125 +67,134 @@ class Server:
61
67
 
62
68
  self._remove_route(info.path)
63
69
 
64
- key = info.create_key()
65
- config_handler.register_handler(key, info.func)
66
- config_fetch_url = (
67
- config_handler.ASYNC_URL if is_async else config_handler.SYNC_URL
68
- )
70
+ if is_async:
69
71
 
70
- @self.app.get(info.path)
71
- def wrapper(request: Request):
72
- page_loadding_json = None
73
- if info.page_loading:
74
- loadding_result = info.page_loading()
75
- if loadding_result:
76
- return loadding_result
77
- page_loadding_json = to_json()
78
-
79
- html = self._to_web_html(
80
- page_info=info,
81
- config_fetch_url=config_fetch_url,
82
- config_fetch_key=key,
83
- query_path=info.path,
84
- request=request,
85
- page_loadding_json=page_loadding_json,
86
- )
87
- response = HTMLResponse(html)
88
- return response
72
+ @self.app.get(info.path)
73
+ async def _(request: Request):
74
+ self._update_page_info(request, info)
75
+ with _execute_request_lifespans():
76
+ await info.func()
77
+ html = self._to_web_html(
78
+ page_info=info,
79
+ request=request,
80
+ )
81
+
82
+ return HTMLResponse(html)
89
83
 
90
- return wrapper
84
+ else:
85
+
86
+ @self.app.get(info.path)
87
+ def _(request: Request):
88
+ self._update_page_info(request, info)
89
+ with _execute_request_lifespans():
90
+ info.func()
91
+ html = self._to_web_html(
92
+ page_info=info,
93
+ request=request,
94
+ )
95
+
96
+ return HTMLResponse(html)
91
97
 
92
98
  def _to_web_html(
93
99
  self,
94
100
  *,
95
101
  page_info: PageInfo,
96
- query_path: str,
97
- config_json: Optional[str] = None,
98
- config_fetch_url: Optional[str] = None,
99
- config_fetch_key: Optional[str] = None,
100
- page_loadding_json: Optional[str] = None,
101
102
  request: Request,
102
103
  ):
103
- system_slot = get_app_slot()
104
- html_resource = system_slot._html_resource
105
- page_info.apply_settings(html_resource)
106
-
107
- html_resource.add_import_map(
108
- "vue", self._static_dir_url + f"/{consts.VUE_ES_JS_PATH.name}"
104
+ config_data = to_config_data()
105
+
106
+ model = web_template.WebTemplateModel(
107
+ vue_js_link=VUE_JS_HASH_LINK,
108
+ instaui_js_link=INSTAUI_JS_HASH_LINK,
109
+ css_links=[
110
+ APP_CSS_LINK,
111
+ ],
112
+ config_dict=config_data,
109
113
  )
110
114
 
111
- html_resource.add_import_map(
112
- "instaui", self._static_dir_url + f"/{consts.APP_ES_JS_PATH.name}"
113
- )
114
- html_resource.add_css_link(consts.APP_CSS_PATH)
115
+ system_slot = get_app_slot()
116
+ default_app_slot = get_default_app_slot()
117
+ html_resource = system_slot._html_resource
118
+ default_html_resource = default_app_slot._html_resource
115
119
 
116
- if html_resource.use_tailwind:
117
- tailwind_url = self._static_dir_url + f"/{consts.TAILWIND_JS_PATH.name}"
118
- html_resource.add_js_link(tailwind_url)
120
+ if html_resource.use_tailwind is None:
121
+ if default_html_resource.use_tailwind:
122
+ model.js_links.append(JsLink(TAILWIND_JS_HASH_LINK))
123
+ else:
124
+ if html_resource.use_tailwind:
125
+ model.js_links.append(JsLink(TAILWIND_JS_HASH_LINK))
119
126
 
120
127
  # register custom components
121
- # TODO: use one get api to get all components
122
- for component in system_slot._js_components:
128
+ for component in system_slot._component_dependencies:
123
129
  if not component.esm:
124
130
  continue
125
- url = self.add_static_file_route(component.esm)
126
- html_resource.add_vue_app_component(name=component.name, url=url)
131
+
132
+ model.vue_app_component.append(
133
+ VueAppComponent(
134
+ name=component.tag_name,
135
+ url=resource.record_resource(component.esm),
136
+ )
137
+ )
127
138
 
128
139
  if component.css:
129
140
  for css_link in component.css:
130
- html_resource.add_css_link(css_link)
141
+ model.css_links.append(resource.record_resource(css_link))
131
142
 
132
143
  # register custom plugins
133
- for plugin in system_slot._plugins:
144
+ for plugin in set(
145
+ itertools.chain(
146
+ system_slot._plugin_dependencies, default_app_slot._plugin_dependencies
147
+ )
148
+ ):
134
149
  if not plugin.esm:
135
150
  continue
136
- url = self.add_static_file_route(plugin.esm)
137
- html_resource.add_vue_app_use(url)
151
+
152
+ model.vue_app_use.append(plugin.name)
153
+
154
+ model.add_extra_import_map(
155
+ plugin.name, resource.record_resource(plugin.esm)
156
+ )
138
157
 
139
158
  if plugin.css:
140
159
  for css_link in plugin.css:
141
- html_resource.add_css_link(css_link)
160
+ model.css_links.append(resource.record_resource(css_link))
142
161
 
143
162
  # css file link to web static link
144
- html_resource._css_links = {
145
- self.add_static_file_route(link) if isinstance(link, Path) else link: None
146
- for link, attrs in html_resource._css_links.items()
147
- }
163
+ for link, attrs in itertools.chain(
164
+ html_resource._css_links.items(), default_html_resource._css_links.items()
165
+ ):
166
+ if isinstance(link, Path):
167
+ model.css_links.append(resource.record_resource(link))
148
168
 
149
169
  # js file link to web static link
150
- for info in html_resource._js_links:
170
+ for info in itertools.chain(
171
+ html_resource._js_links, default_html_resource._js_links
172
+ ):
151
173
  if isinstance(info.link, Path):
152
- info.link = self.add_static_file_route(info.link)
174
+ model.js_links.append(JsLink((resource.record_resource(info.link))))
175
+
176
+ for js_code in itertools.chain(
177
+ html_resource._script_tags, default_html_resource._script_tags
178
+ ):
179
+ model.script_tags.append(js_code)
180
+
181
+ for sylte_code in itertools.chain(
182
+ html_resource._style_tags, default_html_resource._style_tags
183
+ ):
184
+ model.style_tags.append(sylte_code)
153
185
 
154
- prefix = request.headers.get(
186
+ model.prefix = request.headers.get(
155
187
  "X-Forwarded-Prefix", request.scope.get("root_path", "")
156
188
  )
157
189
 
158
- result = web_template.render_web_html(
159
- config_json=config_json,
160
- config_fetch_url=config_fetch_url,
161
- config_fetch_key=config_fetch_key,
162
- query_path_params=str(request.path_params),
163
- query_params=dict(request.query_params),
164
- query_path=query_path,
165
- page_loadding_json=page_loadding_json,
166
- prefix=prefix,
167
- )
168
- get_app_slot().reset_html_resource()
169
- return result
170
-
171
- def add_static_file_route(self, local_file: Path) -> str:
172
- path = file_system.generate_static_url_from_file_path(local_file)
173
- if path in self._registered_static_routes:
174
- return path
175
-
176
- @self.app.get(path)
177
- def _() -> FileResponse:
178
- return FileResponse(
179
- local_file, headers={"Cache-Control": "public, max-age=3600"}
180
- )
190
+ return web_template.render_web_html(model)
191
+
192
+ def _update_page_info(self, request: Request, page_info: PageInfo):
193
+ app = get_app_slot()
181
194
 
182
- return path
195
+ app._page_path = page_info.path
196
+ app._page_params = request.path_params
197
+ app._query_params = dict(request.query_params)
183
198
 
184
199
  def _remove_route(self, path: str) -> None:
185
200
  self.app.routes[:] = [
@@ -236,11 +251,20 @@ class Server:
236
251
  assert isinstance(app, FastAPI), "app must be a FastAPI instance"
237
252
 
238
253
 
239
- def _add_static_dir(app: FastAPI):
240
- url = file_system.generate_static_url_from_file_path(consts._STATIC_DIR)
241
- app.mount(url, StaticFiles(directory=consts._STATIC_DIR))
242
- return url
243
-
244
-
245
254
  def _split_args(args: str):
246
255
  return [a.strip() for a in args.split(",")]
256
+
257
+
258
+ @contextmanager
259
+ def _execute_request_lifespans():
260
+ events = [iter(event()) for event in get_launch_collector().page_request_lifespans]
261
+ for event in events:
262
+ next(event)
263
+
264
+ yield
265
+
266
+ for event in events:
267
+ try:
268
+ next(event)
269
+ except StopIteration:
270
+ pass