pywire 0.1.1__py3-none-any.whl → 0.1.3__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 (103) hide show
  1. pywire/__init__.py +2 -0
  2. pywire/cli/__init__.py +1 -0
  3. pywire/cli/generators.py +48 -0
  4. pywire/cli/main.py +309 -0
  5. pywire/cli/tui.py +563 -0
  6. pywire/cli/validate.py +26 -0
  7. pywire/client/.prettierignore +8 -0
  8. pywire/client/.prettierrc +7 -0
  9. pywire/client/build.mjs +73 -0
  10. pywire/client/eslint.config.js +46 -0
  11. pywire/client/package.json +39 -0
  12. pywire/client/pnpm-lock.yaml +2971 -0
  13. pywire/client/src/core/app.ts +263 -0
  14. pywire/client/src/core/dom-updater.test.ts +78 -0
  15. pywire/client/src/core/dom-updater.ts +321 -0
  16. pywire/client/src/core/index.ts +5 -0
  17. pywire/client/src/core/transport-manager.test.ts +179 -0
  18. pywire/client/src/core/transport-manager.ts +159 -0
  19. pywire/client/src/core/transports/base.ts +122 -0
  20. pywire/client/src/core/transports/http.ts +142 -0
  21. pywire/client/src/core/transports/index.ts +13 -0
  22. pywire/client/src/core/transports/websocket.ts +97 -0
  23. pywire/client/src/core/transports/webtransport.ts +149 -0
  24. pywire/client/src/dev/dev-app.ts +93 -0
  25. pywire/client/src/dev/error-trace.test.ts +97 -0
  26. pywire/client/src/dev/error-trace.ts +76 -0
  27. pywire/client/src/dev/index.ts +4 -0
  28. pywire/client/src/dev/status-overlay.ts +63 -0
  29. pywire/client/src/events/handler.test.ts +318 -0
  30. pywire/client/src/events/handler.ts +454 -0
  31. pywire/client/src/pywire.core.ts +22 -0
  32. pywire/client/src/pywire.dev.ts +27 -0
  33. pywire/client/tsconfig.json +17 -0
  34. pywire/client/vitest.config.ts +15 -0
  35. pywire/compiler/__init__.py +6 -0
  36. pywire/compiler/ast_nodes.py +304 -0
  37. pywire/compiler/attributes/__init__.py +6 -0
  38. pywire/compiler/attributes/base.py +24 -0
  39. pywire/compiler/attributes/conditional.py +37 -0
  40. pywire/compiler/attributes/events.py +55 -0
  41. pywire/compiler/attributes/form.py +37 -0
  42. pywire/compiler/attributes/loop.py +75 -0
  43. pywire/compiler/attributes/reactive.py +34 -0
  44. pywire/compiler/build.py +28 -0
  45. pywire/compiler/build_artifacts.py +342 -0
  46. pywire/compiler/codegen/__init__.py +5 -0
  47. pywire/compiler/codegen/attributes/__init__.py +6 -0
  48. pywire/compiler/codegen/attributes/base.py +19 -0
  49. pywire/compiler/codegen/attributes/events.py +35 -0
  50. pywire/compiler/codegen/directives/__init__.py +6 -0
  51. pywire/compiler/codegen/directives/base.py +16 -0
  52. pywire/compiler/codegen/directives/path.py +53 -0
  53. pywire/compiler/codegen/generator.py +2341 -0
  54. pywire/compiler/codegen/template.py +2178 -0
  55. pywire/compiler/directives/__init__.py +7 -0
  56. pywire/compiler/directives/base.py +20 -0
  57. pywire/compiler/directives/component.py +33 -0
  58. pywire/compiler/directives/context.py +93 -0
  59. pywire/compiler/directives/layout.py +49 -0
  60. pywire/compiler/directives/no_spa.py +24 -0
  61. pywire/compiler/directives/path.py +71 -0
  62. pywire/compiler/directives/props.py +88 -0
  63. pywire/compiler/exceptions.py +19 -0
  64. pywire/compiler/interpolation/__init__.py +6 -0
  65. pywire/compiler/interpolation/base.py +28 -0
  66. pywire/compiler/interpolation/jinja.py +272 -0
  67. pywire/compiler/parser.py +750 -0
  68. pywire/compiler/paths.py +29 -0
  69. pywire/compiler/preprocessor.py +43 -0
  70. pywire/core/wire.py +119 -0
  71. pywire/py.typed +0 -0
  72. pywire/runtime/__init__.py +7 -0
  73. pywire/runtime/aioquic_server.py +194 -0
  74. pywire/runtime/app.py +901 -0
  75. pywire/runtime/compile_error_page.py +195 -0
  76. pywire/runtime/debug.py +203 -0
  77. pywire/runtime/dev_server.py +433 -0
  78. pywire/runtime/dev_server.py.broken +268 -0
  79. pywire/runtime/error_page.py +64 -0
  80. pywire/runtime/error_renderer.py +23 -0
  81. pywire/runtime/escape.py +23 -0
  82. pywire/runtime/files.py +40 -0
  83. pywire/runtime/helpers.py +97 -0
  84. pywire/runtime/http_transport.py +253 -0
  85. pywire/runtime/loader.py +272 -0
  86. pywire/runtime/logging.py +72 -0
  87. pywire/runtime/page.py +384 -0
  88. pywire/runtime/pydantic_integration.py +52 -0
  89. pywire/runtime/router.py +229 -0
  90. pywire/runtime/server.py +25 -0
  91. pywire/runtime/style_collector.py +31 -0
  92. pywire/runtime/upload_manager.py +76 -0
  93. pywire/runtime/validation.py +449 -0
  94. pywire/runtime/websocket.py +665 -0
  95. pywire/runtime/webtransport_handler.py +195 -0
  96. pywire/static/pywire.core.min.js +3 -0
  97. pywire/static/pywire.dev.min.js +20 -0
  98. {pywire-0.1.1.dist-info → pywire-0.1.3.dist-info}/METADATA +1 -1
  99. pywire-0.1.3.dist-info/RECORD +106 -0
  100. pywire-0.1.1.dist-info/RECORD +0 -9
  101. {pywire-0.1.1.dist-info → pywire-0.1.3.dist-info}/WHEEL +0 -0
  102. {pywire-0.1.1.dist-info → pywire-0.1.3.dist-info}/entry_points.txt +0 -0
  103. {pywire-0.1.1.dist-info → pywire-0.1.3.dist-info}/licenses/LICENSE +0 -0
pywire/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .runtime.app import PyWire as PyWire
2
+ from .core.wire import wire as wire
pywire/cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """CLI module."""
@@ -0,0 +1,48 @@
1
+ """Code generators for scaffolding."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def generate_page(name: str) -> None:
7
+ """Generate a new page."""
8
+ pages_dir = Path("pages")
9
+ pages_dir.mkdir(exist_ok=True)
10
+
11
+ page_file = pages_dir / f"{name}.pywire"
12
+
13
+ if page_file.exists():
14
+ raise ValueError(f"Page {name} already exists")
15
+
16
+ template = f"""!path {{ '{name}': '/{name}' }}
17
+
18
+ # Page code here
19
+
20
+ ---html---
21
+ <div>
22
+ <h1>{name.title()} Page</h1>
23
+ <p>Welcome to the {name} page!</p>
24
+ </div>
25
+ """
26
+
27
+ page_file.write_text(template)
28
+
29
+
30
+ def generate_component(name: str) -> None:
31
+ """Generate a new component."""
32
+ components_dir = Path("components")
33
+ components_dir.mkdir(exist_ok=True)
34
+
35
+ component_file = components_dir / f"{name}.pywire"
36
+
37
+ if component_file.exists():
38
+ raise ValueError(f"Component {name} already exists")
39
+
40
+ template = f"""# Component code here
41
+
42
+ ---html---
43
+ <div class="{name}">
44
+ <!-- Component code here -->
45
+ </div>
46
+ """
47
+
48
+ component_file.write_text(template)
pywire/cli/main.py ADDED
@@ -0,0 +1,309 @@
1
+ """Main CLI entry point."""
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Any, Optional
7
+
8
+ import rich.panel
9
+ import rich_click as click
10
+
11
+ # Astro-like styling configuration (Cyan Theme)
12
+ click.rich_click.USE_RICH_MARKUP = True
13
+ click.rich_click.STYLE_HELPTEXT_FIRST = True
14
+ click.rich_click.STYLE_COMMANDS_TABLE_SHOW_LINES = False
15
+ click.rich_click.STYLE_COMMANDS_TABLE_PAD_EDGE = False
16
+ click.rich_click.STYLE_COMMANDS_TABLE_BOX = None
17
+ click.rich_click.STYLE_COMMANDS_TABLE_EXPAND = False
18
+ click.rich_click.STYLE_OPTIONS_TABLE_EXPAND = False
19
+ click.rich_click.STYLE_COMMANDS_TABLE_HEADER = "bold magenta"
20
+ click.rich_click.STYLE_COMMANDS_TABLE_COLUMN_WIDTH_RATIO = None
21
+ click.rich_click.SHOW_ARGUMENTS = True
22
+ click.rich_click.GROUP_ARGUMENTS_OPTIONS = True
23
+ click.rich_click.STYLE_ERRORS_SUGGESTION = "magenta italic"
24
+ click.rich_click.ERRORS_SUGGESTION = "Try running 'pywire --help' for more information."
25
+ click.rich_click.ERRORS_EPILOGUE = "To find out more, visit [link=https://github.com/pywire/pywire]https://github.com/pywire/pywire[/link]"
26
+ click.rich_click.STYLE_OPTIONS_TABLE_BOX = None
27
+ click.rich_click.STYLE_COMMANDS_PANEL_BOX = None
28
+ click.rich_click.STYLE_OPTIONS_PANEL_BOX = None
29
+
30
+ # Cyan theme
31
+ click.rich_click.STYLE_HEADER_TEXT = "bold cyan"
32
+ click.rich_click.STYLE_OPTION = "cyan"
33
+ click.rich_click.STYLE_SWITCH = "cyan"
34
+ click.rich_click.STYLE_METAVAR = "dim white"
35
+ click.rich_click.STYLE_USAGE_COMMAND = "cyan"
36
+ click.rich_click.STYLE_USAGE = "dim"
37
+
38
+ # Grouping options and commands
39
+ click.rich_click.OPTION_GROUPS = {
40
+ "pywire": [
41
+ {
42
+ "name": "Global Flags",
43
+ "options": ["--help", "--version"],
44
+ }
45
+ ]
46
+ }
47
+
48
+ click.rich_click.COMMAND_GROUPS = {
49
+ "pywire": [
50
+ {
51
+ "name": "Commands",
52
+ "commands": ["dev", "run", "build"],
53
+ }
54
+ ]
55
+ }
56
+
57
+
58
+ def import_app(app_str: str) -> Any:
59
+ """Import application from string (e.g. 'main:app')."""
60
+ if ":" not in app_str:
61
+ raise click.BadParameter("App must be in format 'module:app'", param_hint="APP")
62
+
63
+ module_name, app_name = app_str.split(":", 1)
64
+
65
+ # Add current directory to path so we can import local modules
66
+ sys.path.insert(0, os.getcwd())
67
+
68
+ try:
69
+ import importlib
70
+
71
+ module = importlib.import_module(module_name)
72
+ except ImportError as e:
73
+ raise click.BadParameter(
74
+ f"Could not import module '{module_name}': {e}", param_hint="APP"
75
+ )
76
+
77
+ try:
78
+ app = getattr(module, app_name)
79
+ except AttributeError:
80
+ raise click.BadParameter(
81
+ f"Attribute '{app_name}' not found in module '{module_name}'",
82
+ param_hint="APP",
83
+ )
84
+
85
+ return app
86
+
87
+
88
+ def _discover_app_str() -> str:
89
+ """Try to discover the app string automatically."""
90
+ cwd = Path(os.getcwd())
91
+
92
+ # Priority: main.py, app.py, api.py
93
+ # Also check src/ directory
94
+ search_paths = [cwd, cwd / "src"]
95
+
96
+ for path in search_paths:
97
+ if not path.exists():
98
+ continue
99
+
100
+ for filename in ["main.py", "app.py", "api.py"]:
101
+ if (path / filename).exists():
102
+ # Check for common app instance names: app, api
103
+ module_name = filename[:-3]
104
+
105
+ # Construct module path (e.g. src.main)
106
+ if path.name == "src":
107
+ module_path = f"src.{module_name}"
108
+ else:
109
+ module_path = module_name
110
+
111
+ # Simple check: try to import and look for app
112
+ try:
113
+ sys.path.insert(0, str(cwd))
114
+ import importlib
115
+
116
+ module = importlib.import_module(module_path)
117
+
118
+ if hasattr(module, "app"):
119
+ return f"{module_path}:app"
120
+ if hasattr(module, "api"):
121
+ return f"{module_path}:api"
122
+
123
+ except ImportError:
124
+ continue
125
+
126
+ raise click.UsageError(
127
+ "Could not auto-discover app. Please provide 'APP' argument (e.g. 'main:app')."
128
+ )
129
+
130
+
131
+ # Workaround: rich-click wraps tables in Panels which default to expand=True.
132
+ # We monkeypatch Panel to default expand=False to allow natural resizing.
133
+ original_panel_init = rich.panel.Panel.__init__
134
+
135
+
136
+ def panel_init(self, *args, **kwargs):
137
+ kwargs.setdefault("expand", False)
138
+ original_panel_init(self, *args, **kwargs)
139
+
140
+
141
+ rich.panel.Panel.__init__ = panel_init # type: ignore[method-assign]
142
+
143
+
144
+ @click.group()
145
+ @click.version_option()
146
+ def cli() -> None:
147
+ """
148
+ [bold white on cyan] pywire [/] [bold cyan]v0.1.3[/] Build faster python web apps.
149
+
150
+ Run [bold cyan]pywire dev APP[/] to start development server.
151
+ Run [bold cyan]pywire run APP[/] to start production server.
152
+
153
+ [dim]APP should be a string in format 'module:instance', e.g. 'src.main:app' or 'main:app'
154
+ If not provided, pywire tries to discover it in main.py, app.py, etc.[/dim]
155
+ """
156
+ pass
157
+
158
+
159
+ @cli.command()
160
+ @click.argument("app", required=False)
161
+ @click.option("--host", default="127.0.0.1", help="Host to bind to")
162
+ @click.option("--port", default=3000, type=int, help="Port to bind to")
163
+ @click.option("--ssl-keyfile", default=None, help="SSL key file")
164
+ @click.option("--ssl-certfile", default=None, help="SSL certificate file")
165
+ @click.option("--env-file", default=None, help="Environment configuration file")
166
+ @click.option("--no-tui", is_flag=True, help="Disable TUI dashboard")
167
+ def dev(
168
+ app: Optional[str],
169
+ host: str,
170
+ port: int,
171
+ ssl_keyfile: Optional[str],
172
+ ssl_certfile: Optional[str],
173
+ env_file: Optional[str],
174
+ no_tui: bool,
175
+ ) -> None:
176
+ """Start development server."""
177
+ import asyncio
178
+
179
+ from pywire.runtime.dev_server import run_dev_server
180
+
181
+ if not app:
182
+ app = _discover_app_str()
183
+ if no_tui:
184
+ click.echo(f"🔍 Auto-discovered app: {app}")
185
+
186
+ # Verify import
187
+ import_app(app)
188
+
189
+ if no_tui:
190
+ click.echo(f"🚀 Starting pywire dev server on http://{host}:{port}")
191
+ if ssl_certfile:
192
+ click.echo("🔒 SSL enabled")
193
+
194
+ asyncio.run(
195
+ run_dev_server(
196
+ app_str=app, # Pass string for reloadability hooks if needed
197
+ host=host,
198
+ port=port,
199
+ ssl_keyfile=ssl_keyfile,
200
+ ssl_certfile=ssl_certfile,
201
+ )
202
+ )
203
+ else:
204
+ from pywire.cli.tui import start_tui
205
+
206
+ start_tui(
207
+ app_path=app,
208
+ host=host,
209
+ port=port,
210
+ ssl_keyfile=ssl_keyfile,
211
+ ssl_certfile=ssl_certfile,
212
+ env_file=env_file,
213
+ )
214
+
215
+
216
+ @cli.command()
217
+ @click.argument("app", required=False)
218
+ @click.option(
219
+ "--optimize",
220
+ is_flag=True,
221
+ help="Compile bytecode artifacts for faster import.",
222
+ )
223
+ @click.option(
224
+ "--out-dir",
225
+ default=".pywire/build",
226
+ help="Output directory for build artifacts.",
227
+ )
228
+ @click.option(
229
+ "--pages-dir",
230
+ default=None,
231
+ help="Override pages directory (default: app.pages_dir).",
232
+ )
233
+ def build(
234
+ app: Optional[str], optimize: bool, out_dir: str, pages_dir: Optional[str]
235
+ ) -> None:
236
+ """Build the application for production."""
237
+ if not app:
238
+ app = _discover_app_str()
239
+
240
+ click.echo(f"🔨 Building {app}...")
241
+
242
+ app_instance = import_app(app)
243
+
244
+ if pages_dir:
245
+ resolved_pages_dir = Path(pages_dir)
246
+ elif hasattr(app_instance, "pages_dir"):
247
+ resolved_pages_dir = Path(app_instance.pages_dir)
248
+ else:
249
+ resolved_pages_dir = Path("pages")
250
+
251
+ from pywire.compiler.build import build_project
252
+
253
+ summary = build_project(
254
+ optimize=optimize,
255
+ pages_dir=resolved_pages_dir,
256
+ out_dir=Path(out_dir),
257
+ )
258
+
259
+ click.echo(
260
+ "✅ Build complete "
261
+ f"(pages={summary.pages}, layouts={summary.layouts}, "
262
+ f"components={summary.components}, out={summary.out_dir})"
263
+ )
264
+
265
+
266
+ @cli.command()
267
+ @click.argument("app", required=False)
268
+ @click.option("--host", default="0.0.0.0", help="Host to bind to")
269
+ @click.option("--port", default=8000, type=int, help="Port to bind to")
270
+ @click.option("--workers", default=None, type=int, help="Number of worker processes")
271
+ @click.option("--no-access-log", is_flag=True, help="Disable access logging")
272
+ def run(
273
+ app: Optional[str],
274
+ host: str,
275
+ port: int,
276
+ workers: Optional[int],
277
+ no_access_log: bool,
278
+ ) -> None:
279
+ """Run production server using Uvicorn."""
280
+ import multiprocessing
281
+
282
+ import uvicorn
283
+
284
+ if not app:
285
+ app = _discover_app_str()
286
+ click.echo(f"🔍 Auto-discovered app: {app}")
287
+
288
+ if workers is None:
289
+ workers = (multiprocessing.cpu_count() * 2) + 1
290
+
291
+ click.echo(f"🚀 Starting production server for {app}")
292
+ click.echo(f"🌍 Listening on http://{host}:{port}")
293
+ click.echo(f"👷 Workers: {workers}")
294
+
295
+ # Locate the app object to verify, but pass string to uvicorn
296
+ import_app(app)
297
+
298
+ uvicorn.run(
299
+ app,
300
+ host=host,
301
+ port=port,
302
+ workers=workers,
303
+ access_log=not no_access_log,
304
+ factory=False,
305
+ )
306
+
307
+
308
+ if __name__ == "__main__":
309
+ cli()