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.
Files changed (56) hide show
  1. spawn/__init__.py +6 -0
  2. spawn/cli/__init__.py +0 -0
  3. spawn/cli/app.py +218 -0
  4. spawn/cli/noninteractive.py +204 -0
  5. spawn/cli/prompts.py +267 -0
  6. spawn/core/__init__.py +0 -0
  7. spawn/core/exceptions.py +7 -0
  8. spawn/core/models.py +21 -0
  9. spawn/core/registry.py +152 -0
  10. spawn/generators/__init__.py +0 -0
  11. spawn/generators/custom_structure.py +508 -0
  12. spawn/generators/project_generator.py +115 -0
  13. spawn/github/__init__.py +0 -0
  14. spawn/github/exceptions.py +5 -0
  15. spawn/github/publisher.py +69 -0
  16. spawn/github/validators.py +23 -0
  17. spawn/templates/__init__.py +0 -0
  18. spawn/templates/agent/__init__.py +182 -0
  19. spawn/templates/agent/content.py +374 -0
  20. spawn/templates/automation/__init__.py +100 -0
  21. spawn/templates/automation/content.py +181 -0
  22. spawn/templates/backend_api/__init__.py +243 -0
  23. spawn/templates/backend_api/content.py +509 -0
  24. spawn/templates/base.py +58 -0
  25. spawn/templates/chatbot/__init__.py +243 -0
  26. spawn/templates/chatbot/content.py +668 -0
  27. spawn/templates/cli_application/__init__.py +211 -0
  28. spawn/templates/cli_application/content.py +512 -0
  29. spawn/templates/data_project/__init__.py +125 -0
  30. spawn/templates/data_project/analysis/__init__.py +74 -0
  31. spawn/templates/data_project/analysis/content.py +190 -0
  32. spawn/templates/data_project/dashboard/__init__.py +61 -0
  33. spawn/templates/data_project/dashboard/content.py +135 -0
  34. spawn/templates/data_project/etl/__init__.py +66 -0
  35. spawn/templates/data_project/etl/content.py +239 -0
  36. spawn/templates/data_project/ml/__init__.py +62 -0
  37. spawn/templates/data_project/ml/content.py +239 -0
  38. spawn/templates/mcp_server/__init__.py +83 -0
  39. spawn/templates/mcp_server/content.py +161 -0
  40. spawn/templates/rag/__init__.py +124 -0
  41. spawn/templates/rag/content.py +463 -0
  42. spawn/templates/shared_content.py +89 -0
  43. spawn/utils/__init__.py +0 -0
  44. spawn/utils/banner.py +41 -0
  45. spawn/utils/console.py +3 -0
  46. spawn/utils/doctor.py +594 -0
  47. spawn/utils/git.py +132 -0
  48. spawn/utils/success.py +47 -0
  49. spawn/utils/theme.py +25 -0
  50. spawn/utils/uv.py +61 -0
  51. spawn/utils/validators.py +15 -0
  52. spawnio-1.0.5.dist-info/METADATA +798 -0
  53. spawnio-1.0.5.dist-info/RECORD +56 -0
  54. spawnio-1.0.5.dist-info/WHEEL +4 -0
  55. spawnio-1.0.5.dist-info/entry_points.txt +2 -0
  56. 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]