pyxflow 0.4.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 (193) hide show
  1. pyxflow/__init__.py +11 -0
  2. pyxflow/app.py +536 -0
  3. pyxflow/bundle/VAADIN/build/FlowBootstrap-tpaNq9i9.js +3 -0
  4. pyxflow/bundle/VAADIN/build/FlowBootstrap-tpaNq9i9.js.br +0 -0
  5. pyxflow/bundle/VAADIN/build/FlowClient-BJ9ZfTZb.js +3 -0
  6. pyxflow/bundle/VAADIN/build/FlowClient-BJ9ZfTZb.js.br +0 -0
  7. pyxflow/bundle/VAADIN/build/chunk-20b5913756ce3aca167f0ba08695a16bf3b2ced2aa3d79a2a3799bc94e649650-BSat8ge4.js +1646 -0
  8. pyxflow/bundle/VAADIN/build/chunk-20b5913756ce3aca167f0ba08695a16bf3b2ced2aa3d79a2a3799bc94e649650-BSat8ge4.js.br +0 -0
  9. pyxflow/bundle/VAADIN/build/chunk-20b5913756ce3aca167f0ba08695a16bf3b2ced2aa3d79a2a3799bc94e649650-B_vo-znS.js +1646 -0
  10. pyxflow/bundle/VAADIN/build/chunk-20b5913756ce3aca167f0ba08695a16bf3b2ced2aa3d79a2a3799bc94e649650-B_vo-znS.js.br +0 -0
  11. pyxflow/bundle/VAADIN/build/chunk-20b5913756ce3aca167f0ba08695a16bf3b2ced2aa3d79a2a3799bc94e649650-DW726K0S.js +1646 -0
  12. pyxflow/bundle/VAADIN/build/chunk-20b5913756ce3aca167f0ba08695a16bf3b2ced2aa3d79a2a3799bc94e649650-DW726K0S.js.br +0 -0
  13. pyxflow/bundle/VAADIN/build/chunk-3cf1c80c42b2c95b3d2b91b4d1432902255060706be2252df851b542cc34ffc5-A_pwI8nt.js +507 -0
  14. pyxflow/bundle/VAADIN/build/chunk-3cf1c80c42b2c95b3d2b91b4d1432902255060706be2252df851b542cc34ffc5-A_pwI8nt.js.br +0 -0
  15. pyxflow/bundle/VAADIN/build/chunk-3cf1c80c42b2c95b3d2b91b4d1432902255060706be2252df851b542cc34ffc5-BA7ReY0z.js +507 -0
  16. pyxflow/bundle/VAADIN/build/chunk-3cf1c80c42b2c95b3d2b91b4d1432902255060706be2252df851b542cc34ffc5-BA7ReY0z.js.br +0 -0
  17. pyxflow/bundle/VAADIN/build/chunk-3cf1c80c42b2c95b3d2b91b4d1432902255060706be2252df851b542cc34ffc5-Dw4NGPaD.js +507 -0
  18. pyxflow/bundle/VAADIN/build/chunk-3cf1c80c42b2c95b3d2b91b4d1432902255060706be2252df851b542cc34ffc5-Dw4NGPaD.js.br +0 -0
  19. pyxflow/bundle/VAADIN/build/chunk-7e3dc433ae4599f705db307c09bf7a8ba9e18853c089e7754783c59f030922e9-BHlSfVe4.js +1843 -0
  20. pyxflow/bundle/VAADIN/build/chunk-7e3dc433ae4599f705db307c09bf7a8ba9e18853c089e7754783c59f030922e9-BHlSfVe4.js.br +0 -0
  21. pyxflow/bundle/VAADIN/build/chunk-7e3dc433ae4599f705db307c09bf7a8ba9e18853c089e7754783c59f030922e9-D8CLzUCc.js +1843 -0
  22. pyxflow/bundle/VAADIN/build/chunk-7e3dc433ae4599f705db307c09bf7a8ba9e18853c089e7754783c59f030922e9-D8CLzUCc.js.br +0 -0
  23. pyxflow/bundle/VAADIN/build/chunk-7e3dc433ae4599f705db307c09bf7a8ba9e18853c089e7754783c59f030922e9-R5VHaiul.js +1843 -0
  24. pyxflow/bundle/VAADIN/build/chunk-7e3dc433ae4599f705db307c09bf7a8ba9e18853c089e7754783c59f030922e9-R5VHaiul.js.br +0 -0
  25. pyxflow/bundle/VAADIN/build/chunk-c8efc5a648d42f293847c817a5dddab3643111e0d94b848c89e3087c602cc885-CUp7QPlV.js +58 -0
  26. pyxflow/bundle/VAADIN/build/chunk-c8efc5a648d42f293847c817a5dddab3643111e0d94b848c89e3087c602cc885-CUp7QPlV.js.br +0 -0
  27. pyxflow/bundle/VAADIN/build/chunk-c8efc5a648d42f293847c817a5dddab3643111e0d94b848c89e3087c602cc885-CyOyikW_.js +58 -0
  28. pyxflow/bundle/VAADIN/build/chunk-c8efc5a648d42f293847c817a5dddab3643111e0d94b848c89e3087c602cc885-CyOyikW_.js.br +0 -0
  29. pyxflow/bundle/VAADIN/build/chunk-c8efc5a648d42f293847c817a5dddab3643111e0d94b848c89e3087c602cc885-DKkIdWLP.js +58 -0
  30. pyxflow/bundle/VAADIN/build/chunk-c8efc5a648d42f293847c817a5dddab3643111e0d94b848c89e3087c602cc885-DKkIdWLP.js.br +0 -0
  31. pyxflow/bundle/VAADIN/build/chunk-df82228a0d0ba7f82937da4dff5549a7886e68d794f21c59836b0708d2669a75-BBknl1Er.js +1079 -0
  32. pyxflow/bundle/VAADIN/build/chunk-df82228a0d0ba7f82937da4dff5549a7886e68d794f21c59836b0708d2669a75-BBknl1Er.js.br +0 -0
  33. pyxflow/bundle/VAADIN/build/chunk-df82228a0d0ba7f82937da4dff5549a7886e68d794f21c59836b0708d2669a75-BEZ1SplM.js +1079 -0
  34. pyxflow/bundle/VAADIN/build/chunk-df82228a0d0ba7f82937da4dff5549a7886e68d794f21c59836b0708d2669a75-BEZ1SplM.js.br +0 -0
  35. pyxflow/bundle/VAADIN/build/chunk-df82228a0d0ba7f82937da4dff5549a7886e68d794f21c59836b0708d2669a75-Bl2cj9us.js +1079 -0
  36. pyxflow/bundle/VAADIN/build/chunk-df82228a0d0ba7f82937da4dff5549a7886e68d794f21c59836b0708d2669a75-Bl2cj9us.js.br +0 -0
  37. pyxflow/bundle/VAADIN/build/commonjsHelpers-CqkleIqs.js +1 -0
  38. pyxflow/bundle/VAADIN/build/commonjsHelpers-CqkleIqs.js.br +0 -0
  39. pyxflow/bundle/VAADIN/build/generated-flow-imports-A_cbqzPb.js +12141 -0
  40. pyxflow/bundle/VAADIN/build/generated-flow-imports-A_cbqzPb.js.br +0 -0
  41. pyxflow/bundle/VAADIN/build/generated-flow-imports-BNGvL9My.js +12141 -0
  42. pyxflow/bundle/VAADIN/build/generated-flow-imports-BNGvL9My.js.br +0 -0
  43. pyxflow/bundle/VAADIN/build/generated-flow-imports-BkytpRbC.js +12141 -0
  44. pyxflow/bundle/VAADIN/build/generated-flow-imports-BkytpRbC.js.br +0 -0
  45. pyxflow/bundle/VAADIN/build/indexhtml-0Ske2WDa.js +273 -0
  46. pyxflow/bundle/VAADIN/build/indexhtml-0Ske2WDa.js.br +0 -0
  47. pyxflow/bundle/VAADIN/build/indexhtml-C5Ngnva6.js +273 -0
  48. pyxflow/bundle/VAADIN/build/indexhtml-C5Ngnva6.js.br +0 -0
  49. pyxflow/bundle/VAADIN/build/indexhtml-GL0WLVAv.js +273 -0
  50. pyxflow/bundle/VAADIN/build/indexhtml-GL0WLVAv.js.br +0 -0
  51. pyxflow/bundle/VAADIN/build/vaadin-iconset-BXNKwFyY.js +642 -0
  52. pyxflow/bundle/VAADIN/build/vaadin-iconset-BXNKwFyY.js.br +0 -0
  53. pyxflow/bundle/VAADIN/build/vaadin-iconset-CG0RSisR.js +642 -0
  54. pyxflow/bundle/VAADIN/build/vaadin-iconset-CG0RSisR.js.br +0 -0
  55. pyxflow/bundle/VAADIN/build/vaadin-iconset-DFH06Ogz.js +642 -0
  56. pyxflow/bundle/VAADIN/build/vaadin-iconset-DFH06Ogz.js.br +0 -0
  57. pyxflow/bundle/VAADIN/static/push/vaadinPush-min.js +1 -0
  58. pyxflow/bundle/VAADIN/static/push/vaadinPush-min.js.gz +0 -0
  59. pyxflow/bundle/VAADIN/static/push/vaadinPush.js +3508 -0
  60. pyxflow/bundle/VAADIN/static/push/vaadinPush.js.gz +0 -0
  61. pyxflow/bundle/aura/aura.css +3 -0
  62. pyxflow/bundle/aura/fonts/InstrumentSans/InstrumentSans.woff2 +0 -0
  63. pyxflow/bundle/index.html +25 -0
  64. pyxflow/bundle/index.html.br +0 -0
  65. pyxflow/bundle/lumo/lumo.css +55 -0
  66. pyxflow/bundle/lumo/presets/compact.css +1 -0
  67. pyxflow/bundle/lumo/utility.css +1 -0
  68. pyxflow/bundle/pwa-icons/icons/icon-1125x2436.png +0 -0
  69. pyxflow/bundle/pwa-icons/icons/icon-1136x640.png +0 -0
  70. pyxflow/bundle/pwa-icons/icons/icon-1170x2532.png +0 -0
  71. pyxflow/bundle/pwa-icons/icons/icon-1242x2208.png +0 -0
  72. pyxflow/bundle/pwa-icons/icons/icon-1242x2688.png +0 -0
  73. pyxflow/bundle/pwa-icons/icons/icon-1284x2778.png +0 -0
  74. pyxflow/bundle/pwa-icons/icons/icon-1334x750.png +0 -0
  75. pyxflow/bundle/pwa-icons/icons/icon-144x144.png +0 -0
  76. pyxflow/bundle/pwa-icons/icons/icon-1536x2048.png +0 -0
  77. pyxflow/bundle/pwa-icons/icons/icon-1620x2160.png +0 -0
  78. pyxflow/bundle/pwa-icons/icons/icon-1668x2224.png +0 -0
  79. pyxflow/bundle/pwa-icons/icons/icon-1668x2388.png +0 -0
  80. pyxflow/bundle/pwa-icons/icons/icon-16x16.png +0 -0
  81. pyxflow/bundle/pwa-icons/icons/icon-1792x828.png +0 -0
  82. pyxflow/bundle/pwa-icons/icons/icon-180x180.png +0 -0
  83. pyxflow/bundle/pwa-icons/icons/icon-192x192.png +0 -0
  84. pyxflow/bundle/pwa-icons/icons/icon-2048x1536.png +0 -0
  85. pyxflow/bundle/pwa-icons/icons/icon-2048x2732.png +0 -0
  86. pyxflow/bundle/pwa-icons/icons/icon-2160x1620.png +0 -0
  87. pyxflow/bundle/pwa-icons/icons/icon-2208x1242.png +0 -0
  88. pyxflow/bundle/pwa-icons/icons/icon-2224x1668.png +0 -0
  89. pyxflow/bundle/pwa-icons/icons/icon-2388x1668.png +0 -0
  90. pyxflow/bundle/pwa-icons/icons/icon-2436x1125.png +0 -0
  91. pyxflow/bundle/pwa-icons/icons/icon-2532x1170.png +0 -0
  92. pyxflow/bundle/pwa-icons/icons/icon-2688x1242.png +0 -0
  93. pyxflow/bundle/pwa-icons/icons/icon-2732x2048.png +0 -0
  94. pyxflow/bundle/pwa-icons/icons/icon-2778x1284.png +0 -0
  95. pyxflow/bundle/pwa-icons/icons/icon-32x32.png +0 -0
  96. pyxflow/bundle/pwa-icons/icons/icon-512x512.png +0 -0
  97. pyxflow/bundle/pwa-icons/icons/icon-640x1136.png +0 -0
  98. pyxflow/bundle/pwa-icons/icons/icon-750x1334.png +0 -0
  99. pyxflow/bundle/pwa-icons/icons/icon-828x1792.png +0 -0
  100. pyxflow/bundle/pwa-icons/icons/icon-96x96.png +0 -0
  101. pyxflow/bundle/sw.js +2 -0
  102. pyxflow/bundle/sw.js.br +0 -0
  103. pyxflow/bundle_generator.py +658 -0
  104. pyxflow/bundle_generator_resources/.mvn/wrapper/maven-wrapper.properties +3 -0
  105. pyxflow/bundle_generator_resources/mvnw +295 -0
  106. pyxflow/bundle_generator_resources/mvnw.cmd +189 -0
  107. pyxflow/components/__init__.py +255 -0
  108. pyxflow/components/accordion.py +143 -0
  109. pyxflow/components/app_layout.py +123 -0
  110. pyxflow/components/avatar.py +166 -0
  111. pyxflow/components/button.py +150 -0
  112. pyxflow/components/card.py +188 -0
  113. pyxflow/components/checkbox.py +114 -0
  114. pyxflow/components/checkbox_group.py +170 -0
  115. pyxflow/components/combo_box.py +372 -0
  116. pyxflow/components/confirm_dialog.py +278 -0
  117. pyxflow/components/constants.py +618 -0
  118. pyxflow/components/context_menu.py +276 -0
  119. pyxflow/components/custom_field.py +121 -0
  120. pyxflow/components/date_picker.py +216 -0
  121. pyxflow/components/date_time_picker.py +186 -0
  122. pyxflow/components/details.py +156 -0
  123. pyxflow/components/dialog.py +453 -0
  124. pyxflow/components/drawer_toggle.py +10 -0
  125. pyxflow/components/email_field.py +178 -0
  126. pyxflow/components/flex_layout.py +236 -0
  127. pyxflow/components/form_layout.py +421 -0
  128. pyxflow/components/grid.py +1839 -0
  129. pyxflow/components/horizontal_layout.py +197 -0
  130. pyxflow/components/html.py +256 -0
  131. pyxflow/components/icon.py +55 -0
  132. pyxflow/components/list_box.py +284 -0
  133. pyxflow/components/login.py +237 -0
  134. pyxflow/components/markdown.py +38 -0
  135. pyxflow/components/master_detail_layout.py +302 -0
  136. pyxflow/components/menu_bar.py +338 -0
  137. pyxflow/components/message_input.py +64 -0
  138. pyxflow/components/message_list.py +108 -0
  139. pyxflow/components/mixins.py +88 -0
  140. pyxflow/components/multi_select_combo_box.py +343 -0
  141. pyxflow/components/notification.py +339 -0
  142. pyxflow/components/number_field.py +295 -0
  143. pyxflow/components/password_field.py +177 -0
  144. pyxflow/components/popover.py +288 -0
  145. pyxflow/components/progress_bar.py +82 -0
  146. pyxflow/components/radio_button_group.py +149 -0
  147. pyxflow/components/renderer.py +76 -0
  148. pyxflow/components/router_link.py +55 -0
  149. pyxflow/components/scroller.py +91 -0
  150. pyxflow/components/select.py +263 -0
  151. pyxflow/components/side_nav.py +226 -0
  152. pyxflow/components/span.py +44 -0
  153. pyxflow/components/split_layout.py +181 -0
  154. pyxflow/components/tab_sheet.py +152 -0
  155. pyxflow/components/tabs.py +255 -0
  156. pyxflow/components/text_area.py +192 -0
  157. pyxflow/components/text_field.py +238 -0
  158. pyxflow/components/time_picker.py +201 -0
  159. pyxflow/components/upload.py +280 -0
  160. pyxflow/components/value_change_mode.py +5 -0
  161. pyxflow/components/vertical_layout.py +194 -0
  162. pyxflow/components/virtual_list.py +234 -0
  163. pyxflow/core/__init__.py +9 -0
  164. pyxflow/core/component.py +794 -0
  165. pyxflow/core/element.py +130 -0
  166. pyxflow/core/keys.py +34 -0
  167. pyxflow/core/state_node.py +121 -0
  168. pyxflow/core/state_tree.py +127 -0
  169. pyxflow/data/__init__.py +47 -0
  170. pyxflow/data/binder.py +382 -0
  171. pyxflow/data/converter.py +51 -0
  172. pyxflow/data/provider.py +191 -0
  173. pyxflow/data/result.py +78 -0
  174. pyxflow/data/validator.py +89 -0
  175. pyxflow/menu.py +130 -0
  176. pyxflow/router.py +674 -0
  177. pyxflow/scaffold/favicon.ico +0 -0
  178. pyxflow/scaffold/project/__main__.py.tmpl +5 -0
  179. pyxflow/scaffold/project/static/styles/styles.css +8 -0
  180. pyxflow/scaffold/project/views/hello_world.py.tmpl +15 -0
  181. pyxflow/scaffold/project/views/main_layout.py.tmpl +17 -0
  182. pyxflow/scaffold/vscode/extensions.json +7 -0
  183. pyxflow/scaffold/vscode/python.code-snippets +22 -0
  184. pyxflow/scaffold/vscode/settings.json +48 -0
  185. pyxflow/scaffold/vscode/templates/pyxflow_view.py +10 -0
  186. pyxflow/server/__init__.py +6 -0
  187. pyxflow/server/http_server.py +820 -0
  188. pyxflow/server/uidl_handler.py +1099 -0
  189. pyxflow-0.4.0.dist-info/METADATA +281 -0
  190. pyxflow-0.4.0.dist-info/RECORD +193 -0
  191. pyxflow-0.4.0.dist-info/WHEEL +4 -0
  192. pyxflow-0.4.0.dist-info/entry_points.txt +3 -0
  193. pyxflow-0.4.0.dist-info/licenses/LICENSE +191 -0
pyxflow/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Vaadin Flow for Python - Server-side UI framework."""
2
+
3
+ __version__ = "0.4.0"
4
+
5
+ from pyxflow.core.component import Component
6
+ from pyxflow.core.element import Element
7
+ from pyxflow.router import AppShell, BeforeEnterEvent, ColorScheme, Location, Push, QueryParameters, Route, RouteAlias, RouteParameters, PageTitle, StyleSheet, discover_views
8
+ from pyxflow.menu import Menu, MenuEntry, get_menu_entries, get_page_header
9
+ from pyxflow.app import FlowApp
10
+
11
+ __all__ = ["AppShell", "BeforeEnterEvent", "ColorScheme", "Component", "Element", "FlowApp", "Location", "Menu", "MenuEntry", "PageTitle", "Push", "QueryParameters", "Route", "RouteAlias", "RouteParameters", "StyleSheet", "discover_views", "get_menu_entries", "get_page_header", "__version__"]
pyxflow/app.py ADDED
@@ -0,0 +1,536 @@
1
+ """FlowApp — single entry point to configure and run a PyXFlow application."""
2
+
3
+ import inspect
4
+ import json
5
+ import os
6
+ import sys
7
+
8
+
9
+ class FlowApp:
10
+ """Configure and run a PyXFlow application.
11
+
12
+ Usage::
13
+
14
+ # demo/__main__.py
15
+ from pyxflow import FlowApp
16
+
17
+ FlowApp(port=8088).run()
18
+
19
+ CLI flags:
20
+ --dev Auto-reload on source changes (requires watchfiles)
21
+ --debug Verbose UIDL protocol logging
22
+ """
23
+
24
+ def __init__(self, *, port: int = 8080, host: str = "localhost"):
25
+ # Auto-detect caller's package → views module
26
+ frame = inspect.stack()[1]
27
+ module_name = frame.frame.f_globals.get("__name__", "")
28
+ if module_name == "__main__":
29
+ # python -m <package> → resolve from __spec__ or file path
30
+ spec = frame.frame.f_globals.get("__spec__")
31
+ if spec and spec.parent:
32
+ package = spec.parent
33
+ else:
34
+ # Fallback: derive from file path (e.g. demo/__main__.py → demo)
35
+ from pathlib import Path
36
+ caller_path = Path(frame.filename).resolve()
37
+ package = caller_path.parent.name
38
+ elif "." in module_name:
39
+ package = module_name.rsplit(".", 1)[0]
40
+ else:
41
+ package = module_name
42
+ self._views = f"{package}.views"
43
+ self._port = port
44
+ self._host = host
45
+
46
+ def run(self):
47
+ """Parse CLI flags and start the server."""
48
+ args = set(sys.argv[1:])
49
+ dev = "--dev" in args
50
+ debug = "--debug" in args
51
+
52
+ if dev:
53
+ self._run_dev(debug)
54
+ else:
55
+ _serve(self._views, self._host, self._port, debug, dev=False)
56
+
57
+ def _run_dev(self, debug: bool):
58
+ """Dev mode: parent owns the socket, children are restarted on changes.
59
+
60
+ The listening socket is created once in this parent process and
61
+ inherited by each child via pass_fds. When a child is killed for
62
+ reload, the socket stays open — no EADDRINUSE race.
63
+ """
64
+ import socket
65
+ import subprocess
66
+ import watchfiles
67
+ from pathlib import Path
68
+
69
+ watch_dir = Path(".").resolve()
70
+ print(f" Dev mode: watching {watch_dir} for Python file changes", flush=True)
71
+
72
+ # Check if something is already listening on the port
73
+ probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
74
+ try:
75
+ probe.connect((self._host, self._port))
76
+ probe.close()
77
+ print(f"\n ERROR: Port {self._port} is already in use.")
78
+ print(f" Kill the other process: lsof -ti :{self._port} | xargs kill -9\n")
79
+ return
80
+ except ConnectionRefusedError:
81
+ pass # Nothing listening — good
82
+ finally:
83
+ probe.close()
84
+
85
+ # Create listening socket in the parent — survives child restarts
86
+ srv_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
87
+ srv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
88
+ srv_sock.bind((self._host, self._port))
89
+ srv_sock.listen(128)
90
+ srv_sock.set_inheritable(True)
91
+ fd = srv_sock.fileno()
92
+ print(f" Listening on http://{self._host}:{self._port}", flush=True)
93
+
94
+ env = os.environ.copy()
95
+ env["_PYFLOW_APP"] = json.dumps({
96
+ "views": self._views,
97
+ "host": self._host,
98
+ "port": self._port,
99
+ "debug": debug,
100
+ "socket_fd": fd,
101
+ })
102
+
103
+ def start_child():
104
+ return subprocess.Popen(
105
+ [sys.executable, "-c",
106
+ "from pyxflow.app import _dev_serve; _dev_serve()"],
107
+ pass_fds=(fd,),
108
+ env=env,
109
+ )
110
+
111
+ def stop_child(proc):
112
+ if proc.poll() is None:
113
+ proc.kill()
114
+ proc.wait(timeout=2)
115
+
116
+ # Track mtimes to ignore metadata-only events (macOS FSEvents
117
+ # fires for extended attribute changes like "last opened date")
118
+ file_mtimes: dict[str, float] = {}
119
+
120
+ proc = start_child()
121
+ try:
122
+ for changes in watchfiles.watch(
123
+ ".",
124
+ watch_filter=watchfiles.PythonFilter(),
125
+ debounce=800,
126
+ ):
127
+ actually_changed = []
128
+ for _, p in changes:
129
+ try:
130
+ mtime = os.path.getmtime(p)
131
+ except OSError:
132
+ mtime = 0
133
+ if file_mtimes.get(p) != mtime:
134
+ file_mtimes[p] = mtime
135
+ actually_changed.append(os.path.relpath(p, watch_dir))
136
+ if not actually_changed:
137
+ continue
138
+ paths = sorted(actually_changed)
139
+ print(f" Reloading because files modified: {', '.join(paths)}", flush=True)
140
+ stop_child(proc)
141
+ proc = start_child()
142
+ except KeyboardInterrupt:
143
+ pass
144
+ finally:
145
+ stop_child(proc)
146
+ srv_sock.close()
147
+
148
+
149
+ def _auto_detect_app() -> str | None:
150
+ """Try to find an app module in cwd by looking for <pkg>/views/ or ./views/."""
151
+ from pathlib import Path
152
+ # Check subdirectories first (e.g. myapp/views/)
153
+ for entry in sorted(Path.cwd().iterdir()):
154
+ if entry.is_dir() and (entry / "views").is_dir():
155
+ return entry.name
156
+ # Check cwd itself (e.g. views/ at root, from --setup without name)
157
+ if (Path.cwd() / "views").is_dir():
158
+ return Path.cwd().name.replace("-", "_")
159
+ return None
160
+
161
+
162
+ def _usage() -> None:
163
+ print("Usage: pyxflow [app_module] [--dev] [--debug] [--port PORT] [--host HOST]")
164
+ print(" pyxflow [app_module] bundle [--copy|--build] [--vaadin-version VERSION]")
165
+ print(" pyxflow --vscode")
166
+ print(" pyxflow --setup [app_name]")
167
+ print()
168
+ print(" app_module Python module with views (auto-detected if omitted)")
169
+ print(" --dev Auto-reload on source changes")
170
+ print(" --debug Verbose UIDL protocol logging")
171
+ print(" --port N Server port (default: 8080)")
172
+ print(" --host H Server host (default: localhost)")
173
+ print(" bundle Generate frontend bundle")
174
+ print(" --copy Copy bundle from Maven JARs in ~/.m2 (default, fast)")
175
+ print(" --build Full Maven build (slower, generates custom bundle)")
176
+ print(" --keep Keep bundle-project/ after build (only with --build)")
177
+ print(" --optimized Use optimized bundle (code-split chunks, larger on disk)")
178
+ print(" --vscode Generate .vscode/ config and install recommended extensions")
179
+ print(" --setup Scaffold a new Vaadin PyXFlow project (views, static, __main__.py)")
180
+ sys.exit(0)
181
+
182
+
183
+ def _scaffold_dir():
184
+ """Return the scaffold resource directory."""
185
+ import importlib.resources
186
+ return importlib.resources.files("pyxflow") / "scaffold"
187
+
188
+
189
+ def _setup_project(app_name: str | None = None) -> None:
190
+ """Scaffold a new Vaadin PyXFlow project structure."""
191
+ import shutil
192
+ from pathlib import Path
193
+
194
+ root = Path.cwd()
195
+ if app_name is not None:
196
+ # Explicit name: create as subdirectory
197
+ pkg = root / app_name
198
+ else:
199
+ # No name: scaffold in cwd, derive name from directory
200
+ app_name = root.name.replace("-", "_")
201
+ pkg = root
202
+ scaffold = _scaffold_dir() / "project"
203
+
204
+ # --- Package directories ---
205
+ views_dir = pkg / "views"
206
+ static_styles = pkg / "static" / "styles"
207
+ static_images = pkg / "static" / "images"
208
+
209
+ for d in (views_dir, static_styles, static_images):
210
+ d.mkdir(parents=True, exist_ok=True)
211
+
212
+ def _write(path: Path, content: str) -> None:
213
+ action = "Updated" if path.exists() else "Created"
214
+ path.write_text(content)
215
+ print(f" {action} {path.relative_to(root)}")
216
+
217
+ def _write_template(src_name: str, dest: Path) -> None:
218
+ """Read a .tmpl file from scaffold, replace {{app_name}}, write to dest."""
219
+ if dest.exists():
220
+ return
221
+ text = (scaffold / src_name).read_text()
222
+ if pkg == root:
223
+ # cwd mode: "from views.main_layout" not "from pepe.views.main_layout"
224
+ text = text.replace("{{app_name}}.", "")
225
+ text = text.replace("{{app_name}}", app_name)
226
+ _write(dest, text)
227
+
228
+ # --- __init__.py ---
229
+ for init in (pkg / "__init__.py", views_dir / "__init__.py"):
230
+ if not init.exists():
231
+ _write(init, "")
232
+
233
+ # --- Templated files ---
234
+ _write_template("__main__.py.tmpl", pkg / "__main__.py")
235
+ _write_template("views/main_layout.py.tmpl", views_dir / "main_layout.py")
236
+ _write_template("views/hello_world.py.tmpl", views_dir / "hello_world.py")
237
+
238
+ # --- Static files (copy as-is) ---
239
+ css_dest = static_styles / "styles.css"
240
+ if not css_dest.exists():
241
+ text = (scaffold / "static" / "styles" / "styles.css").read_text()
242
+ _write(css_dest, text)
243
+
244
+ # --- favicon.ico (binary copy) ---
245
+ favicon_dest = pkg / "static" / "favicon.ico"
246
+ if not favicon_dest.exists():
247
+ try:
248
+ import importlib.resources
249
+ favicon_src = _scaffold_dir() / "favicon.ico"
250
+ with importlib.resources.as_file(favicon_src) as src:
251
+ shutil.copy2(src, favicon_dest)
252
+ print(f" Created {favicon_dest.relative_to(root)}")
253
+ except (FileNotFoundError, TypeError):
254
+ print(" Warning: favicon.ico not found in package scaffold")
255
+
256
+ print(f"\n Project '{app_name}' scaffolded!")
257
+ if pkg == root:
258
+ print(f" Run: cd .. && python -m {app_name}")
259
+ else:
260
+ print(f" Run: python -m {app_name}")
261
+
262
+
263
+ def _setup_vscode() -> None:
264
+ """Generate .vscode/ config for Vaadin PyXFlow development."""
265
+ import shutil
266
+ import subprocess
267
+ from pathlib import Path
268
+
269
+ scaffold = _scaffold_dir() / "vscode"
270
+ vscode_dir = Path.cwd() / ".vscode"
271
+ vscode_dir.mkdir(exist_ok=True)
272
+
273
+ def _rel(path: Path) -> str:
274
+ return str(path.relative_to(Path.cwd()))
275
+
276
+ # --- settings.json (merge, don't overwrite) ---
277
+ settings_path = vscode_dir / "settings.json"
278
+ defaults = json.loads((scaffold / "settings.json").read_text())
279
+ if settings_path.exists():
280
+ existing = json.loads(settings_path.read_text())
281
+ for key, val in defaults.items():
282
+ if key not in existing:
283
+ existing[key] = val
284
+ settings_path.write_text(json.dumps(existing, indent=4) + "\n")
285
+ print(f" Updated {_rel(settings_path)}")
286
+ else:
287
+ settings_path.write_text(json.dumps(defaults, indent=4) + "\n")
288
+ print(f" Created {_rel(settings_path)}")
289
+
290
+ # --- extensions.json (merge recommendations) ---
291
+ extensions_path = vscode_dir / "extensions.json"
292
+ ext_defaults = json.loads((scaffold / "extensions.json").read_text())
293
+ recommended = ext_defaults["recommendations"]
294
+ if extensions_path.exists():
295
+ ext_data = json.loads(extensions_path.read_text())
296
+ existing_recs = ext_data.get("recommendations", [])
297
+ for ext_id in recommended:
298
+ if ext_id not in existing_recs:
299
+ existing_recs.append(ext_id)
300
+ ext_data["recommendations"] = existing_recs
301
+ extensions_path.write_text(json.dumps(ext_data, indent=4) + "\n")
302
+ print(f" Updated {_rel(extensions_path)}")
303
+ else:
304
+ extensions_path.write_text(json.dumps(ext_defaults, indent=4) + "\n")
305
+ print(f" Created {_rel(extensions_path)}")
306
+
307
+ # --- python.code-snippets ---
308
+ snippets_path = vscode_dir / "python.code-snippets"
309
+ snippets_path.write_text((scaffold / "python.code-snippets").read_text())
310
+ print(f" Created {_rel(snippets_path)}")
311
+
312
+ # --- templates/pyxflow_view.py ---
313
+ templates_dir = vscode_dir / "templates"
314
+ templates_dir.mkdir(exist_ok=True)
315
+ template_dest = templates_dir / "pyxflow_view.py"
316
+ template_dest.write_text((scaffold / "templates" / "pyxflow_view.py").read_text())
317
+ print(f" Created {_rel(template_dest)}")
318
+
319
+ # --- Install extensions ---
320
+ code_cmd = shutil.which("code")
321
+ if code_cmd:
322
+ for ext_id in recommended:
323
+ try:
324
+ subprocess.run(
325
+ [code_cmd, "--install-extension", ext_id],
326
+ capture_output=True, timeout=30,
327
+ )
328
+ print(f" Installed extension {ext_id}")
329
+ except (subprocess.TimeoutExpired, OSError):
330
+ print(f" Warning: failed to install {ext_id}")
331
+ else:
332
+ print(" Warning: 'code' CLI not found -- install extensions manually in VSCode")
333
+
334
+ print("\n VSCode configuration ready!")
335
+
336
+
337
+ def _ensure_importable(views: str) -> None:
338
+ """Make sure the views module is importable from the current directory."""
339
+ cwd = os.getcwd()
340
+ package = views.rsplit(".", 1)[0]
341
+ package_dir = os.path.join(cwd, package.replace(".", os.sep))
342
+ if os.path.isdir(package_dir):
343
+ # Package is a subdirectory of cwd (e.g. myapp/views/)
344
+ if cwd not in sys.path:
345
+ sys.path.insert(0, cwd)
346
+ elif os.path.isdir(os.path.join(cwd, "views")):
347
+ # Package is cwd itself (views/ at root, from --setup without name).
348
+ # Directory may have hyphens (app-1) while module uses underscores (app_1).
349
+ # Inject cwd as a synthetic package so `import app_1.views` works.
350
+ import types
351
+ pkg_mod = types.ModuleType(package)
352
+ pkg_mod.__path__ = [cwd]
353
+ pkg_mod.__package__ = package
354
+ sys.modules[package] = pkg_mod
355
+ # Also add cwd to sys.path so sibling packages (e.g. lib/) are importable.
356
+ if cwd not in sys.path:
357
+ sys.path.insert(0, cwd)
358
+ else:
359
+ if cwd not in sys.path:
360
+ sys.path.insert(0, cwd)
361
+
362
+
363
+ def main_deprecated():
364
+ """Deprecated CLI entry point (``vaadin`` command)."""
365
+ print("WARNING: 'vaadin' command is deprecated. Use 'pyxflow' instead.", file=sys.stderr)
366
+ main()
367
+
368
+
369
+ def main():
370
+ """CLI entry point: ``pyxflow [app_module] [--dev] [--debug]``."""
371
+ if len(sys.argv) >= 2 and sys.argv[1] in ("-h", "--help"):
372
+ _usage()
373
+
374
+ # Parse all arguments: find module name (first non-flag) and flags.
375
+ # "bundle" is a subcommand alias for "--bundle"
376
+ _SUBCOMMANDS = {"bundle": "--bundle"}
377
+ # Flags that consume the next token as their value:
378
+ _VALUE_FLAGS = {"--vaadin-version", "--port", "--host"}
379
+ args = sys.argv[1:]
380
+ views = None
381
+ rest: list[str] = []
382
+ skip_next = False
383
+ for arg in args:
384
+ if skip_next:
385
+ rest.append(arg)
386
+ skip_next = False
387
+ elif arg in _VALUE_FLAGS:
388
+ rest.append(arg)
389
+ skip_next = True
390
+ elif arg in _SUBCOMMANDS:
391
+ rest.append(_SUBCOMMANDS[arg])
392
+ elif views is None and not arg.startswith("-"):
393
+ views = arg
394
+ else:
395
+ rest.append(arg)
396
+
397
+ if "--setup" in rest:
398
+ _setup_project(views)
399
+ _setup_vscode()
400
+ sys.exit(0)
401
+
402
+ if "--vscode" in rest:
403
+ _setup_vscode()
404
+ sys.exit(0)
405
+
406
+ if "--bundle" in rest:
407
+ from pathlib import Path
408
+ from pyxflow.bundle_generator import (
409
+ generate_and_build, copy_from_jars, _DEFAULT_VAADIN_VERSION,
410
+ )
411
+
412
+ keep = "--keep" in rest
413
+ use_build = "--build" in rest
414
+ optimized = "--optimized" in rest
415
+ vaadin_version = _DEFAULT_VAADIN_VERSION
416
+ if "--vaadin-version" in rest:
417
+ idx = rest.index("--vaadin-version")
418
+ vaadin_version = rest[idx + 1]
419
+
420
+ # Resolve app_dir from module name if given
421
+ app_dir = None
422
+ if views:
423
+ # "demo" → "demo/", "my_app" → "my_app/"
424
+ app_dir = Path.cwd() / views.replace(".", os.sep).replace("-", "_")
425
+
426
+ cwd = os.getcwd()
427
+ if cwd not in sys.path:
428
+ sys.path.insert(0, cwd)
429
+
430
+ if use_build:
431
+ generate_and_build(app_dir=app_dir, keep=keep, vaadin_version=vaadin_version, optimized=optimized)
432
+ else:
433
+ # Default: copy from Maven JARs (fast, no build needed)
434
+ is_pyxflow_dev = (Path.cwd() / "src" / "pyxflow").is_dir()
435
+ if is_pyxflow_dev:
436
+ bundle_dir = Path.cwd() / "src" / "pyxflow" / "bundle"
437
+ elif app_dir:
438
+ bundle_dir = app_dir / "bundle"
439
+ else:
440
+ print("ERROR: Cannot determine bundle output directory.")
441
+ print(" Run from a pyxflow source tree (src/pyxflow/ exists)")
442
+ print(" or specify an app module: pyxflow <app_module> bundle")
443
+ sys.exit(1)
444
+
445
+ print("===================================================")
446
+ print(" Vaadin PyXFlow Bundle Generator (copy mode)")
447
+ print("===================================================")
448
+ print(f" Vaadin version: {vaadin_version}")
449
+ print(f" Bundle output: {bundle_dir}")
450
+ print()
451
+
452
+ copy_from_jars(bundle_dir, vaadin_version, optimized=optimized)
453
+
454
+ print()
455
+ print("===================================================")
456
+ print(" Bundle copied successfully!")
457
+ print("===================================================")
458
+ sys.exit(0)
459
+
460
+ if views is None or views == ".":
461
+ views = _auto_detect_app()
462
+ if views is None:
463
+ print(" No views/ directory found in the current directory.")
464
+ print()
465
+ print(" To scaffold a new project: vaadin --setup")
466
+ print(" Or create views/ manually: mkdir -p views && touch views/__init__.py")
467
+ print()
468
+ _usage()
469
+
470
+ if "." not in views:
471
+ views = f"{views}.views"
472
+
473
+ _ensure_importable(views)
474
+
475
+ port = 8080
476
+ host = "localhost"
477
+ if "--port" in rest:
478
+ idx = rest.index("--port")
479
+ port = int(rest[idx + 1])
480
+ rest = rest[:idx] + rest[idx + 2:]
481
+ if "--host" in rest:
482
+ idx = rest.index("--host")
483
+ host = rest[idx + 1]
484
+ rest = rest[:idx] + rest[idx + 2:]
485
+
486
+ debug = "--debug" in rest
487
+ dev = "--dev" in rest
488
+
489
+ if dev:
490
+ # Reuse FlowApp dev mode
491
+ app = FlowApp.__new__(FlowApp)
492
+ app._views = views
493
+ app._port = port
494
+ app._host = host
495
+ app._run_dev(debug)
496
+ else:
497
+ _serve(views, host, port, debug, dev=dev)
498
+
499
+
500
+ def _dev_serve():
501
+ """Entry point for dev-mode child process (reads config from env)."""
502
+ try:
503
+ cfg = json.loads(os.environ["_PYFLOW_APP"])
504
+ _ensure_importable(cfg["views"])
505
+ _serve(cfg["views"], cfg["host"], cfg["port"], cfg["debug"],
506
+ dev=True, socket_fd=cfg.get("socket_fd"))
507
+ except KeyboardInterrupt:
508
+ pass
509
+
510
+
511
+ def _serve(views: str, host: str, port: int, debug: bool, *, dev: bool = False, socket_fd: int | None = None):
512
+ import importlib
513
+ from pathlib import Path
514
+ from pyxflow.router import discover_views
515
+ from pyxflow.server.http_server import run_server, set_app_directory
516
+ import pyxflow.server.http_server as _http
517
+
518
+ _http._dev_mode = dev
519
+ _http._views_module = views
520
+ discover_views(views)
521
+
522
+ # Resolve app package directory (e.g. "demo.views" → demo/)
523
+ package = views.rsplit(".", 1)[0]
524
+ pkg_mod = importlib.import_module(package)
525
+ if getattr(pkg_mod, "__file__", None):
526
+ set_app_directory(Path(pkg_mod.__file__).parent)
527
+ elif hasattr(pkg_mod, "__path__"):
528
+ # Namespace or synthetic package — use __path__
529
+ set_app_directory(Path(list(pkg_mod.__path__)[0]))
530
+
531
+ if socket_fd is not None:
532
+ import socket
533
+ sock = socket.socket(fileno=socket_fd)
534
+ run_server(debug=debug, sock=sock)
535
+ else:
536
+ run_server(host=host, port=port, debug=debug)
@@ -0,0 +1,3 @@
1
+ const h=function(l){window.Vaadin=window.Vaadin||{},window.Vaadin.Flow=window.Vaadin.Flow||{};var r={},a={},t;typeof window.console===void 0||!window.location.search.match(/[&?]debug(&|$)/)?t=function(){}:typeof window.console.log=="function"?t=function(){window.console.log.apply(window.console,arguments)}:t=window.console.log;var f=function(n){var o=document.getElementById(n);if(!o)return!1;for(var i=0;i<o.childElementCount;i++){var e=o.childNodes[i].className;if(e&&e.indexOf("v-app-loading")!=-1)return!1}return!0};window.Vaadin=window.Vaadin||{},window.Vaadin.Flow=window.Vaadin.Flow||{},window.Vaadin.Flow.tryCatchWrapper=function(n,o){return function(){try{return n.apply(this,arguments)}catch(i){console.error(`There seems to be an error in ${o}:
2
+ ${i.message}
3
+ Please submit an issue to https://github.com/vaadin/flow-components/issues/new/choose`)}}},window.Vaadin.Flow.initApplication||(window.Vaadin.Flow.clients=window.Vaadin.Flow.clients||{},window.Vaadin.Flow.initApplication=function(n,o){var i=n.replace(/-\d+$/,"");if(r[n]){if(window.Vaadin&&window.Vaadin.Flow&&window.Vaadin.Flow.clients&&window.Vaadin.Flow.clients[i]&&window.Vaadin.Flow.clients[i].initializing)throw new Error("Application "+n+" is already being initialized");if(f(n)){if(l.appConfig.productionMode)throw new Error("Application "+n+" already initialized");for(var e=document.getElementById(n),w=0;w<e.childElementCount;w++)e.childNodes[w].remove();const s={getConfig:function(V){return o[V]}};return r[n]=s,a.client.callback?(t("Starting from bootstrap",n),a.client.callback(n)):(t("Setting pending startup",n),a.client.pendingApps.push(n)),r[n]}}t("init application",n,o),window.Vaadin.Flow.clients[i]={isActive:function(){return!0},initializing:!0,productionMode:p};var v=function(g){var s=o[g];return s},u={getConfig:v};r[n]=u;var d="client";return a[d]={pendingApps:[]},a[d].callback?(t("Starting from bootstrap",n),a[d].callback(n)):(t("Setting pending startup",n),a[d].pendingApps.push(n)),u},window.Vaadin.Flow.getAppIds=function(){var n=[];for(var o in r)Object.prototype.hasOwnProperty.call(r,o)&&n.push(o);return n},window.Vaadin.Flow.getApp=function(n){return r[n]},window.Vaadin.Flow.registerWidgetset=function(n,o){t("Widgetset registered",n);var i=a[n];if(i&&i.pendingApps){i.callback=o;for(var e=0;e<i.pendingApps.length;e++){var w=i.pendingApps[e];t("Starting from register widgetset",w),o(w)}i.pendingApps=null}}),t("Flow bootstrap loaded"),l.appConfig.productionMode&&typeof window.__gwtStatsEvent!="function"&&(window.Vaadin.Flow.gwtStatsEvents=[],window.__gwtStatsEvent=function(n){return window.Vaadin.Flow.gwtStatsEvents.push(n),!0});var c=l.appConfig,p=l.appConfig.productionMode;window.Vaadin.Flow.initApplication(c.appId,c)};export{h as init};