stario 2.0.0__tar.gz

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 (41) hide show
  1. stario-2.0.0/PKG-INFO +51 -0
  2. stario-2.0.0/README.md +36 -0
  3. stario-2.0.0/pyproject.toml +36 -0
  4. stario-2.0.0/src/stario/.DS_Store +0 -0
  5. stario-2.0.0/src/stario/__init__.py +126 -0
  6. stario-2.0.0/src/stario/cli/__init__.py +371 -0
  7. stario-2.0.0/src/stario/cli/__main__.py +5 -0
  8. stario-2.0.0/src/stario/cli/templates/hello-world/main.py +139 -0
  9. stario-2.0.0/src/stario/cli/templates/hello-world/static/js/datastar.js +9 -0
  10. stario-2.0.0/src/stario/cli/templates/tiles/main.py +359 -0
  11. stario-2.0.0/src/stario/cli/templates/tiles/static/css/style.css +320 -0
  12. stario-2.0.0/src/stario/cli/templates/tiles/static/js/datastar.js +9 -0
  13. stario-2.0.0/src/stario/datastar/__init__.py +48 -0
  14. stario-2.0.0/src/stario/datastar/actions.py +327 -0
  15. stario-2.0.0/src/stario/datastar/attributes.py +440 -0
  16. stario-2.0.0/src/stario/datastar/format.py +266 -0
  17. stario-2.0.0/src/stario/datastar/parse.py +398 -0
  18. stario-2.0.0/src/stario/datastar/signals.py +59 -0
  19. stario-2.0.0/src/stario/datastar/sse.py +154 -0
  20. stario-2.0.0/src/stario/exceptions.py +111 -0
  21. stario-2.0.0/src/stario/html/__init__.py +358 -0
  22. stario-2.0.0/src/stario/html/core.py +586 -0
  23. stario-2.0.0/src/stario/html/safestring.py +60 -0
  24. stario-2.0.0/src/stario/html/types.py +300 -0
  25. stario-2.0.0/src/stario/http/__init__.py +11 -0
  26. stario-2.0.0/src/stario/http/app.py +344 -0
  27. stario-2.0.0/src/stario/http/headers.py +595 -0
  28. stario-2.0.0/src/stario/http/protocol.py +320 -0
  29. stario-2.0.0/src/stario/http/request.py +347 -0
  30. stario-2.0.0/src/stario/http/router.py +285 -0
  31. stario-2.0.0/src/stario/http/staticassets.py +383 -0
  32. stario-2.0.0/src/stario/http/types.py +128 -0
  33. stario-2.0.0/src/stario/http/writer.py +754 -0
  34. stario-2.0.0/src/stario/py.typed +0 -0
  35. stario-2.0.0/src/stario/relay.py +130 -0
  36. stario-2.0.0/src/stario/telemetry/__init__.py +6 -0
  37. stario-2.0.0/src/stario/telemetry/core.py +414 -0
  38. stario-2.0.0/src/stario/telemetry/json.py +144 -0
  39. stario-2.0.0/src/stario/telemetry/rich.py +539 -0
  40. stario-2.0.0/src/stario/testing.py +540 -0
  41. stario-2.0.0/src/stario/toys.py +89 -0
stario-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.3
2
+ Name: stario
3
+ Version: 2.0.0
4
+ Summary: Stario - High-performance Python web framework
5
+ Author: Adam Bobowski
6
+ Author-email: Adam Bobowski <adam.bobowski@wratilabs.com>
7
+ Requires-Dist: aiofiles>=24.1.0
8
+ Requires-Dist: brotli>=1.1.0
9
+ Requires-Dist: click>=8.1.0
10
+ Requires-Dist: httptools>=0.7.1
11
+ Requires-Dist: rich>=14.1.0
12
+ Requires-Dist: xxhash>=3.5.0
13
+ Requires-Python: >=3.14
14
+ Description-Content-Type: text/markdown
15
+
16
+ <p align="center">
17
+ <picture>
18
+ <img alt="stario-logo" src="https://raw.githubusercontent.com/bobowski/stario/main/docs/img/stario.png" style="height: 200px; width: auto;">
19
+ </picture>
20
+ </p>
21
+
22
+ <p align="center">
23
+ <em>Real-time hypermedia for Python 3.14+</em>
24
+ </p>
25
+
26
+ ---
27
+
28
+ **Documentation**: [stario.dev](https://stario.dev) · **Source**: [github.com/bobowski/stario](https://github.com/bobowski/stario)
29
+
30
+ ---
31
+
32
+ ## What is Stario?
33
+
34
+ Stario is a Python web framework for **real-time hypermedia**. While most frameworks treat HTTP as request → response, Stario treats connections as ongoing conversations — open an SSE stream, push DOM patches, sync reactive signals.
35
+
36
+ ## Why Stario?
37
+
38
+ - **Real-time first** — SSE streaming, DOM patching, reactive signals built-in
39
+ - **Hypermedia** — Native [Datastar](https://data-star.dev/) integration, no JavaScript frameworks needed
40
+ - **Simple** — Go-style handlers `(Context, Writer) → None`
41
+ - **Fast** — Built on `httptools` with zstd/brotli/gzip compression
42
+
43
+ ## Get Started
44
+
45
+ Install with `uv add stario` or `pip install stario`, then run `stario init` to create a new project. Requires **Python 3.14+**.
46
+
47
+ See the [documentation](https://stario.dev) for tutorials, API reference, and how-to guides.
48
+
49
+ ---
50
+
51
+ <p align="center"><em>Stario: Real-time hypermedia, made simple.</em></p>
stario-2.0.0/README.md ADDED
@@ -0,0 +1,36 @@
1
+ <p align="center">
2
+ <picture>
3
+ <img alt="stario-logo" src="https://raw.githubusercontent.com/bobowski/stario/main/docs/img/stario.png" style="height: 200px; width: auto;">
4
+ </picture>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <em>Real-time hypermedia for Python 3.14+</em>
9
+ </p>
10
+
11
+ ---
12
+
13
+ **Documentation**: [stario.dev](https://stario.dev) · **Source**: [github.com/bobowski/stario](https://github.com/bobowski/stario)
14
+
15
+ ---
16
+
17
+ ## What is Stario?
18
+
19
+ Stario is a Python web framework for **real-time hypermedia**. While most frameworks treat HTTP as request → response, Stario treats connections as ongoing conversations — open an SSE stream, push DOM patches, sync reactive signals.
20
+
21
+ ## Why Stario?
22
+
23
+ - **Real-time first** — SSE streaming, DOM patching, reactive signals built-in
24
+ - **Hypermedia** — Native [Datastar](https://data-star.dev/) integration, no JavaScript frameworks needed
25
+ - **Simple** — Go-style handlers `(Context, Writer) → None`
26
+ - **Fast** — Built on `httptools` with zstd/brotli/gzip compression
27
+
28
+ ## Get Started
29
+
30
+ Install with `uv add stario` or `pip install stario`, then run `stario init` to create a new project. Requires **Python 3.14+**.
31
+
32
+ See the [documentation](https://stario.dev) for tutorials, API reference, and how-to guides.
33
+
34
+ ---
35
+
36
+ <p align="center"><em>Stario: Real-time hypermedia, made simple.</em></p>
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "stario"
3
+ version = "2.0.0"
4
+ description = "Stario - High-performance Python web framework"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Adam Bobowski", email = "adam.bobowski@wratilabs.com" }
8
+ ]
9
+ requires-python = ">=3.14"
10
+ dependencies = [
11
+ "aiofiles>=24.1.0",
12
+ "brotli>=1.1.0",
13
+ "click>=8.1.0",
14
+ "httptools>=0.7.1",
15
+ "rich>=14.1.0",
16
+ "xxhash>=3.5.0",
17
+ ]
18
+
19
+ [project.scripts]
20
+ stario = "stario.cli:main"
21
+
22
+ [build-system]
23
+ requires = ["uv_build>=0.8.6,<0.9.0"]
24
+ build-backend = "uv_build"
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "pyright>=1.1.403",
29
+ "pytest>=8.4.1",
30
+ "pytest-asyncio>=1.3.0",
31
+ "ruff>=0.12.8",
32
+ ]
33
+
34
+ [tool.pytest.ini_options]
35
+ asyncio_mode = "auto"
36
+ asyncio_default_fixture_loop_scope = "function"
Binary file
@@ -0,0 +1,126 @@
1
+ """
2
+ Stario - Real-time Hypermedia for Python.
3
+
4
+ Core:
5
+ from stario import Stario, Router, Request, Writer
6
+
7
+ Types:
8
+ from stario import Handler, Middleware, Context
9
+
10
+ Static files:
11
+ from stario import StaticAssets, asset
12
+
13
+ Pub/Sub:
14
+ from stario import Relay
15
+
16
+ Telemetry:
17
+ from stario import Tracer, Span, Event, Link, RichTracer, JsonTracer
18
+
19
+ Datastar (hypermedia):
20
+ from stario import at, data
21
+
22
+ HTML (separate module):
23
+ from stario.html import Div, Span, render
24
+ """
25
+
26
+ from importlib.metadata import version
27
+
28
+ __version__ = version("stario")
29
+
30
+ # =============================================================================
31
+ # Core - App and routing
32
+ # =============================================================================
33
+ # =============================================================================
34
+ # Datastar - Hypermedia helpers
35
+ # =============================================================================
36
+ from stario.datastar import at as at
37
+ from stario.datastar import data as data
38
+
39
+ # =============================================================================
40
+ # Exceptions
41
+ # =============================================================================
42
+ from stario.exceptions import ClientDisconnected as ClientDisconnected
43
+ from stario.exceptions import HttpException as HttpException
44
+ from stario.exceptions import StarioError as StarioError
45
+ from stario.http.app import Stario as Stario
46
+
47
+ # =============================================================================
48
+ # Request/Response - Handler parameters
49
+ # =============================================================================
50
+ from stario.http.request import Request as Request
51
+ from stario.http.router import Router as Router
52
+
53
+ # =============================================================================
54
+ # Static Files - Fingerprinted asset serving
55
+ # =============================================================================
56
+ from stario.http.staticassets import StaticAssets as StaticAssets
57
+ from stario.http.staticassets import asset as asset
58
+
59
+ # =============================================================================
60
+ # Types - Handler signatures
61
+ # =============================================================================
62
+ from stario.http.types import Context as Context
63
+ from stario.http.types import Handler as Handler
64
+ from stario.http.types import Middleware as Middleware
65
+ from stario.http.writer import CompressionConfig as CompressionConfig
66
+ from stario.http.writer import Writer as Writer
67
+
68
+ # =============================================================================
69
+ # Pub/Sub - In-process messaging
70
+ # =============================================================================
71
+ from stario.relay import Relay as Relay
72
+
73
+ # =============================================================================
74
+ # Telemetry - Tracing and observability
75
+ # =============================================================================
76
+ from stario.telemetry import Event as Event
77
+ from stario.telemetry import JsonTracer as JsonTracer
78
+ from stario.telemetry import Link as Link
79
+ from stario.telemetry import RichTracer as RichTracer
80
+ from stario.telemetry import Span as Span
81
+ from stario.telemetry import Tracer as Tracer
82
+
83
+ # =============================================================================
84
+ # Testing
85
+ # =============================================================================
86
+ from stario.testing import ResponseRecorder as ResponseRecorder
87
+ from stario.testing import TestRequest as TestRequest
88
+
89
+ # =============================================================================
90
+ # __all__ - Public API
91
+ # =============================================================================
92
+ __all__ = [
93
+ # Core
94
+ "Stario",
95
+ "Router",
96
+ # Request/Response
97
+ "Request",
98
+ "Writer",
99
+ "CompressionConfig",
100
+ "Context",
101
+ # Types
102
+ "Handler",
103
+ "Middleware",
104
+ # Static Files
105
+ "StaticAssets",
106
+ "asset",
107
+ # Pub/Sub
108
+ "Relay",
109
+ # Telemetry
110
+ "Tracer",
111
+ "Span",
112
+ "Event",
113
+ "Link",
114
+ "RichTracer",
115
+ "JsonTracer",
116
+ # Datastar
117
+ "at",
118
+ "data",
119
+ # Exceptions
120
+ "HttpException",
121
+ "ClientDisconnected",
122
+ "StarioError",
123
+ # Testing
124
+ "TestRequest",
125
+ "ResponseRecorder",
126
+ ]
@@ -0,0 +1,371 @@
1
+ """
2
+ Stario CLI - Initialize Stario projects.
3
+
4
+ Usage:
5
+ stario init # Interactive project setup
6
+ stario init myproject # Create project with name
7
+ stario init -t hello-world # Specify template
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import shutil
15
+ import subprocess
16
+ import urllib.error
17
+ import urllib.request
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+
21
+ import click
22
+
23
+ # Path to bundled templates (relative to this file)
24
+ TEMPLATES_DIR = Path(__file__).parent / "templates"
25
+
26
+ # GitHub URLs for fetching remote examples
27
+ GITHUB_API = "https://api.github.com/repos/Bobowski/stario/contents"
28
+ MANIFEST_URL = (
29
+ "https://raw.githubusercontent.com/Bobowski/stario/main/examples/manifest.json"
30
+ )
31
+
32
+
33
+ @dataclass
34
+ class Template:
35
+ """Represents a project template."""
36
+
37
+ name: str
38
+ description: str
39
+ long_description: str = ""
40
+ bundled: bool = True
41
+ recommended: bool = False
42
+
43
+
44
+ # Bundled templates (always available)
45
+ BUNDLED_TEMPLATES = [
46
+ Template(
47
+ name="tiles",
48
+ description="Collaborative painting board",
49
+ long_description=(
50
+ "The best way to start! A multiplayer canvas where users paint colored\n"
51
+ " tiles together in real-time. Experience Datastar's reactive signals,\n"
52
+ " SSE streaming, and see how Stario makes multiplayer trivial."
53
+ ),
54
+ bundled=True,
55
+ recommended=True,
56
+ ),
57
+ Template(
58
+ name="hello-world",
59
+ description="Minimal counter app",
60
+ long_description=(
61
+ "A clean starting point with just the essentials. Simple counter\n"
62
+ " demonstrating Datastar signals and server interaction."
63
+ ),
64
+ bundled=True,
65
+ ),
66
+ ]
67
+
68
+
69
+ def _fetch_remote_templates() -> list[Template]:
70
+ """Fetch remote examples manifest. Returns empty list on any failure."""
71
+ try:
72
+ req = urllib.request.Request(
73
+ MANIFEST_URL,
74
+ headers={"User-Agent": "stario-cli"},
75
+ )
76
+ with urllib.request.urlopen(req, timeout=3) as response:
77
+ data = json.loads(response.read())
78
+ return [
79
+ Template(
80
+ name=ex["name"],
81
+ description=ex["description"],
82
+ long_description=ex.get("long_description", ""),
83
+ bundled=False,
84
+ )
85
+ for ex in data.get("examples", [])
86
+ ]
87
+ except Exception:
88
+ return [] # Silent failure - just show bundled templates
89
+
90
+
91
+ def _get_all_templates() -> tuple[list[Template], bool]:
92
+ """Get all available templates. Returns (templates, has_remote)."""
93
+ templates = list(BUNDLED_TEMPLATES)
94
+ remote = _fetch_remote_templates()
95
+ templates.extend(remote)
96
+ return templates, len(remote) > 0
97
+
98
+
99
+ def _fetch_remote_example(example_name: str, dest: Path) -> None:
100
+ """Fetch example directory from GitHub."""
101
+ api_url = f"{GITHUB_API}/examples/{example_name}"
102
+
103
+ try:
104
+ req = urllib.request.Request(api_url, headers={"User-Agent": "stario-cli"})
105
+ with urllib.request.urlopen(req, timeout=10) as response:
106
+ contents = json.loads(response.read())
107
+
108
+ for item in _walk_github_contents(contents):
109
+ if item["type"] == "file":
110
+ # Strip the example prefix from path
111
+ rel_path = item["path"].split(f"examples/{example_name}/", 1)[1]
112
+ file_path = dest / rel_path
113
+ file_path.parent.mkdir(parents=True, exist_ok=True)
114
+
115
+ with urllib.request.urlopen(item["download_url"]) as f:
116
+ file_path.write_bytes(f.read())
117
+
118
+ except urllib.error.URLError as e:
119
+ raise click.ClickException(
120
+ f"Failed to fetch example '{example_name}': {e}\n"
121
+ "Check your internet connection or try a bundled template."
122
+ )
123
+
124
+
125
+ def _walk_github_contents(items: list) -> list:
126
+ """Recursively walk directory contents from GitHub API."""
127
+ result = []
128
+ for item in items:
129
+ if item["type"] == "file":
130
+ result.append(item)
131
+ elif item["type"] == "dir":
132
+ req = urllib.request.Request(
133
+ item["url"], headers={"User-Agent": "stario-cli"}
134
+ )
135
+ with urllib.request.urlopen(req, timeout=10) as response:
136
+ subdir = json.loads(response.read())
137
+ result.extend(_walk_github_contents(subdir))
138
+ return result
139
+
140
+
141
+ @click.group()
142
+ @click.version_option(package_name="stario")
143
+ def main() -> None:
144
+ """Stario - High-performance Python web framework."""
145
+ pass
146
+
147
+
148
+ @main.command()
149
+ @click.argument("name", required=False)
150
+ @click.option(
151
+ "--template",
152
+ "-t",
153
+ "template_name",
154
+ help="Template to use (skip interactive selection)",
155
+ )
156
+ def init(name: str | None, template_name: str | None) -> None:
157
+ """
158
+ Create a new Stario project.
159
+
160
+ NAME: Project directory name (prompted if not provided)
161
+ """
162
+ click.echo()
163
+ click.echo(click.style("⭐ Stario", fg="yellow", bold=True))
164
+ click.echo()
165
+
166
+ # Get available templates
167
+ templates, has_remote = _get_all_templates()
168
+ template_map = {t.name: t for t in templates}
169
+ # Also map by number
170
+ num_map = {str(i + 1): t for i, t in enumerate(templates)}
171
+
172
+ # Template selection first
173
+ if template_name is None:
174
+ default_idx = next((i for i, t in enumerate(templates) if t.recommended), 0)
175
+
176
+ click.echo(click.style("? Choose a template:", fg="cyan", bold=True))
177
+ click.echo()
178
+
179
+ for i, t in enumerate(templates):
180
+ num = click.style(f" [{i + 1}]", fg="cyan")
181
+ name_style = click.style(t.name, bold=True)
182
+
183
+ # Build suffix
184
+ suffix = ""
185
+ if t.recommended:
186
+ suffix = click.style(" ★ great starting point", fg="yellow")
187
+ elif not t.bundled:
188
+ suffix = click.style(" ↓", fg="blue")
189
+
190
+ click.echo(f"{num} {name_style} - {t.description}{suffix}")
191
+
192
+ # Show long description if available
193
+ if t.long_description:
194
+ click.echo(click.style(f" {t.long_description}", dim=True))
195
+ click.echo()
196
+
197
+ # Show legend only if we have remote templates
198
+ if has_remote:
199
+ click.echo(
200
+ click.style(" ↓", fg="blue")
201
+ + click.style(" = downloaded from GitHub", dim=True)
202
+ )
203
+ click.echo()
204
+
205
+ # Prompt with number as default
206
+ choice = click.prompt(
207
+ click.style("? Enter number or name", fg="cyan", bold=True),
208
+ default=str(default_idx + 1),
209
+ )
210
+
211
+ # Resolve choice (number or name)
212
+ if choice in num_map:
213
+ template_name = num_map[choice].name
214
+ elif choice in template_map:
215
+ template_name = choice
216
+ else:
217
+ raise click.ClickException(
218
+ f"Unknown template '{choice}'. Use a number (1-{len(templates)}) or template name."
219
+ )
220
+
221
+ # Validate template
222
+ if template_name not in template_map:
223
+ raise click.ClickException(
224
+ f"Unknown template '{template_name}'. "
225
+ f"Available: {', '.join(template_map.keys())}"
226
+ )
227
+
228
+ template = template_map[template_name]
229
+
230
+ # Project name (after template selection)
231
+ if name is None:
232
+ prompt_name = click.prompt(
233
+ click.style("? Project name", fg="cyan", bold=True),
234
+ default="stario-app",
235
+ )
236
+ name = str(prompt_name)
237
+
238
+ project_dir = Path.cwd() / name
239
+
240
+ if project_dir.exists():
241
+ raise click.ClickException(f"Directory '{name}' already exists")
242
+
243
+ click.echo()
244
+ click.echo(
245
+ click.style(" Creating ", dim=True)
246
+ + click.style(name, fg="green", bold=True)
247
+ + click.style(f" with {template_name} template...", dim=True)
248
+ )
249
+ click.echo()
250
+
251
+ # 1. Initialize with uv
252
+ click.echo(click.style(" ◐ ", fg="yellow") + "Setting up project with uv...")
253
+ result = subprocess.run(
254
+ ["uv", "init", "--app", name],
255
+ capture_output=True,
256
+ text=True,
257
+ )
258
+ if result.returncode != 0:
259
+ raise click.ClickException(f"uv init failed: {result.stderr}")
260
+
261
+ # 2. Add stario dependency
262
+ click.echo(click.style(" ◐ ", fg="yellow") + "Adding stario dependency...")
263
+ subprocess.run(
264
+ ["uv", "add", "stario"],
265
+ cwd=project_dir,
266
+ capture_output=True,
267
+ )
268
+
269
+ # 3. Remove default files created by uv
270
+ for file in ["hello.py", "README.md"]:
271
+ default_file = project_dir / file
272
+ if default_file.exists():
273
+ default_file.unlink()
274
+
275
+ # 4. Copy/fetch template files
276
+ if template.bundled:
277
+ click.echo(click.style(" ◐ ", fg="yellow") + "Copying template files...")
278
+ _copy_bundled_template(project_dir, template_name)
279
+ else:
280
+ click.echo(
281
+ click.style(" ◐ ", fg="yellow") + "Downloading template from GitHub..."
282
+ )
283
+ _fetch_remote_example(template_name, project_dir)
284
+
285
+ # Success!
286
+ click.echo()
287
+ click.echo(
288
+ click.style(" ✓ ", fg="green")
289
+ + click.style("Project created successfully!", fg="green", bold=True)
290
+ )
291
+ click.echo()
292
+
293
+ # Ask if they want to start the server
294
+ click.echo(click.style(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", dim=True))
295
+ click.echo()
296
+
297
+ if click.confirm(
298
+ click.style(" ? Start the server now?", fg="cyan", bold=True),
299
+ default=True,
300
+ ):
301
+ click.echo()
302
+ click.echo(
303
+ click.style(" 🚀 Starting server at ", fg="white")
304
+ + click.style("http://localhost:8000", fg="cyan", underline=True)
305
+ )
306
+ click.echo(click.style(" Press Ctrl+C to stop", dim=True))
307
+ click.echo()
308
+
309
+ # Run the server in the project directory
310
+ os.chdir(project_dir)
311
+ subprocess.run(["uv", "run", "main.py"])
312
+
313
+ # After server stops, show how to get back
314
+ click.echo()
315
+ click.echo(click.style(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", dim=True))
316
+ click.echo()
317
+ click.echo(
318
+ click.style(" To continue working on your project:", fg="white", bold=True)
319
+ )
320
+ click.echo()
321
+ click.echo(click.style(f" cd {name}", fg="cyan"))
322
+ click.echo(click.style(" uv run main.py", fg="cyan"))
323
+ click.echo()
324
+ click.echo(click.style(" # Or with auto-reload", dim=True))
325
+ click.echo(click.style(' uvx watchfiles "uv run main.py" .', fg="cyan"))
326
+ click.echo()
327
+ else:
328
+ # Show manual instructions
329
+ click.echo()
330
+ click.echo(
331
+ click.style(" 🚀 Ready to go! Run these commands:", fg="white", bold=True)
332
+ )
333
+ click.echo()
334
+ click.echo(click.style(f" cd {name}", fg="cyan"))
335
+ click.echo(click.style(" uv run main.py", fg="cyan"))
336
+ click.echo()
337
+ click.echo(click.style(" # Or with auto-reload", dim=True))
338
+ click.echo(click.style(' uvx watchfiles "uv run main.py" .', fg="cyan"))
339
+ click.echo()
340
+ click.echo(click.style(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", dim=True))
341
+ click.echo()
342
+ click.echo(
343
+ click.style(" Open ", dim=True)
344
+ + click.style("http://localhost:8000", fg="cyan", underline=True)
345
+ + click.style(" and have fun! ⭐", dim=True)
346
+ )
347
+ click.echo()
348
+
349
+
350
+ def _copy_bundled_template(project_dir: Path, template_name: str) -> None:
351
+ """Copy bundled template files to project directory."""
352
+ template_dir = TEMPLATES_DIR / template_name
353
+
354
+ if not template_dir.exists():
355
+ raise click.ClickException(
356
+ f"Template directory not found: {template_dir}\n"
357
+ "This is a bug in stario - please report it."
358
+ )
359
+
360
+ for item in template_dir.iterdir():
361
+ src = item
362
+ dst = project_dir / item.name
363
+
364
+ if src.is_dir():
365
+ shutil.copytree(src, dst)
366
+ else:
367
+ shutil.copy2(src, dst)
368
+
369
+
370
+ if __name__ == "__main__":
371
+ main()
@@ -0,0 +1,5 @@
1
+ """Allow running with: python -m stario.cli"""
2
+
3
+ from . import main
4
+
5
+ main()