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.
- stario-2.0.0/PKG-INFO +51 -0
- stario-2.0.0/README.md +36 -0
- stario-2.0.0/pyproject.toml +36 -0
- stario-2.0.0/src/stario/.DS_Store +0 -0
- stario-2.0.0/src/stario/__init__.py +126 -0
- stario-2.0.0/src/stario/cli/__init__.py +371 -0
- stario-2.0.0/src/stario/cli/__main__.py +5 -0
- stario-2.0.0/src/stario/cli/templates/hello-world/main.py +139 -0
- stario-2.0.0/src/stario/cli/templates/hello-world/static/js/datastar.js +9 -0
- stario-2.0.0/src/stario/cli/templates/tiles/main.py +359 -0
- stario-2.0.0/src/stario/cli/templates/tiles/static/css/style.css +320 -0
- stario-2.0.0/src/stario/cli/templates/tiles/static/js/datastar.js +9 -0
- stario-2.0.0/src/stario/datastar/__init__.py +48 -0
- stario-2.0.0/src/stario/datastar/actions.py +327 -0
- stario-2.0.0/src/stario/datastar/attributes.py +440 -0
- stario-2.0.0/src/stario/datastar/format.py +266 -0
- stario-2.0.0/src/stario/datastar/parse.py +398 -0
- stario-2.0.0/src/stario/datastar/signals.py +59 -0
- stario-2.0.0/src/stario/datastar/sse.py +154 -0
- stario-2.0.0/src/stario/exceptions.py +111 -0
- stario-2.0.0/src/stario/html/__init__.py +358 -0
- stario-2.0.0/src/stario/html/core.py +586 -0
- stario-2.0.0/src/stario/html/safestring.py +60 -0
- stario-2.0.0/src/stario/html/types.py +300 -0
- stario-2.0.0/src/stario/http/__init__.py +11 -0
- stario-2.0.0/src/stario/http/app.py +344 -0
- stario-2.0.0/src/stario/http/headers.py +595 -0
- stario-2.0.0/src/stario/http/protocol.py +320 -0
- stario-2.0.0/src/stario/http/request.py +347 -0
- stario-2.0.0/src/stario/http/router.py +285 -0
- stario-2.0.0/src/stario/http/staticassets.py +383 -0
- stario-2.0.0/src/stario/http/types.py +128 -0
- stario-2.0.0/src/stario/http/writer.py +754 -0
- stario-2.0.0/src/stario/py.typed +0 -0
- stario-2.0.0/src/stario/relay.py +130 -0
- stario-2.0.0/src/stario/telemetry/__init__.py +6 -0
- stario-2.0.0/src/stario/telemetry/core.py +414 -0
- stario-2.0.0/src/stario/telemetry/json.py +144 -0
- stario-2.0.0/src/stario/telemetry/rich.py +539 -0
- stario-2.0.0/src/stario/testing.py +540 -0
- 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()
|