pulse-framework 0.1.51__py3-none-any.whl → 0.1.53__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.
- pulse/__init__.py +542 -562
- pulse/_examples.py +29 -0
- pulse/app.py +0 -14
- pulse/cli/cmd.py +96 -80
- pulse/cli/dependencies.py +10 -41
- pulse/cli/folder_lock.py +3 -3
- pulse/cli/helpers.py +40 -67
- pulse/cli/logging.py +102 -0
- pulse/cli/packages.py +16 -0
- pulse/cli/processes.py +40 -23
- pulse/codegen/codegen.py +70 -35
- pulse/codegen/js.py +2 -4
- pulse/codegen/templates/route.py +94 -146
- pulse/component.py +115 -0
- pulse/components/for_.py +1 -1
- pulse/components/if_.py +1 -1
- pulse/components/react_router.py +16 -22
- pulse/{html → dom}/events.py +1 -1
- pulse/{html → dom}/props.py +6 -6
- pulse/{html → dom}/tags.py +11 -11
- pulse/dom/tags.pyi +480 -0
- pulse/form.py +7 -6
- pulse/hooks/init.py +1 -13
- pulse/js/__init__.py +37 -41
- pulse/js/__init__.pyi +22 -2
- pulse/js/_types.py +5 -3
- pulse/js/array.py +121 -38
- pulse/js/console.py +9 -9
- pulse/js/date.py +22 -19
- pulse/js/document.py +8 -4
- pulse/js/error.py +12 -14
- pulse/js/json.py +4 -3
- pulse/js/map.py +17 -7
- pulse/js/math.py +2 -2
- pulse/js/navigator.py +4 -4
- pulse/js/number.py +8 -8
- pulse/js/object.py +9 -13
- pulse/js/promise.py +25 -9
- pulse/js/regexp.py +6 -6
- pulse/js/set.py +20 -8
- pulse/js/string.py +7 -7
- pulse/js/weakmap.py +6 -6
- pulse/js/weakset.py +6 -6
- pulse/js/window.py +17 -14
- pulse/messages.py +1 -4
- pulse/react_component.py +3 -1001
- pulse/render_session.py +74 -66
- pulse/renderer.py +311 -238
- pulse/routing.py +1 -10
- pulse/transpiler/__init__.py +84 -114
- pulse/transpiler/builtins.py +661 -343
- pulse/transpiler/errors.py +78 -2
- pulse/transpiler/function.py +463 -133
- pulse/transpiler/id.py +18 -0
- pulse/transpiler/imports.py +230 -325
- pulse/transpiler/js_module.py +218 -209
- pulse/transpiler/modules/__init__.py +16 -13
- pulse/transpiler/modules/asyncio.py +45 -26
- pulse/transpiler/modules/json.py +12 -8
- pulse/transpiler/modules/math.py +161 -216
- pulse/transpiler/modules/pulse/__init__.py +5 -0
- pulse/transpiler/modules/pulse/tags.py +231 -0
- pulse/transpiler/modules/typing.py +33 -28
- pulse/transpiler/nodes.py +1607 -923
- pulse/transpiler/py_module.py +118 -95
- pulse/transpiler/react_component.py +51 -0
- pulse/transpiler/transpiler.py +593 -437
- pulse/transpiler/vdom.py +255 -0
- {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.53.dist-info}/METADATA +1 -1
- pulse_framework-0.1.53.dist-info/RECORD +120 -0
- pulse/html/tags.pyi +0 -470
- pulse/transpiler/constants.py +0 -110
- pulse/transpiler/context.py +0 -26
- pulse/transpiler/ids.py +0 -16
- pulse/transpiler/modules/re.py +0 -466
- pulse/transpiler/modules/tags.py +0 -268
- pulse/transpiler/utils.py +0 -4
- pulse/vdom.py +0 -599
- pulse_framework-0.1.51.dist-info/RECORD +0 -119
- /pulse/{html → dom}/__init__.py +0 -0
- /pulse/{html → dom}/elements.py +0 -0
- /pulse/{html → dom}/svg.py +0 -0
- {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.53.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.53.dist-info}/entry_points.txt +0 -0
pulse/_examples.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
def square(x: float) -> float:
|
|
2
|
+
"""Return the square of x."""
|
|
3
|
+
return x * x
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def cube(x: float) -> float:
|
|
7
|
+
"""Return the cube of x."""
|
|
8
|
+
return x * x * x
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def factorial(n: int) -> int:
|
|
12
|
+
"""Calculate factorial recursively."""
|
|
13
|
+
if n <= 1:
|
|
14
|
+
return 1
|
|
15
|
+
return n * factorial(n - 1)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_even(n: int) -> bool:
|
|
19
|
+
"""Check if n is even."""
|
|
20
|
+
return n % 2 == 0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def clamp(value: float, min_val: float, max_val: float) -> float:
|
|
24
|
+
"""Clamp value between min_val and max_val."""
|
|
25
|
+
if value < min_val:
|
|
26
|
+
return min_val
|
|
27
|
+
if value > max_val:
|
|
28
|
+
return max_val
|
|
29
|
+
return value
|
pulse/app.py
CHANGED
|
@@ -69,7 +69,6 @@ from pulse.middleware import (
|
|
|
69
69
|
)
|
|
70
70
|
from pulse.plugin import Plugin
|
|
71
71
|
from pulse.proxy import ReactProxy
|
|
72
|
-
from pulse.react_component import ReactComponent, registered_react_components
|
|
73
72
|
from pulse.render_session import RenderSession
|
|
74
73
|
from pulse.request import PulseRequest
|
|
75
74
|
from pulse.routing import Layout, Route, RouteTree
|
|
@@ -208,8 +207,6 @@ class App:
|
|
|
208
207
|
for plugin in self.plugins:
|
|
209
208
|
all_routes.extend(plugin.routes())
|
|
210
209
|
|
|
211
|
-
# Auto-add React components to all routes
|
|
212
|
-
add_react_components(all_routes, registered_react_components())
|
|
213
210
|
# RouteTree filters routes based on dev flag and environment during construction
|
|
214
211
|
self.routes = RouteTree(all_routes)
|
|
215
212
|
self.not_found = not_found
|
|
@@ -988,14 +985,3 @@ class App:
|
|
|
988
985
|
# We don't want to wait for this to resolve
|
|
989
986
|
create_task(render.call_api(f"{self.api_prefix}/set-cookies", method="GET"))
|
|
990
987
|
sess.scheduled_cookie_refresh = True
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
def add_react_components(
|
|
994
|
-
routes: Sequence[Route | Layout],
|
|
995
|
-
components: list[ReactComponent[Any]],
|
|
996
|
-
):
|
|
997
|
-
for route in routes:
|
|
998
|
-
if route.components is None:
|
|
999
|
-
route.components = components
|
|
1000
|
-
if route.children:
|
|
1001
|
-
add_react_components(route.children, components)
|
pulse/cli/cmd.py
CHANGED
|
@@ -15,7 +15,6 @@ from pathlib import Path
|
|
|
15
15
|
from typing import Callable, cast
|
|
16
16
|
|
|
17
17
|
import typer
|
|
18
|
-
from rich.console import Console
|
|
19
18
|
|
|
20
19
|
from pulse.cli.dependencies import (
|
|
21
20
|
DependencyError,
|
|
@@ -26,6 +25,7 @@ from pulse.cli.dependencies import (
|
|
|
26
25
|
)
|
|
27
26
|
from pulse.cli.folder_lock import FolderLock
|
|
28
27
|
from pulse.cli.helpers import load_app_from_target
|
|
28
|
+
from pulse.cli.logging import CLILogger
|
|
29
29
|
from pulse.cli.models import AppLoadResult, CommandSpec
|
|
30
30
|
from pulse.cli.processes import execute_commands
|
|
31
31
|
from pulse.cli.secrets import resolve_dev_secret
|
|
@@ -65,9 +65,11 @@ def run(
|
|
|
65
65
|
),
|
|
66
66
|
port: int = typer.Option(8000, "--port", help="Port uvicorn binds to"),
|
|
67
67
|
# Env flags
|
|
68
|
-
dev: bool = typer.Option(False, "--dev", help="Run in development
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
dev: bool = typer.Option(False, "--dev", help="Run in development mode"),
|
|
69
|
+
prod: bool = typer.Option(False, "--prod", help="Run in production mode"),
|
|
70
|
+
plain: bool = typer.Option(
|
|
71
|
+
False, "--plain", help="Use plain output without colors or emojis"
|
|
72
|
+
),
|
|
71
73
|
server_only: bool = typer.Option(False, "--server-only", "--backend-only"),
|
|
72
74
|
web_only: bool = typer.Option(False, "--web-only"),
|
|
73
75
|
react_server_address: str | None = typer.Option(
|
|
@@ -84,53 +86,49 @@ def run(
|
|
|
84
86
|
"""Run the Pulse server and web development server together."""
|
|
85
87
|
extra_flags = list(ctx.args)
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
typer.echo("❌ Please specify only one of --dev, --ci, or --prod.")
|
|
89
|
+
# Validate mode flags (dev is default if neither specified)
|
|
90
|
+
if dev and prod:
|
|
91
|
+
logger = CLILogger("dev", plain=plain)
|
|
92
|
+
logger.error("Please specify only one of --dev or --prod.")
|
|
92
93
|
raise typer.Exit(1)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if len(env_flags) == 1:
|
|
99
|
-
env.pulse_env = cast(PulseEnv, env_flags[0])
|
|
94
|
+
|
|
95
|
+
# Set mode: prod if specified, otherwise dev (default)
|
|
96
|
+
mode: PulseEnv = "prod" if prod else "dev"
|
|
97
|
+
env.pulse_env = mode
|
|
98
|
+
logger = CLILogger(mode, plain=plain)
|
|
100
99
|
|
|
101
100
|
# Turn on reload in dev only
|
|
102
101
|
if reload is None:
|
|
103
102
|
reload = env.pulse_env == "dev"
|
|
104
103
|
|
|
105
104
|
if server_only and web_only:
|
|
106
|
-
|
|
105
|
+
logger.error("Cannot use --server-only and --web-only at the same time.")
|
|
107
106
|
raise typer.Exit(1)
|
|
108
107
|
|
|
109
108
|
if find_port:
|
|
110
109
|
port = find_available_port(port)
|
|
111
110
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
app_ctx = load_app_from_target(app_file)
|
|
111
|
+
logger.print(f"Loading app from {app_file}")
|
|
112
|
+
app_ctx = load_app_from_target(app_file, logger)
|
|
115
113
|
_apply_app_context_to_env(app_ctx)
|
|
116
114
|
app_instance = app_ctx.app
|
|
117
115
|
|
|
118
116
|
is_single_server = app_instance.mode == "single-server"
|
|
119
117
|
if is_single_server:
|
|
120
|
-
|
|
118
|
+
logger.print("Single-server mode")
|
|
121
119
|
|
|
122
120
|
# In single-server + server-only mode, require explicit React server address
|
|
123
121
|
if is_single_server and server_only:
|
|
124
122
|
if not react_server_address:
|
|
125
|
-
|
|
126
|
-
"
|
|
123
|
+
logger.error(
|
|
124
|
+
"--react-server-address is required when using single-server mode with --server-only."
|
|
127
125
|
)
|
|
128
126
|
raise typer.Exit(1)
|
|
129
127
|
os.environ[ENV_PULSE_REACT_SERVER_ADDRESS] = react_server_address
|
|
130
128
|
|
|
131
129
|
web_root = app_instance.codegen.cfg.web_root
|
|
132
130
|
if not web_root.exists() and not server_only:
|
|
133
|
-
|
|
131
|
+
logger.error(f"Directory not found: {web_root.absolute()}")
|
|
134
132
|
raise typer.Exit(1)
|
|
135
133
|
|
|
136
134
|
dev_secret: str | None = None
|
|
@@ -146,10 +144,10 @@ def run(
|
|
|
146
144
|
pulse_version=PULSE_PY_VERSION,
|
|
147
145
|
)
|
|
148
146
|
except DependencyResolutionError as exc:
|
|
149
|
-
|
|
147
|
+
logger.error(str(exc))
|
|
150
148
|
raise typer.Exit(1) from None
|
|
151
149
|
except DependencyError as exc:
|
|
152
|
-
|
|
150
|
+
logger.error(str(exc))
|
|
153
151
|
raise typer.Exit(1) from None
|
|
154
152
|
|
|
155
153
|
if to_add:
|
|
@@ -159,9 +157,9 @@ def run(
|
|
|
159
157
|
pulse_version=PULSE_PY_VERSION,
|
|
160
158
|
)
|
|
161
159
|
if dep_plan:
|
|
162
|
-
_run_dependency_plan(
|
|
160
|
+
_run_dependency_plan(logger, web_root, dep_plan)
|
|
163
161
|
except subprocess.CalledProcessError:
|
|
164
|
-
|
|
162
|
+
logger.error("Failed to install web dependencies with Bun.")
|
|
165
163
|
raise typer.Exit(1) from None
|
|
166
164
|
|
|
167
165
|
server_args = extra_flags if not web_only else []
|
|
@@ -199,12 +197,7 @@ def run(
|
|
|
199
197
|
announced = True
|
|
200
198
|
protocol = "http" if address in ("127.0.0.1", "localhost") else "https"
|
|
201
199
|
server_url = f"{protocol}://{address}:{port}"
|
|
202
|
-
|
|
203
|
-
console.print(
|
|
204
|
-
f"✨ [bold green]Pulse running at:[/bold green] [bold cyan][link={server_url}]{server_url}[/link][/bold cyan]"
|
|
205
|
-
)
|
|
206
|
-
console.print(f" [dim]API: {server_url}/_pulse/...[/dim]")
|
|
207
|
-
console.print("")
|
|
200
|
+
logger.write_ready_announcement(address, port, server_url)
|
|
208
201
|
|
|
209
202
|
# Build web command first (when needed) so we can set PULSE_REACT_SERVER_ADDRESS
|
|
210
203
|
# before building the uvicorn command, which needs that env var
|
|
@@ -231,7 +224,6 @@ def run(
|
|
|
231
224
|
extra_args=server_args,
|
|
232
225
|
dev_secret=dev_secret,
|
|
233
226
|
server_only=server_only,
|
|
234
|
-
console=console,
|
|
235
227
|
web_root=web_root,
|
|
236
228
|
verbose=verbose,
|
|
237
229
|
ready_pattern=r"Application startup complete",
|
|
@@ -239,21 +231,15 @@ def run(
|
|
|
239
231
|
)
|
|
240
232
|
commands.append(server_cmd)
|
|
241
233
|
|
|
242
|
-
# Only add tags in dev mode to avoid breaking structured output (e.g., CloudWatch EMF metrics)
|
|
243
|
-
tag_colors = (
|
|
244
|
-
{"server": "cyan", "web": "orange1"} if env.pulse_env == "dev" else None
|
|
245
|
-
)
|
|
246
|
-
|
|
247
234
|
with FolderLock(web_root):
|
|
248
235
|
try:
|
|
249
236
|
exit_code = execute_commands(
|
|
250
237
|
commands,
|
|
251
|
-
|
|
252
|
-
tag_colors=tag_colors,
|
|
238
|
+
tag_mode=logger.get_tag_mode(),
|
|
253
239
|
)
|
|
254
240
|
raise typer.Exit(exit_code)
|
|
255
241
|
except RuntimeError as exc:
|
|
256
|
-
|
|
242
|
+
logger.error(str(exc))
|
|
257
243
|
raise typer.Exit(1) from None
|
|
258
244
|
|
|
259
245
|
|
|
@@ -266,42 +252,54 @@ def generate(
|
|
|
266
252
|
dev: bool = typer.Option(False, "--dev", help="Generate in development mode"),
|
|
267
253
|
ci: bool = typer.Option(False, "--ci", help="Generate in CI mode"),
|
|
268
254
|
prod: bool = typer.Option(False, "--prod", help="Generate in production mode"),
|
|
255
|
+
plain: bool = typer.Option(
|
|
256
|
+
False, "--plain", help="Use plain output without colors or emojis"
|
|
257
|
+
),
|
|
269
258
|
):
|
|
270
259
|
"""Generate TypeScript routes without starting the server."""
|
|
271
|
-
|
|
272
|
-
console.log("🔄 Generating TypeScript routes...")
|
|
273
|
-
|
|
260
|
+
# Validate mode flags
|
|
274
261
|
mode_flags = [
|
|
275
262
|
name for flag, name in [(dev, "dev"), (ci, "ci"), (prod, "prod")] if flag
|
|
276
263
|
]
|
|
277
264
|
if len(mode_flags) > 1:
|
|
278
|
-
|
|
265
|
+
logger = CLILogger("dev", plain=plain)
|
|
266
|
+
logger.error("Please specify only one of --dev, --ci, or --prod.")
|
|
279
267
|
raise typer.Exit(1)
|
|
280
|
-
if len(mode_flags) == 1:
|
|
281
|
-
env.pulse_env = cast(PulseEnv, mode_flags[0])
|
|
282
268
|
|
|
283
|
-
|
|
269
|
+
# Set mode: use specified mode, otherwise dev (default)
|
|
270
|
+
mode: PulseEnv = cast(PulseEnv, mode_flags[0]) if mode_flags else "dev"
|
|
271
|
+
env.pulse_env = mode
|
|
272
|
+
logger = CLILogger(mode, plain=plain)
|
|
273
|
+
|
|
274
|
+
logger.print(f"Generating routes from {app_file}")
|
|
284
275
|
env.codegen_disabled = False
|
|
285
|
-
app_ctx = load_app_from_target(app_file)
|
|
276
|
+
app_ctx = load_app_from_target(app_file, logger)
|
|
286
277
|
_apply_app_context_to_env(app_ctx)
|
|
287
278
|
app = app_ctx.app
|
|
288
|
-
console.log(f"📋 Found {len(app.routes.flat_tree)} routes")
|
|
289
279
|
|
|
290
280
|
# In CI or prod mode, server_address must be provided
|
|
291
281
|
if (ci or prod) and not app.server_address:
|
|
292
|
-
|
|
293
|
-
"
|
|
282
|
+
logger.error(
|
|
283
|
+
"server_address must be provided when generating in CI or production mode. "
|
|
294
284
|
+ "Set it in your App constructor or via the PULSE_SERVER_ADDRESS environment variable."
|
|
295
285
|
)
|
|
296
286
|
raise typer.Exit(1)
|
|
297
287
|
|
|
298
288
|
addr = app.server_address or "http://localhost:8000"
|
|
299
|
-
|
|
289
|
+
try:
|
|
290
|
+
app.run_codegen(addr)
|
|
291
|
+
except Exception:
|
|
292
|
+
logger.error("Failed to generate routes")
|
|
293
|
+
logger.print_exception()
|
|
294
|
+
raise typer.Exit(1) from None
|
|
300
295
|
|
|
301
|
-
|
|
302
|
-
|
|
296
|
+
route_count = len(app.routes.flat_tree)
|
|
297
|
+
if route_count > 0:
|
|
298
|
+
logger.success(
|
|
299
|
+
f"Generated {route_count} route{'s' if route_count != 1 else ''}"
|
|
300
|
+
)
|
|
303
301
|
else:
|
|
304
|
-
|
|
302
|
+
logger.warning("No routes found")
|
|
305
303
|
|
|
306
304
|
|
|
307
305
|
@cli.command("check")
|
|
@@ -312,18 +310,37 @@ def check(
|
|
|
312
310
|
fix: bool = typer.Option(
|
|
313
311
|
False, "--fix", help="Install missing or outdated dependencies"
|
|
314
312
|
),
|
|
313
|
+
# Mode flags
|
|
314
|
+
dev: bool = typer.Option(False, "--dev", help="Run in development mode"),
|
|
315
|
+
ci: bool = typer.Option(False, "--ci", help="Run in CI mode"),
|
|
316
|
+
prod: bool = typer.Option(False, "--prod", help="Run in production mode"),
|
|
317
|
+
plain: bool = typer.Option(
|
|
318
|
+
False, "--plain", help="Use plain output without colors or emojis"
|
|
319
|
+
),
|
|
315
320
|
):
|
|
316
321
|
"""Check if web project dependencies are in sync with Pulse app requirements."""
|
|
317
|
-
|
|
322
|
+
# Validate mode flags
|
|
323
|
+
mode_flags = [
|
|
324
|
+
name for flag, name in [(dev, "dev"), (ci, "ci"), (prod, "prod")] if flag
|
|
325
|
+
]
|
|
326
|
+
if len(mode_flags) > 1:
|
|
327
|
+
logger = CLILogger("dev", plain=plain)
|
|
328
|
+
logger.error("Please specify only one of --dev, --ci, or --prod.")
|
|
329
|
+
raise typer.Exit(1)
|
|
318
330
|
|
|
319
|
-
|
|
320
|
-
|
|
331
|
+
# Set mode: use specified mode, otherwise dev (default)
|
|
332
|
+
mode: PulseEnv = cast(PulseEnv, mode_flags[0]) if mode_flags else "dev"
|
|
333
|
+
env.pulse_env = mode
|
|
334
|
+
logger = CLILogger(mode, plain=plain)
|
|
335
|
+
|
|
336
|
+
logger.print(f"Checking dependencies for {app_file}")
|
|
337
|
+
app_ctx = load_app_from_target(app_file, logger)
|
|
321
338
|
_apply_app_context_to_env(app_ctx)
|
|
322
339
|
app_instance = app_ctx.app
|
|
323
340
|
|
|
324
341
|
web_root = app_instance.codegen.cfg.web_root
|
|
325
342
|
if not web_root.exists():
|
|
326
|
-
|
|
343
|
+
logger.error(f"Directory not found: {web_root.absolute()}")
|
|
327
344
|
raise typer.Exit(1)
|
|
328
345
|
|
|
329
346
|
try:
|
|
@@ -332,22 +349,22 @@ def check(
|
|
|
332
349
|
pulse_version=PULSE_PY_VERSION,
|
|
333
350
|
)
|
|
334
351
|
except DependencyResolutionError as exc:
|
|
335
|
-
|
|
352
|
+
logger.error(str(exc))
|
|
336
353
|
raise typer.Exit(1) from None
|
|
337
354
|
except DependencyError as exc:
|
|
338
|
-
|
|
355
|
+
logger.error(str(exc))
|
|
339
356
|
raise typer.Exit(1) from None
|
|
340
357
|
|
|
341
358
|
if not to_add:
|
|
342
|
-
|
|
359
|
+
logger.success("Dependencies in sync")
|
|
343
360
|
return
|
|
344
361
|
|
|
345
|
-
|
|
362
|
+
logger.print("Missing dependencies:")
|
|
346
363
|
for pkg in to_add:
|
|
347
|
-
|
|
364
|
+
logger.print(f" {pkg}")
|
|
348
365
|
|
|
349
366
|
if not fix:
|
|
350
|
-
|
|
367
|
+
logger.print("Run 'pulse check --fix' to install")
|
|
351
368
|
return
|
|
352
369
|
|
|
353
370
|
# Apply fix
|
|
@@ -357,10 +374,10 @@ def check(
|
|
|
357
374
|
pulse_version=PULSE_PY_VERSION,
|
|
358
375
|
)
|
|
359
376
|
if dep_plan:
|
|
360
|
-
_run_dependency_plan(
|
|
361
|
-
|
|
377
|
+
_run_dependency_plan(logger, web_root, dep_plan)
|
|
378
|
+
logger.success("Dependencies synced")
|
|
362
379
|
except subprocess.CalledProcessError:
|
|
363
|
-
|
|
380
|
+
logger.error("Failed to install web dependencies with Bun.")
|
|
364
381
|
raise typer.Exit(1) from None
|
|
365
382
|
|
|
366
383
|
|
|
@@ -373,7 +390,6 @@ def build_uvicorn_command(
|
|
|
373
390
|
extra_args: Sequence[str],
|
|
374
391
|
dev_secret: str | None,
|
|
375
392
|
server_only: bool,
|
|
376
|
-
console: Console,
|
|
377
393
|
web_root: Path,
|
|
378
394
|
verbose: bool = False,
|
|
379
395
|
ready_pattern: str | None = None,
|
|
@@ -500,13 +516,10 @@ def _apply_app_context_to_env(app_ctx: AppLoadResult) -> None:
|
|
|
500
516
|
|
|
501
517
|
|
|
502
518
|
def _run_dependency_plan(
|
|
503
|
-
|
|
519
|
+
logger: CLILogger, web_root: Path, plan: DependencyPlan
|
|
504
520
|
) -> None:
|
|
505
|
-
# command_display = " ".join(plan.command)
|
|
506
521
|
if plan.to_add:
|
|
507
|
-
|
|
508
|
-
else:
|
|
509
|
-
console.log(f"📦 Installing web dependencies in {web_root}")
|
|
522
|
+
logger.print(f"Installing dependencies in {web_root}")
|
|
510
523
|
subprocess.run(plan.command, cwd=web_root, check=True)
|
|
511
524
|
|
|
512
525
|
|
|
@@ -514,10 +527,13 @@ def main():
|
|
|
514
527
|
"""Main CLI entry point."""
|
|
515
528
|
try:
|
|
516
529
|
cli()
|
|
530
|
+
except SystemExit:
|
|
531
|
+
# Let typer.Exit and sys.exit propagate normally (no traceback)
|
|
532
|
+
raise
|
|
517
533
|
except Exception:
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
534
|
+
logger = CLILogger(env.pulse_env)
|
|
535
|
+
logger.print_exception()
|
|
536
|
+
sys.exit(1)
|
|
521
537
|
|
|
522
538
|
|
|
523
539
|
def production_flags():
|
pulse/cli/dependencies.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
-
from collections.abc import
|
|
4
|
+
from collections.abc import Sequence
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
8
7
|
|
|
9
8
|
from pulse.cli.packages import (
|
|
10
9
|
VersionConflict,
|
|
@@ -16,7 +15,7 @@ from pulse.cli.packages import (
|
|
|
16
15
|
resolve_versions,
|
|
17
16
|
spec_satisfies,
|
|
18
17
|
)
|
|
19
|
-
from pulse.
|
|
18
|
+
from pulse.transpiler.imports import get_registered_imports
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
def convert_pep440_to_semver(python_version: str) -> str:
|
|
@@ -90,51 +89,29 @@ def get_required_dependencies(
|
|
|
90
89
|
web_root: Path,
|
|
91
90
|
*,
|
|
92
91
|
pulse_version: str,
|
|
93
|
-
component_provider: Callable[
|
|
94
|
-
[], Iterable[ReactComponent[Any]]
|
|
95
|
-
] = registered_react_components,
|
|
96
92
|
) -> dict[str, str | None]:
|
|
97
93
|
"""Get the required dependencies for a Pulse app."""
|
|
98
94
|
if not web_root.exists():
|
|
99
95
|
raise DependencyError(f"Directory not found: {web_root}")
|
|
100
96
|
|
|
101
|
-
try:
|
|
102
|
-
components = list(component_provider())
|
|
103
|
-
except Exception as exc:
|
|
104
|
-
raise DependencyError("Unable to inspect registered React components") from exc
|
|
105
|
-
|
|
106
97
|
constraints: dict[str, list[str | None]] = {
|
|
107
98
|
"pulse-ui-client": [pulse_version],
|
|
108
99
|
}
|
|
109
100
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if src:
|
|
101
|
+
# New transpiler v2 imports
|
|
102
|
+
for imp in get_registered_imports():
|
|
103
|
+
if imp.src:
|
|
114
104
|
try:
|
|
115
|
-
spec = parse_install_spec(src)
|
|
105
|
+
spec = parse_install_spec(imp.src)
|
|
116
106
|
except ValueError as exc:
|
|
107
|
+
# We might want to be more lenient here or at least log it,
|
|
108
|
+
# but following existing pattern of raising DependencyError
|
|
117
109
|
raise DependencyError(str(exc)) from None
|
|
118
110
|
if spec:
|
|
119
111
|
name_only, ver = parse_dependency_spec(spec)
|
|
120
112
|
constraints.setdefault(name_only, []).append(ver)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
comp_version = getattr(comp, "version", None)
|
|
124
|
-
if comp_version and component_pkg_name:
|
|
125
|
-
constraints.setdefault(component_pkg_name, []).append(comp_version)
|
|
126
|
-
|
|
127
|
-
for extra in getattr(comp, "extra_imports", []):
|
|
128
|
-
extra_src = getattr(extra, "src", None) if extra is not None else None
|
|
129
|
-
if not extra_src:
|
|
130
|
-
continue
|
|
131
|
-
try:
|
|
132
|
-
spec2 = parse_install_spec(extra_src)
|
|
133
|
-
except ValueError as exc:
|
|
134
|
-
raise DependencyError(str(exc)) from None
|
|
135
|
-
if spec2:
|
|
136
|
-
name_only2, ver2 = parse_dependency_spec(spec2)
|
|
137
|
-
constraints.setdefault(name_only2, []).append(ver2)
|
|
113
|
+
if imp.version:
|
|
114
|
+
constraints.setdefault(name_only, []).append(imp.version)
|
|
138
115
|
|
|
139
116
|
try:
|
|
140
117
|
resolved = resolve_versions(constraints)
|
|
@@ -157,15 +134,11 @@ def check_web_dependencies(
|
|
|
157
134
|
web_root: Path,
|
|
158
135
|
*,
|
|
159
136
|
pulse_version: str,
|
|
160
|
-
component_provider: Callable[
|
|
161
|
-
[], Iterable[ReactComponent[Any]]
|
|
162
|
-
] = registered_react_components,
|
|
163
137
|
) -> list[str]:
|
|
164
138
|
"""Check if web dependencies are in sync and return list of packages that need to be added/updated."""
|
|
165
139
|
desired = get_required_dependencies(
|
|
166
140
|
web_root=web_root,
|
|
167
141
|
pulse_version=pulse_version,
|
|
168
|
-
component_provider=component_provider,
|
|
169
142
|
)
|
|
170
143
|
pkg_json = load_package_json(web_root)
|
|
171
144
|
|
|
@@ -195,15 +168,11 @@ def prepare_web_dependencies(
|
|
|
195
168
|
web_root: Path,
|
|
196
169
|
*,
|
|
197
170
|
pulse_version: str,
|
|
198
|
-
component_provider: Callable[
|
|
199
|
-
[], Iterable[ReactComponent[Any]]
|
|
200
|
-
] = registered_react_components,
|
|
201
171
|
) -> DependencyPlan | None:
|
|
202
172
|
"""Inspect registered components and return the Bun command needed to sync dependencies."""
|
|
203
173
|
to_add = check_web_dependencies(
|
|
204
174
|
web_root=web_root,
|
|
205
175
|
pulse_version=pulse_version,
|
|
206
|
-
component_provider=component_provider,
|
|
207
176
|
)
|
|
208
177
|
|
|
209
178
|
if to_add:
|
pulse/cli/folder_lock.py
CHANGED
|
@@ -90,7 +90,7 @@ def _remove_lock_file(lock_path: Path) -> None:
|
|
|
90
90
|
pass
|
|
91
91
|
|
|
92
92
|
|
|
93
|
-
def lock_path_for_web_root(web_root: Path, filename: str = ".pulse
|
|
93
|
+
def lock_path_for_web_root(web_root: Path, filename: str = ".pulse/lock") -> Path:
|
|
94
94
|
"""Return the lock file path for a given web root."""
|
|
95
95
|
return Path(web_root) / filename
|
|
96
96
|
|
|
@@ -110,13 +110,13 @@ class FolderLock:
|
|
|
110
110
|
pass
|
|
111
111
|
"""
|
|
112
112
|
|
|
113
|
-
def __init__(self, web_root: Path, *, filename: str = ".pulse
|
|
113
|
+
def __init__(self, web_root: Path, *, filename: str = ".pulse/lock"):
|
|
114
114
|
"""
|
|
115
115
|
Initialize FolderLock.
|
|
116
116
|
|
|
117
117
|
Args:
|
|
118
118
|
web_root: Path to the web root directory
|
|
119
|
-
filename: Name of the lock file (default: ".pulse
|
|
119
|
+
filename: Name of the lock file (default: ".pulse/lock")
|
|
120
120
|
"""
|
|
121
121
|
self.lock_path: Path = lock_path_for_web_root(web_root, filename)
|
|
122
122
|
|