spawnio 1.0.5__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.
- spawn/__init__.py +6 -0
- spawn/cli/__init__.py +0 -0
- spawn/cli/app.py +218 -0
- spawn/cli/noninteractive.py +204 -0
- spawn/cli/prompts.py +267 -0
- spawn/core/__init__.py +0 -0
- spawn/core/exceptions.py +7 -0
- spawn/core/models.py +21 -0
- spawn/core/registry.py +152 -0
- spawn/generators/__init__.py +0 -0
- spawn/generators/custom_structure.py +508 -0
- spawn/generators/project_generator.py +115 -0
- spawn/github/__init__.py +0 -0
- spawn/github/exceptions.py +5 -0
- spawn/github/publisher.py +69 -0
- spawn/github/validators.py +23 -0
- spawn/templates/__init__.py +0 -0
- spawn/templates/agent/__init__.py +182 -0
- spawn/templates/agent/content.py +374 -0
- spawn/templates/automation/__init__.py +100 -0
- spawn/templates/automation/content.py +181 -0
- spawn/templates/backend_api/__init__.py +243 -0
- spawn/templates/backend_api/content.py +509 -0
- spawn/templates/base.py +58 -0
- spawn/templates/chatbot/__init__.py +243 -0
- spawn/templates/chatbot/content.py +668 -0
- spawn/templates/cli_application/__init__.py +211 -0
- spawn/templates/cli_application/content.py +512 -0
- spawn/templates/data_project/__init__.py +125 -0
- spawn/templates/data_project/analysis/__init__.py +74 -0
- spawn/templates/data_project/analysis/content.py +190 -0
- spawn/templates/data_project/dashboard/__init__.py +61 -0
- spawn/templates/data_project/dashboard/content.py +135 -0
- spawn/templates/data_project/etl/__init__.py +66 -0
- spawn/templates/data_project/etl/content.py +239 -0
- spawn/templates/data_project/ml/__init__.py +62 -0
- spawn/templates/data_project/ml/content.py +239 -0
- spawn/templates/mcp_server/__init__.py +83 -0
- spawn/templates/mcp_server/content.py +161 -0
- spawn/templates/rag/__init__.py +124 -0
- spawn/templates/rag/content.py +463 -0
- spawn/templates/shared_content.py +89 -0
- spawn/utils/__init__.py +0 -0
- spawn/utils/banner.py +41 -0
- spawn/utils/console.py +3 -0
- spawn/utils/doctor.py +594 -0
- spawn/utils/git.py +132 -0
- spawn/utils/success.py +47 -0
- spawn/utils/theme.py +25 -0
- spawn/utils/uv.py +61 -0
- spawn/utils/validators.py +15 -0
- spawnio-1.0.5.dist-info/METADATA +798 -0
- spawnio-1.0.5.dist-info/RECORD +56 -0
- spawnio-1.0.5.dist-info/WHEEL +4 -0
- spawnio-1.0.5.dist-info/entry_points.txt +2 -0
- spawnio-1.0.5.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from spawn.templates.base import BaseTemplate
|
|
4
|
+
from spawn.templates.data_project.analysis import DataAnalysisTemplate
|
|
5
|
+
from spawn.templates.data_project.dashboard import DashboardTemplate
|
|
6
|
+
from spawn.templates.data_project.etl import ETLPipelineTemplate
|
|
7
|
+
from spawn.templates.data_project.ml import MLProjectTemplate
|
|
8
|
+
|
|
9
|
+
_SUBTEMPLATE_MAP = {
|
|
10
|
+
"Data Analysis": DataAnalysisTemplate,
|
|
11
|
+
"Dashboard": DashboardTemplate,
|
|
12
|
+
"ETL Pipeline": ETLPipelineTemplate,
|
|
13
|
+
"Machine Learning": MLProjectTemplate,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
_GITIGNORE_RULES: dict[str, str] = {
|
|
17
|
+
"Data Analysis": (
|
|
18
|
+
"\n# Data Analysis artifacts\n"
|
|
19
|
+
"data/*.csv\n"
|
|
20
|
+
"!data/sample.csv\n"
|
|
21
|
+
"reports/*.png\n"
|
|
22
|
+
"notebooks/.ipynb_checkpoints/\n"
|
|
23
|
+
),
|
|
24
|
+
"Dashboard": (
|
|
25
|
+
"\n# Dashboard artifacts\n"
|
|
26
|
+
"data/*.csv\n"
|
|
27
|
+
"!data/sample.csv\n"
|
|
28
|
+
),
|
|
29
|
+
"ETL Pipeline": (
|
|
30
|
+
"\n# ETL artifacts\n"
|
|
31
|
+
"data/cleaned.csv\n"
|
|
32
|
+
),
|
|
33
|
+
"Machine Learning": (
|
|
34
|
+
"\n# ML artifacts\n"
|
|
35
|
+
"models/*.joblib\n"
|
|
36
|
+
"experiments/*.json\n"
|
|
37
|
+
"data/*.csv\n"
|
|
38
|
+
"!data/dataset.csv\n"
|
|
39
|
+
),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _DataProjectPostInstallMixin:
|
|
44
|
+
"""
|
|
45
|
+
Shared post_install logic for all Data Project subtemplates.
|
|
46
|
+
|
|
47
|
+
Subtemplates must set:
|
|
48
|
+
- self.extras (list[str])
|
|
49
|
+
- self.data_type (str)
|
|
50
|
+
and import their own CI constants from their content module.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
# Subtemplates override these with their own imported constants.
|
|
54
|
+
_CI_BASE: str = ""
|
|
55
|
+
_CI_RUFF_STEP: str = ""
|
|
56
|
+
_CI_PYTEST_STEP: str = ""
|
|
57
|
+
|
|
58
|
+
def post_install(self, project_path: Path) -> None:
|
|
59
|
+
# ── pyproject.toml extras ─────────────────────────────────────────
|
|
60
|
+
needs_pyproject = any(e in self.extras for e in ("pytest", "ruff"))
|
|
61
|
+
if needs_pyproject:
|
|
62
|
+
pyproject = project_path / "pyproject.toml"
|
|
63
|
+
current = pyproject.read_text(encoding="utf-8")
|
|
64
|
+
additions = ""
|
|
65
|
+
if "pytest" in self.extras:
|
|
66
|
+
additions += '\n[tool.pytest.ini_options]\ntestpaths = ["tests"]\n'
|
|
67
|
+
if "ruff" in self.extras:
|
|
68
|
+
additions += "\n[tool.ruff]\nline-length = 88\n"
|
|
69
|
+
if additions:
|
|
70
|
+
pyproject.write_text(current + additions, encoding="utf-8")
|
|
71
|
+
|
|
72
|
+
# ── GitHub Actions ────────────────────────────────────────────────
|
|
73
|
+
if "github-actions" in self.extras and self._CI_BASE:
|
|
74
|
+
workflows_path = project_path / ".github" / "workflows"
|
|
75
|
+
workflows_path.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
ci = self._CI_BASE
|
|
77
|
+
if "ruff" in self.extras:
|
|
78
|
+
ci += self._CI_RUFF_STEP
|
|
79
|
+
if "pytest" in self.extras:
|
|
80
|
+
ci += self._CI_PYTEST_STEP
|
|
81
|
+
(workflows_path / "ci.yml").write_text(ci, encoding="utf-8")
|
|
82
|
+
|
|
83
|
+
# ── Data-artifact gitignore rules ─────────────────────────────────
|
|
84
|
+
gitignore_path = project_path / ".gitignore"
|
|
85
|
+
current_gitignore = gitignore_path.read_text(encoding="utf-8")
|
|
86
|
+
rule = _GITIGNORE_RULES.get(self.data_type, "")
|
|
87
|
+
if rule and rule.strip() not in current_gitignore:
|
|
88
|
+
gitignore_path.write_text(current_gitignore + rule, encoding="utf-8")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class DataProjectTemplate(BaseTemplate):
|
|
92
|
+
"""
|
|
93
|
+
Dispatcher that returns the appropriate per-data_type subtemplate.
|
|
94
|
+
Uses __new__ so the returned object IS the subtemplate.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __new__(
|
|
98
|
+
cls,
|
|
99
|
+
data_type: str = "Data Analysis",
|
|
100
|
+
extras: list[str] | None = None,
|
|
101
|
+
) -> BaseTemplate: # type: ignore[misc]
|
|
102
|
+
klass = _SUBTEMPLATE_MAP.get(data_type)
|
|
103
|
+
if klass is not None:
|
|
104
|
+
return klass(extras=extras)
|
|
105
|
+
instance = super().__new__(cls)
|
|
106
|
+
return instance
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
data_type: str = "Data Analysis",
|
|
111
|
+
extras: list[str] | None = None,
|
|
112
|
+
) -> None:
|
|
113
|
+
if isinstance(self, tuple(_SUBTEMPLATE_MAP.values())):
|
|
114
|
+
return
|
|
115
|
+
self.data_type = data_type
|
|
116
|
+
self.extras = extras or []
|
|
117
|
+
super().__init__(
|
|
118
|
+
name="Data Project",
|
|
119
|
+
folders=[],
|
|
120
|
+
starter_files=[],
|
|
121
|
+
next_steps=["cd {project_name}"],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def get_dependencies(self) -> list[str]:
|
|
125
|
+
return []
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from spawn.templates.base import BaseTemplate
|
|
4
|
+
from spawn.templates.data_project.analysis.content import (
|
|
5
|
+
SAMPLE_CSV,
|
|
6
|
+
NOTEBOOK_CONTENT,
|
|
7
|
+
TEST_CONTENT,
|
|
8
|
+
make_readme,
|
|
9
|
+
GITHUB_ACTIONS_CI_BASE,
|
|
10
|
+
GITHUB_ACTIONS_CI_RUFF_STEP,
|
|
11
|
+
GITHUB_ACTIONS_CI_PYTEST_STEP,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
FOLDERS = ["data", "notebooks", "reports", "src", "tests"]
|
|
15
|
+
|
|
16
|
+
FILES = [
|
|
17
|
+
("data/sample.csv", SAMPLE_CSV),
|
|
18
|
+
("notebooks/analysis.ipynb", NOTEBOOK_CONTENT),
|
|
19
|
+
("tests/test_analysis.py", TEST_CONTENT),
|
|
20
|
+
("tests/__init__.py", ""),
|
|
21
|
+
("reports/.gitkeep", ""),
|
|
22
|
+
("src/__init__.py", ""),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
DEPENDENCIES = ["pandas", "numpy", "jupyter", "matplotlib"]
|
|
26
|
+
|
|
27
|
+
NEXT_STEPS = [
|
|
28
|
+
"cd {project_name}",
|
|
29
|
+
"uv run jupyter notebook",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DataAnalysisTemplate(BaseTemplate):
|
|
34
|
+
data_type = "Data Analysis"
|
|
35
|
+
_CI_BASE = GITHUB_ACTIONS_CI_BASE
|
|
36
|
+
_CI_RUFF_STEP = GITHUB_ACTIONS_CI_RUFF_STEP
|
|
37
|
+
_CI_PYTEST_STEP = GITHUB_ACTIONS_CI_PYTEST_STEP
|
|
38
|
+
|
|
39
|
+
def __init__(self, extras: list[str] | None = None) -> None:
|
|
40
|
+
self.extras = extras or []
|
|
41
|
+
super().__init__(
|
|
42
|
+
name="Data Project",
|
|
43
|
+
folders=list(FOLDERS),
|
|
44
|
+
starter_files=list(FILES),
|
|
45
|
+
next_steps=list(NEXT_STEPS),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def get_readme_content(self, context: dict) -> str | None:
|
|
49
|
+
return make_readme().format_map(context)
|
|
50
|
+
|
|
51
|
+
def get_dependencies(self) -> list[str]:
|
|
52
|
+
base = list(DEPENDENCIES)
|
|
53
|
+
if "pytest" in self.extras:
|
|
54
|
+
base.append("pytest")
|
|
55
|
+
if "ruff" in self.extras:
|
|
56
|
+
base.append("ruff")
|
|
57
|
+
return base
|
|
58
|
+
|
|
59
|
+
def generate(self, project_path: Path, context: dict) -> None:
|
|
60
|
+
"""Write files, skipping format_map for .ipynb (raw JSON)."""
|
|
61
|
+
for folder in self.folders:
|
|
62
|
+
(project_path / folder).mkdir(parents=True, exist_ok=True)
|
|
63
|
+
|
|
64
|
+
for relative_path, content in self.starter_files:
|
|
65
|
+
file_path = project_path / relative_path
|
|
66
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
if relative_path.endswith(".ipynb"):
|
|
68
|
+
file_path.write_text(content, encoding="utf-8")
|
|
69
|
+
else:
|
|
70
|
+
file_path.write_text(content.format_map(context), encoding="utf-8")
|
|
71
|
+
|
|
72
|
+
def post_install(self, project_path: Path) -> None:
|
|
73
|
+
from spawn.templates.data_project import _DataProjectPostInstallMixin
|
|
74
|
+
_DataProjectPostInstallMixin.post_install(self, project_path) # type: ignore[arg-type]
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
SAMPLE_CSV = """\
|
|
4
|
+
date,category,value
|
|
5
|
+
2024-01-05,Electronics,1200
|
|
6
|
+
2024-01-12,Clothing,430
|
|
7
|
+
2024-01-18,Electronics,980
|
|
8
|
+
2024-01-25,Food,210
|
|
9
|
+
2024-02-03,Clothing,560
|
|
10
|
+
2024-02-10,Electronics,1450
|
|
11
|
+
2024-02-14,Food,330
|
|
12
|
+
2024-02-22,Clothing,390
|
|
13
|
+
2024-03-01,Electronics,1100
|
|
14
|
+
2024-03-08,Food,270
|
|
15
|
+
2024-03-15,Clothing,620
|
|
16
|
+
2024-03-21,Electronics,870
|
|
17
|
+
2024-03-28,Food,190
|
|
18
|
+
2024-04-04,Clothing,710
|
|
19
|
+
2024-04-11,Electronics,1340
|
|
20
|
+
2024-04-18,Food,250
|
|
21
|
+
2024-04-25,Clothing,480
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
NOTEBOOK_CONTENT = json.dumps(
|
|
25
|
+
{
|
|
26
|
+
"nbformat": 4,
|
|
27
|
+
"nbformat_minor": 5,
|
|
28
|
+
"metadata": {
|
|
29
|
+
"kernelspec": {
|
|
30
|
+
"display_name": "Python 3",
|
|
31
|
+
"language": "python",
|
|
32
|
+
"name": "python3",
|
|
33
|
+
},
|
|
34
|
+
"language_info": {"name": "python", "version": "3.12.0"},
|
|
35
|
+
},
|
|
36
|
+
"cells": [
|
|
37
|
+
{
|
|
38
|
+
"cell_type": "code",
|
|
39
|
+
"execution_count": None,
|
|
40
|
+
"metadata": {},
|
|
41
|
+
"outputs": [],
|
|
42
|
+
"source": [
|
|
43
|
+
"import pandas as pd\n",
|
|
44
|
+
"import matplotlib.pyplot as plt\n",
|
|
45
|
+
"\n",
|
|
46
|
+
'df = pd.read_csv("data/sample.csv", parse_dates=["date"])\n',
|
|
47
|
+
"df.head()",
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"cell_type": "code",
|
|
52
|
+
"execution_count": None,
|
|
53
|
+
"metadata": {},
|
|
54
|
+
"outputs": [],
|
|
55
|
+
"source": [
|
|
56
|
+
"print(df.describe())\n",
|
|
57
|
+
"print()\n",
|
|
58
|
+
'summary = df.groupby("category")["value"].sum().reset_index()\n',
|
|
59
|
+
"print(summary)",
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"cell_type": "code",
|
|
64
|
+
"execution_count": None,
|
|
65
|
+
"metadata": {},
|
|
66
|
+
"outputs": [],
|
|
67
|
+
"source": [
|
|
68
|
+
"import os\n",
|
|
69
|
+
'os.makedirs("reports", exist_ok=True)\n',
|
|
70
|
+
"\n",
|
|
71
|
+
"fig, ax = plt.subplots(figsize=(8, 5))\n",
|
|
72
|
+
'ax.bar(summary["category"], summary["value"], color="steelblue")\n',
|
|
73
|
+
'ax.set_title("Total Value by Category")\n',
|
|
74
|
+
'ax.set_xlabel("Category")\n',
|
|
75
|
+
'ax.set_ylabel("Total Value")\n',
|
|
76
|
+
"plt.tight_layout()\n",
|
|
77
|
+
'plt.savefig("reports/summary.png", dpi=150)\n',
|
|
78
|
+
"plt.show()\n",
|
|
79
|
+
'print("Chart saved to reports/summary.png")',
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"cell_type": "markdown",
|
|
84
|
+
"metadata": {},
|
|
85
|
+
"source": [
|
|
86
|
+
"## Insights\n",
|
|
87
|
+
"\n",
|
|
88
|
+
"_Fill in your observations here after reviewing the chart above._",
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
indent=2,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
TEST_CONTENT = """\
|
|
97
|
+
import os
|
|
98
|
+
from pathlib import Path
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_sample_csv_exists_and_nonempty():
|
|
102
|
+
import pandas as pd
|
|
103
|
+
|
|
104
|
+
df = pd.read_csv("data/sample.csv")
|
|
105
|
+
assert len(df) > 0, "data/sample.csv is empty"
|
|
106
|
+
assert "category" in df.columns
|
|
107
|
+
assert "value" in df.columns
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_reports_directory_exists():
|
|
111
|
+
reports_path = Path("reports")
|
|
112
|
+
assert reports_path.exists(), (
|
|
113
|
+
"reports/ directory missing — run the notebook first to generate it"
|
|
114
|
+
)
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def make_readme() -> str:
|
|
119
|
+
return (
|
|
120
|
+
"# {project_name}\n\n"
|
|
121
|
+
"A Data Analysis project generated with Spawn using pandas, matplotlib, and Jupyter.\n\n"
|
|
122
|
+
"## Getting Started\n\n"
|
|
123
|
+
"1. Install dependencies:\n\n"
|
|
124
|
+
"```bash\n"
|
|
125
|
+
"uv sync\n"
|
|
126
|
+
"```\n\n"
|
|
127
|
+
"2. Launch the notebook:\n\n"
|
|
128
|
+
"```bash\n"
|
|
129
|
+
"uv run jupyter notebook\n"
|
|
130
|
+
"```\n\n"
|
|
131
|
+
"3. Open `notebooks/analysis.ipynb` and run all cells.\n"
|
|
132
|
+
" A bar chart is saved to `reports/summary.png` automatically.\n\n"
|
|
133
|
+
"## Example\n\n"
|
|
134
|
+
"```\n"
|
|
135
|
+
"$ uv run jupyter notebook\n"
|
|
136
|
+
"# Open notebooks/analysis.ipynb\n"
|
|
137
|
+
"# Run all cells → reports/summary.png is generated\n"
|
|
138
|
+
"```\n\n"
|
|
139
|
+
"## Project Structure\n\n"
|
|
140
|
+
"```\n"
|
|
141
|
+
"{project_name}/\n"
|
|
142
|
+
"├── data/\n"
|
|
143
|
+
"│ └── sample.csv # Sample sales data\n"
|
|
144
|
+
"├── notebooks/\n"
|
|
145
|
+
"│ └── analysis.ipynb # Main analysis notebook\n"
|
|
146
|
+
"├── reports/ # Generated charts and outputs\n"
|
|
147
|
+
"├── src/\n"
|
|
148
|
+
"├── tests/\n"
|
|
149
|
+
"│ └── test_analysis.py\n"
|
|
150
|
+
"└── README.md\n"
|
|
151
|
+
"```\n\n"
|
|
152
|
+
"## Running Tests\n\n"
|
|
153
|
+
"```bash\n"
|
|
154
|
+
"uv run pytest\n"
|
|
155
|
+
"```\n"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# ─── GitHub Actions ───────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
GITHUB_ACTIONS_CI_BASE = """\
|
|
161
|
+
name: CI
|
|
162
|
+
|
|
163
|
+
on:
|
|
164
|
+
push:
|
|
165
|
+
branches: [main]
|
|
166
|
+
pull_request:
|
|
167
|
+
branches: [main]
|
|
168
|
+
|
|
169
|
+
jobs:
|
|
170
|
+
ci:
|
|
171
|
+
runs-on: ubuntu-latest
|
|
172
|
+
|
|
173
|
+
steps:
|
|
174
|
+
- uses: actions/checkout@v4
|
|
175
|
+
|
|
176
|
+
- uses: astral-sh/setup-uv@v5
|
|
177
|
+
|
|
178
|
+
- name: Install dependencies
|
|
179
|
+
run: uv sync
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
GITHUB_ACTIONS_CI_RUFF_STEP = """\
|
|
183
|
+
- name: Lint
|
|
184
|
+
run: uv run ruff check .
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
GITHUB_ACTIONS_CI_PYTEST_STEP = """\
|
|
188
|
+
- name: Test
|
|
189
|
+
run: uv run pytest
|
|
190
|
+
"""
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from spawn.templates.base import BaseTemplate
|
|
4
|
+
from spawn.templates.data_project.dashboard.content import (
|
|
5
|
+
DASHBOARD_SAMPLE_CSV,
|
|
6
|
+
DASHBOARD_APP_CONTENT,
|
|
7
|
+
DASHBOARD_TEST_CONTENT,
|
|
8
|
+
make_readme,
|
|
9
|
+
GITHUB_ACTIONS_CI_BASE,
|
|
10
|
+
GITHUB_ACTIONS_CI_RUFF_STEP,
|
|
11
|
+
GITHUB_ACTIONS_CI_PYTEST_STEP,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
FOLDERS = ["data", "dashboard", "src", "tests"]
|
|
15
|
+
|
|
16
|
+
FILES = [
|
|
17
|
+
("data/sample.csv", DASHBOARD_SAMPLE_CSV),
|
|
18
|
+
("dashboard/__init__.py", ""),
|
|
19
|
+
("dashboard/app.py", DASHBOARD_APP_CONTENT),
|
|
20
|
+
("tests/test_dashboard.py", DASHBOARD_TEST_CONTENT),
|
|
21
|
+
("tests/__init__.py", ""),
|
|
22
|
+
("src/__init__.py", ""),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
DEPENDENCIES = ["streamlit", "pandas", "plotly"]
|
|
26
|
+
|
|
27
|
+
NEXT_STEPS = [
|
|
28
|
+
"cd {project_name}",
|
|
29
|
+
"uv run streamlit run dashboard/app.py",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DashboardTemplate(BaseTemplate):
|
|
34
|
+
data_type = "Dashboard"
|
|
35
|
+
_CI_BASE = GITHUB_ACTIONS_CI_BASE
|
|
36
|
+
_CI_RUFF_STEP = GITHUB_ACTIONS_CI_RUFF_STEP
|
|
37
|
+
_CI_PYTEST_STEP = GITHUB_ACTIONS_CI_PYTEST_STEP
|
|
38
|
+
|
|
39
|
+
def __init__(self, extras: list[str] | None = None) -> None:
|
|
40
|
+
self.extras = extras or []
|
|
41
|
+
super().__init__(
|
|
42
|
+
name="Data Project",
|
|
43
|
+
folders=list(FOLDERS),
|
|
44
|
+
starter_files=list(FILES),
|
|
45
|
+
next_steps=list(NEXT_STEPS),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def get_readme_content(self, context: dict) -> str | None:
|
|
49
|
+
return make_readme().format_map(context)
|
|
50
|
+
|
|
51
|
+
def get_dependencies(self) -> list[str]:
|
|
52
|
+
base = list(DEPENDENCIES)
|
|
53
|
+
if "pytest" in self.extras:
|
|
54
|
+
base.append("pytest")
|
|
55
|
+
if "ruff" in self.extras:
|
|
56
|
+
base.append("ruff")
|
|
57
|
+
return base
|
|
58
|
+
|
|
59
|
+
def post_install(self, project_path: Path) -> None:
|
|
60
|
+
from spawn.templates.data_project import _DataProjectPostInstallMixin
|
|
61
|
+
_DataProjectPostInstallMixin.post_install(self, project_path) # type: ignore[arg-type]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from spawn.templates.data_project.analysis.content import SAMPLE_CSV
|
|
2
|
+
|
|
3
|
+
# Reuse the same sample dataset — date/category/value fits both use cases
|
|
4
|
+
DASHBOARD_SAMPLE_CSV = SAMPLE_CSV
|
|
5
|
+
|
|
6
|
+
DASHBOARD_APP_CONTENT = """\
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import plotly.express as px
|
|
9
|
+
import streamlit as st
|
|
10
|
+
|
|
11
|
+
df = pd.read_csv("data/sample.csv", parse_dates=["date"])
|
|
12
|
+
|
|
13
|
+
st.title("{project_name} Dashboard")
|
|
14
|
+
|
|
15
|
+
# ── Sidebar filters ───────────────────────────────────────────────────────
|
|
16
|
+
st.sidebar.header("Filters")
|
|
17
|
+
all_categories = sorted(df["category"].unique().tolist())
|
|
18
|
+
selected_categories = st.sidebar.multiselect(
|
|
19
|
+
"Category",
|
|
20
|
+
options=all_categories,
|
|
21
|
+
default=all_categories,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
filtered = df[df["category"].isin(selected_categories)]
|
|
25
|
+
|
|
26
|
+
# ── Raw data ──────────────────────────────────────────────────────────────
|
|
27
|
+
st.subheader("Raw Data")
|
|
28
|
+
st.dataframe(filtered, width="stretch")
|
|
29
|
+
|
|
30
|
+
# ── Chart ─────────────────────────────────────────────────────────────────
|
|
31
|
+
st.subheader("Total Value by Category")
|
|
32
|
+
summary = filtered.groupby("category")["value"].sum().reset_index()
|
|
33
|
+
fig = px.bar(
|
|
34
|
+
summary,
|
|
35
|
+
x="category",
|
|
36
|
+
y="value",
|
|
37
|
+
color="category",
|
|
38
|
+
title="Total Value by Category",
|
|
39
|
+
labels={{"category": "Category", "value": "Total Value"}},
|
|
40
|
+
)
|
|
41
|
+
st.plotly_chart(fig, width="stretch")
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
DASHBOARD_TEST_CONTENT = """\
|
|
45
|
+
def test_sample_csv_loads_with_expected_columns():
|
|
46
|
+
import pandas as pd
|
|
47
|
+
|
|
48
|
+
df = pd.read_csv("data/sample.csv")
|
|
49
|
+
assert len(df) > 0, "data/sample.csv is empty"
|
|
50
|
+
assert "date" in df.columns
|
|
51
|
+
assert "category" in df.columns
|
|
52
|
+
assert "value" in df.columns
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_dashboard_app_exists():
|
|
56
|
+
from pathlib import Path
|
|
57
|
+
|
|
58
|
+
assert Path("dashboard/app.py").exists(), "dashboard/app.py is missing"
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def make_readme() -> str:
|
|
63
|
+
return (
|
|
64
|
+
"# {project_name}\n\n"
|
|
65
|
+
"An interactive Dashboard generated with Spawn using Streamlit and Plotly.\n\n"
|
|
66
|
+
"## Getting Started\n\n"
|
|
67
|
+
"1. Install dependencies:\n\n"
|
|
68
|
+
"```bash\n"
|
|
69
|
+
"uv sync\n"
|
|
70
|
+
"```\n\n"
|
|
71
|
+
"2. Launch the dashboard:\n\n"
|
|
72
|
+
"```bash\n"
|
|
73
|
+
"uv run streamlit run dashboard/app.py\n"
|
|
74
|
+
"```\n\n"
|
|
75
|
+
"3. Open the URL shown in the terminal (usually http://localhost:8501).\n\n"
|
|
76
|
+
"## Example\n\n"
|
|
77
|
+
"```\n"
|
|
78
|
+
"$ uv run streamlit run dashboard/app.py\n"
|
|
79
|
+
" Local URL: http://localhost:8501\n"
|
|
80
|
+
"# Use the sidebar to filter by category\n"
|
|
81
|
+
"```\n\n"
|
|
82
|
+
"## Adding Data\n\n"
|
|
83
|
+
"Replace `data/sample.csv` with your own CSV that has `date`, `category`,\n"
|
|
84
|
+
"and `value` columns (or update `dashboard/app.py` to match your schema).\n\n"
|
|
85
|
+
"## Project Structure\n\n"
|
|
86
|
+
"```\n"
|
|
87
|
+
"{project_name}/\n"
|
|
88
|
+
"├── data/\n"
|
|
89
|
+
"│ └── sample.csv # Sample sales data\n"
|
|
90
|
+
"├── dashboard/\n"
|
|
91
|
+
"│ └── app.py # Streamlit application\n"
|
|
92
|
+
"├── src/\n"
|
|
93
|
+
"├── tests/\n"
|
|
94
|
+
"│ └── test_dashboard.py\n"
|
|
95
|
+
"└── README.md\n"
|
|
96
|
+
"```\n\n"
|
|
97
|
+
"## Running Tests\n\n"
|
|
98
|
+
"```bash\n"
|
|
99
|
+
"uv run pytest\n"
|
|
100
|
+
"```\n"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# ─── GitHub Actions ───────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
GITHUB_ACTIONS_CI_BASE = """\
|
|
106
|
+
name: CI
|
|
107
|
+
|
|
108
|
+
on:
|
|
109
|
+
push:
|
|
110
|
+
branches: [main]
|
|
111
|
+
pull_request:
|
|
112
|
+
branches: [main]
|
|
113
|
+
|
|
114
|
+
jobs:
|
|
115
|
+
ci:
|
|
116
|
+
runs-on: ubuntu-latest
|
|
117
|
+
|
|
118
|
+
steps:
|
|
119
|
+
- uses: actions/checkout@v4
|
|
120
|
+
|
|
121
|
+
- uses: astral-sh/setup-uv@v5
|
|
122
|
+
|
|
123
|
+
- name: Install dependencies
|
|
124
|
+
run: uv sync
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
GITHUB_ACTIONS_CI_RUFF_STEP = """\
|
|
128
|
+
- name: Lint
|
|
129
|
+
run: uv run ruff check .
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
GITHUB_ACTIONS_CI_PYTEST_STEP = """\
|
|
133
|
+
- name: Test
|
|
134
|
+
run: uv run pytest
|
|
135
|
+
"""
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from spawn.templates.base import BaseTemplate
|
|
4
|
+
from spawn.templates.data_project.etl.content import (
|
|
5
|
+
ETL_RAW_CSV,
|
|
6
|
+
ETL_CLEAN_CONTENT,
|
|
7
|
+
ETL_RUN_CONTENT,
|
|
8
|
+
ETL_ENV_CONTENT,
|
|
9
|
+
ETL_TEST_CONTENT,
|
|
10
|
+
make_readme,
|
|
11
|
+
GITHUB_ACTIONS_CI_BASE,
|
|
12
|
+
GITHUB_ACTIONS_CI_RUFF_STEP,
|
|
13
|
+
GITHUB_ACTIONS_CI_PYTEST_STEP,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
FOLDERS = ["data", "pipelines", "src", "tests"]
|
|
17
|
+
|
|
18
|
+
FILES = [
|
|
19
|
+
("data/raw.csv", ETL_RAW_CSV),
|
|
20
|
+
("pipelines/__init__.py", ""),
|
|
21
|
+
("pipelines/clean_data.py", ETL_CLEAN_CONTENT),
|
|
22
|
+
("pipelines/run.py", ETL_RUN_CONTENT),
|
|
23
|
+
("tests/test_etl.py", ETL_TEST_CONTENT),
|
|
24
|
+
("tests/__init__.py", ""),
|
|
25
|
+
("src/__init__.py", ""),
|
|
26
|
+
(".env.example", ETL_ENV_CONTENT),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
DEPENDENCIES = ["pandas", "python-dotenv"]
|
|
30
|
+
|
|
31
|
+
NEXT_STEPS = [
|
|
32
|
+
"cd {project_name}",
|
|
33
|
+
"Rename .env.example to .env if you want custom paths",
|
|
34
|
+
"uv run python -m pipelines.run",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ETLPipelineTemplate(BaseTemplate):
|
|
39
|
+
data_type = "ETL Pipeline"
|
|
40
|
+
_CI_BASE = GITHUB_ACTIONS_CI_BASE
|
|
41
|
+
_CI_RUFF_STEP = GITHUB_ACTIONS_CI_RUFF_STEP
|
|
42
|
+
_CI_PYTEST_STEP = GITHUB_ACTIONS_CI_PYTEST_STEP
|
|
43
|
+
|
|
44
|
+
def __init__(self, extras: list[str] | None = None) -> None:
|
|
45
|
+
self.extras = extras or []
|
|
46
|
+
super().__init__(
|
|
47
|
+
name="Data Project",
|
|
48
|
+
folders=list(FOLDERS),
|
|
49
|
+
starter_files=list(FILES),
|
|
50
|
+
next_steps=list(NEXT_STEPS),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def get_readme_content(self, context: dict) -> str | None:
|
|
54
|
+
return make_readme().format_map(context)
|
|
55
|
+
|
|
56
|
+
def get_dependencies(self) -> list[str]:
|
|
57
|
+
base = list(DEPENDENCIES)
|
|
58
|
+
if "pytest" in self.extras:
|
|
59
|
+
base.append("pytest")
|
|
60
|
+
if "ruff" in self.extras:
|
|
61
|
+
base.append("ruff")
|
|
62
|
+
return base
|
|
63
|
+
|
|
64
|
+
def post_install(self, project_path: Path) -> None:
|
|
65
|
+
from spawn.templates.data_project import _DataProjectPostInstallMixin
|
|
66
|
+
_DataProjectPostInstallMixin.post_install(self, project_path) # type: ignore[arg-type]
|