spikard_cli/init/
python.rs1use super::scaffolder::{ProjectScaffolder, ScaffoldedFile};
12use anyhow::Result;
13use std::path::Path;
14use std::path::PathBuf;
15
16pub struct PythonScaffolder;
18
19impl PythonScaffolder {
20 fn to_package_name(project_name: &str) -> String {
22 project_name.replace('-', "_").to_lowercase()
24 }
25
26 fn generate_pyproject_toml(project_name: &str, _package_name: &str) -> String {
28 let version = env!("CARGO_PKG_VERSION");
29 format!(
30 r#"[project]
31name = "{project_name}"
32version = "0.1.0"
33description = "A Spikard application"
34requires-python = ">=3.10"
35dependencies = [
36 "spikard>={version}",
37]
38
39[build-system]
40requires = ["hatchling"]
41build-backend = "hatchling.build"
42
43[dependency-groups]
44dev = [
45 "pytest>=8.0.0",
46 "mypy>=1.8.0",
47 "ruff>=0.3.0",
48]
49"#
50 )
51 }
52
53 fn generate_app_module(_package_name: &str) -> String {
55 format!(
56 r#""""Main application module."""
57from spikard import ServerConfig, Spikard
58
59app = Spikard()
60
61@app.get("/health")
62async def health() -> dict[str, str]:
63 """Health check endpoint."""
64 return {{"status": "ok"}}
65
66
67if __name__ == "__main__":
68 app.run(config=ServerConfig(host="127.0.0.1", port=8000))
69"#
70 )
71 }
72
73 fn generate_init_py() -> String {
75 r#""""Spikard application package.""""#.to_string()
76 }
77
78 fn generate_test_app(package_name: &str) -> String {
80 format!(
81 r#""""Tests for the application."""
82import pytest
83from {package_name}.app import app
84
85
86@pytest.fixture
87def client():
88 """Create a test client."""
89 from spikard.testing import TestClient
90 return TestClient(app)
91
92
93def test_health(client):
94 """Test health endpoint."""
95 response = client.get("/health")
96 assert response.status_code == 200
97 assert response.json() == {{"status": "ok"}}
98"#
99 )
100 }
101
102 fn generate_gitignore() -> String {
104 r"__pycache__/
105*.py[cod]
106*$py.class
107*.so
108.Python
109build/
110develop-eggs/
111dist/
112downloads/
113eggs/
114.eggs/
115lib/
116lib64/
117parts/
118sdist/
119var/
120wheels/
121*.egg-info/
122.installed.cfg
123*.egg
124.pytest_cache/
125.mypy_cache/
126.ruff_cache/
127.venv/
128venv/
129ENV/
130env/
131.vscode/
132.idea/
133*.swp
134*.swo
135*~
136.DS_Store
137"
138 .to_string()
139 }
140
141 fn generate_readme(project_name: &str, package_name: &str) -> String {
143 format!(
144 r"# {project_name}
145
146A Spikard application.
147
148## Setup
149
150Install dependencies with uv:
151
152```bash
153uv sync
154```
155
156## Run
157
158Start the development server:
159
160```bash
161uv run python -m {package_name}.app
162```
163
164## Test
165
166Run the test suite:
167
168```bash
169uv run pytest
170```
171
172## Type checking
173
174Run mypy to check types:
175
176```bash
177uv run mypy src/{package_name}/
178```
179
180## Linting
181
182Run ruff to lint code:
183
184```bash
185uv run ruff check src/{package_name}/ tests/
186uv run ruff format src/{package_name}/ tests/
187```
188
189## Documentation
190
191For more information about Spikard, visit:
192- [Spikard Documentation](https://spikard.dev)
193- [Spikard GitHub](https://github.com/Goldziher/spikard)
194"
195 )
196 }
197}
198
199impl ProjectScaffolder for PythonScaffolder {
200 fn scaffold(&self, _project_dir: &Path, project_name: &str) -> Result<Vec<ScaffoldedFile>> {
201 let package_name = Self::to_package_name(project_name);
202
203 let mut files = Vec::new();
204
205 files.push(ScaffoldedFile::new(
207 PathBuf::from("pyproject.toml"),
208 Self::generate_pyproject_toml(project_name, &package_name),
209 ));
210
211 files.push(ScaffoldedFile::new(
213 PathBuf::from(format!("src/{package_name}/__init__.py")),
214 Self::generate_init_py(),
215 ));
216
217 files.push(ScaffoldedFile::new(
219 PathBuf::from(format!("src/{package_name}/app.py")),
220 Self::generate_app_module(&package_name),
221 ));
222
223 files.push(ScaffoldedFile::new(
225 PathBuf::from("tests/test_app.py"),
226 Self::generate_test_app(&package_name),
227 ));
228
229 files.push(ScaffoldedFile::new(
231 PathBuf::from(".gitignore"),
232 Self::generate_gitignore(),
233 ));
234
235 files.push(ScaffoldedFile::new(
237 PathBuf::from("README.md"),
238 Self::generate_readme(project_name, &package_name),
239 ));
240
241 Ok(files)
242 }
243
244 fn next_steps(&self, project_name: &str) -> Vec<String> {
245 let package_name = Self::to_package_name(project_name);
246 vec![
247 format!("cd {}", project_name),
248 "uv sync".to_string(),
249 format!("uv run python -m {}.app", package_name),
250 ]
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_to_package_name() {
260 assert_eq!(PythonScaffolder::to_package_name("my-app"), "my_app");
261 assert_eq!(PythonScaffolder::to_package_name("MyApp"), "myapp");
262 assert_eq!(PythonScaffolder::to_package_name("my_app"), "my_app");
263 assert_eq!(PythonScaffolder::to_package_name("MY_APP"), "my_app");
264 }
265
266 #[test]
267 #[allow(clippy::cmp_owned)]
268 fn test_scaffold_creates_files() {
269 let files = PythonScaffolder.scaffold(Path::new("."), "test_app").unwrap();
270
271 assert!(files.iter().any(|f| f.path == PathBuf::from("pyproject.toml")));
273 assert!(files.iter().any(|f| f.path == PathBuf::from("tests/test_app.py")));
274 assert!(files.iter().any(|f| f.path == PathBuf::from(".gitignore")));
275 assert!(files.iter().any(|f| f.path == PathBuf::from("README.md")));
276 }
277
278 #[test]
279 fn test_next_steps() {
280 let steps = PythonScaffolder.next_steps("my-app");
281 assert_eq!(steps.len(), 3);
282 assert!(steps[0].contains("my-app"));
283 assert_eq!(steps[1], "uv sync");
284 assert!(steps[2].contains("my_app.app"));
285 }
286
287 #[test]
288 fn test_pyproject_contains_spikard() {
289 let content = PythonScaffolder::generate_pyproject_toml("test-app", "test_app");
290 assert!(content.contains("spikard>="));
291 assert!(content.contains("pytest"));
292 assert!(content.contains("mypy"));
293 assert!(!content.contains("[tool.uv.sources]"));
294 }
295
296 #[test]
297 fn test_app_module_contains_health_endpoint() {
298 let content = PythonScaffolder::generate_app_module("test_app");
299 assert!(content.contains("@app.get(\"/health\")"));
300 assert!(content.contains("status"));
301 assert!(content.contains("ok"));
302 assert!(content.contains("Spikard()"));
303 assert!(content.contains("ServerConfig"));
304 }
305}