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,115 @@
1
+ import datetime
2
+ import json
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ from spawn import __version__
7
+ from spawn.templates.shared_content import (
8
+ README_CONTENT,
9
+ GITIGNORE_CONTENT,
10
+ AGENTS_MD_CONTENT,
11
+ )
12
+ from spawn.core.models import ProjectConfig
13
+ from spawn.core.registry import instantiate_template
14
+ from spawn.utils.git import initialize_git
15
+ from spawn.utils.uv import initialize_uv, install_packages
16
+ from spawn.core.exceptions import SpawnError
17
+ from spawn.utils.console import console
18
+
19
+
20
+ class ProjectGenerator:
21
+ def generate(self, config: ProjectConfig) -> Path:
22
+ template = instantiate_template(config)
23
+
24
+ if template is None:
25
+ raise SpawnError(
26
+ f"Unknown template: {config.template}"
27
+ )
28
+
29
+ project_path = Path(config.name)
30
+
31
+ if project_path.exists():
32
+ raise SpawnError(
33
+ f"Directory '{config.name}' already exists."
34
+ )
35
+
36
+ try:
37
+ project_path.mkdir()
38
+
39
+ context = {"project_name": config.name}
40
+ template.generate(project_path, context)
41
+
42
+ readme_content = template.get_readme_content(context)
43
+ if readme_content is None:
44
+ readme_content = README_CONTENT.format(project_name=config.name)
45
+
46
+ readme_path = project_path / "README.md"
47
+ readme_path.write_text(readme_content, encoding="utf-8")
48
+
49
+ agents_md_content = template.get_agents_md_content(context)
50
+ if agents_md_content is None:
51
+ agents_md_content = AGENTS_MD_CONTENT.format(project_name=config.name)
52
+
53
+ agents_md_path = project_path / "AGENTS.md"
54
+ agents_md_path.write_text(agents_md_content, encoding="utf-8")
55
+
56
+ if config.generate_claude_md:
57
+ claude_md_path = project_path / "CLAUDE.md"
58
+ claude_md_path.write_text(agents_md_content, encoding="utf-8")
59
+
60
+ gitignore_path = project_path / ".gitignore"
61
+
62
+ gitignore_path.write_text(
63
+ GITIGNORE_CONTENT,
64
+ encoding="utf-8",
65
+ )
66
+
67
+ if config.use_git:
68
+ console.print(
69
+ "[yellow]Initializing Git...[/yellow]"
70
+ )
71
+ initialize_git(project_path)
72
+
73
+ initialize_uv(project_path)
74
+
75
+ deps = template.get_dependencies()
76
+ if deps:
77
+ console.print(
78
+ "[yellow]Installing dependencies...[/yellow]"
79
+ )
80
+ install_packages(project_path, deps)
81
+
82
+ template.post_install(project_path)
83
+
84
+ meta_dir = project_path / ".spawn"
85
+ meta_dir.mkdir()
86
+ meta_file = meta_dir / "meta.json"
87
+ meta_file.write_text(
88
+ json.dumps(
89
+ {
90
+ "intent": config.template,
91
+ "framework": config.framework,
92
+ "provider": config.provider,
93
+ "spawn_version": __version__,
94
+ "created_at": datetime.datetime.now(
95
+ datetime.timezone.utc
96
+ ).isoformat(),
97
+ "generator": "blueprint",
98
+ "git": config.use_git,
99
+ "uv": True,
100
+ "source": None,
101
+ },
102
+ indent=2,
103
+ ),
104
+ encoding="utf-8",
105
+ )
106
+
107
+ except OSError as e:
108
+ shutil.rmtree(project_path, ignore_errors=True)
109
+ raise SpawnError(str(e)) from e
110
+
111
+ except BaseException:
112
+ shutil.rmtree(project_path, ignore_errors=True)
113
+ raise
114
+
115
+ return project_path
File without changes
@@ -0,0 +1,5 @@
1
+ from spawn.core.exceptions import SpawnError
2
+
3
+
4
+ class GitHubPublishError(SpawnError):
5
+ """Raised when GitHub publishing fails."""
@@ -0,0 +1,69 @@
1
+ from pathlib import Path
2
+
3
+ from spawn.github.validators import (
4
+ is_valid_github_url,
5
+ )
6
+ from spawn.github.exceptions import (
7
+ GitHubPublishError,
8
+ )
9
+ from spawn.core.exceptions import SpawnError
10
+
11
+ from spawn.utils.git import (
12
+ add_all,
13
+ commit,
14
+ rename_main_branch,
15
+ add_remote,
16
+ push_origin_main,
17
+ remote_exists,
18
+ is_git_repository,
19
+ )
20
+
21
+
22
+ class GitHubPublisher:
23
+ def publish(
24
+ self,
25
+ project_path: Path,
26
+ repo_url: str,
27
+ ) -> None:
28
+
29
+ if not project_path.exists():
30
+ raise GitHubPublishError(
31
+ f"Project path does not exist: {project_path}"
32
+ )
33
+
34
+ if not is_valid_github_url(repo_url):
35
+ raise GitHubPublishError(
36
+ "Invalid GitHub repository URL."
37
+ )
38
+
39
+ if not is_git_repository(project_path):
40
+ raise GitHubPublishError(
41
+ "Project is not a Git repository."
42
+ )
43
+
44
+ if remote_exists(project_path):
45
+ raise GitHubPublishError(
46
+ "Origin remote already exists."
47
+ )
48
+
49
+ try:
50
+ add_all(project_path)
51
+
52
+ commit(
53
+ project_path,
54
+ "Initial commit",
55
+ )
56
+
57
+ rename_main_branch(project_path)
58
+
59
+ add_remote(
60
+ project_path,
61
+ repo_url,
62
+ )
63
+
64
+ push_origin_main(project_path)
65
+
66
+ except SpawnError as exc:
67
+ raise GitHubPublishError(
68
+ str(exc)
69
+ ) from exc
@@ -0,0 +1,23 @@
1
+ import re
2
+
3
+
4
+ _GITHUB_PATTERNS = (
5
+ r"^https://github\.com/[a-zA-Z0-9][a-zA-Z0-9_-]*/[a-zA-Z0-9][a-zA-Z0-9_.-]*/?$",
6
+ r"^https://github\.com/[a-zA-Z0-9][a-zA-Z0-9_-]*/[a-zA-Z0-9][a-zA-Z0-9_.-]*\.git$",
7
+ r"^git@github\.com:[a-zA-Z0-9][a-zA-Z0-9_-]*/[a-zA-Z0-9][a-zA-Z0-9_.-]*\.git$",
8
+ )
9
+
10
+
11
+ def is_valid_github_url(url: str) -> bool:
12
+ """
13
+ Validate supported GitHub repository URLs.
14
+ """
15
+ url = url.strip()
16
+
17
+ if not url:
18
+ return False
19
+
20
+ return any(
21
+ re.match(pattern, url)
22
+ for pattern in _GITHUB_PATTERNS
23
+ )
File without changes
@@ -0,0 +1,182 @@
1
+ from pathlib import Path
2
+
3
+ from spawn.templates.base import BaseTemplate
4
+ from spawn.templates.agent.content import (
5
+ INIT_CONTENT,
6
+ AGENT_PROMPT_TXT_CONTENT,
7
+ SETTINGS_CONTENT,
8
+ CALCULATOR_TOOL_CONTENT,
9
+ PYDANTIC_AI_AGENT_CONTENT,
10
+ OPENAI_AGENTS_AGENT_CONTENT,
11
+ MAIN_CONTENT,
12
+ PYDANTIC_AI_TEST_CONTENT,
13
+ OPENAI_AGENTS_TEST_CONTENT,
14
+ CONFTEST_CONTENT,
15
+ GITHUB_ACTIONS_CI_BASE,
16
+ GITHUB_ACTIONS_CI_RUFF_STEP,
17
+ GITHUB_ACTIONS_CI_PYTEST_STEP,
18
+ ENV_PYDANTIC_OPENAI,
19
+ ENV_PYDANTIC_ANTHROPIC,
20
+ ENV_PYDANTIC_GEMINI,
21
+ ENV_PYDANTIC_OPENROUTER,
22
+ ENV_PYDANTIC_OLLAMA,
23
+ ENV_PYDANTIC_GROQ,
24
+ ENV_OPENAI_AGENTS_OPENAI,
25
+ ENV_OPENAI_AGENTS_OPENROUTER,
26
+ make_readme,
27
+ make_agents_md,
28
+ )
29
+
30
+ AGENT_FOLDERS = [
31
+ "src/agent",
32
+ "src/tools",
33
+ "src/prompts",
34
+ "src/config",
35
+ "tests",
36
+ ]
37
+
38
+ # Maps (framework, provider) → agent run.py content
39
+ _AGENT_MAP: dict[tuple[str, str], str] = {
40
+ ("pydantic-ai", "openai"): PYDANTIC_AI_AGENT_CONTENT,
41
+ ("pydantic-ai", "anthropic"): PYDANTIC_AI_AGENT_CONTENT,
42
+ ("pydantic-ai", "gemini"): PYDANTIC_AI_AGENT_CONTENT,
43
+ ("pydantic-ai", "openrouter"): PYDANTIC_AI_AGENT_CONTENT,
44
+ ("pydantic-ai", "ollama"): PYDANTIC_AI_AGENT_CONTENT,
45
+ ("pydantic-ai", "groq"): PYDANTIC_AI_AGENT_CONTENT,
46
+ ("openai-agents", "openai"): OPENAI_AGENTS_AGENT_CONTENT,
47
+ ("openai-agents", "openrouter"): OPENAI_AGENTS_AGENT_CONTENT,
48
+ }
49
+
50
+ # Maps (framework, provider) → env example
51
+ _ENV_MAP: dict[tuple[str, str], str] = {
52
+ ("pydantic-ai", "openai"): ENV_PYDANTIC_OPENAI,
53
+ ("pydantic-ai", "anthropic"): ENV_PYDANTIC_ANTHROPIC,
54
+ ("pydantic-ai", "gemini"): ENV_PYDANTIC_GEMINI,
55
+ ("pydantic-ai", "openrouter"): ENV_PYDANTIC_OPENROUTER,
56
+ ("pydantic-ai", "ollama"): ENV_PYDANTIC_OLLAMA,
57
+ ("pydantic-ai", "groq"): ENV_PYDANTIC_GROQ,
58
+ ("openai-agents", "openai"): ENV_OPENAI_AGENTS_OPENAI,
59
+ ("openai-agents", "openrouter"): ENV_OPENAI_AGENTS_OPENROUTER,
60
+ }
61
+
62
+ # Maps framework → test content
63
+ _TEST_MAP: dict[str, str] = {
64
+ "pydantic-ai": PYDANTIC_AI_TEST_CONTENT,
65
+ "openai-agents": OPENAI_AGENTS_TEST_CONTENT,
66
+ }
67
+
68
+ AGENT_PROVIDER_MAP: dict[str, list[str]] = {
69
+ "pydantic-ai": ["openai", "anthropic", "gemini", "openrouter", "ollama", "groq"],
70
+ "openai-agents": ["openai", "openrouter"],
71
+ }
72
+
73
+
74
+ def get_supported_providers(framework: str) -> list[str]:
75
+ return AGENT_PROVIDER_MAP.get(framework, ["openai"])
76
+
77
+
78
+ def _build_files(
79
+ agent_content: str,
80
+ test_content: str,
81
+ env_content: str,
82
+ ) -> list:
83
+ return [
84
+ ("src/__init__.py", INIT_CONTENT),
85
+ ("src/agent/__init__.py", INIT_CONTENT),
86
+ ("src/agent/run.py", agent_content),
87
+ ("src/tools/__init__.py", INIT_CONTENT),
88
+ ("src/tools/calculator.py", CALCULATOR_TOOL_CONTENT),
89
+ ("src/prompts/__init__.py", INIT_CONTENT),
90
+ ("src/prompts/agent_prompt.txt", AGENT_PROMPT_TXT_CONTENT),
91
+ ("src/config/__init__.py", INIT_CONTENT),
92
+ ("src/config/settings.py", SETTINGS_CONTENT),
93
+ ("src/main.py", MAIN_CONTENT),
94
+ ("tests/__init__.py", INIT_CONTENT),
95
+ ("tests/conftest.py", CONFTEST_CONTENT),
96
+ ("tests/test_agent.py", test_content),
97
+ (".env.example", env_content),
98
+ ]
99
+
100
+
101
+ class AgentTemplate(BaseTemplate):
102
+ def __init__(
103
+ self,
104
+ framework: str | None = None,
105
+ provider: str | None = None,
106
+ extras: list[str] | None = None,
107
+ ) -> None:
108
+ self.framework = framework or "pydantic-ai"
109
+ self.provider = provider or "openai"
110
+ self.extras = extras or []
111
+
112
+ key = (self.framework, self.provider)
113
+ agent_content = _AGENT_MAP.get(key, PYDANTIC_AI_AGENT_CONTENT)
114
+ env_content = _ENV_MAP.get(key, ENV_PYDANTIC_OPENAI)
115
+ test_content = _TEST_MAP.get(self.framework, PYDANTIC_AI_TEST_CONTENT)
116
+
117
+ super().__init__(
118
+ name="AI Agent",
119
+ folders=list(AGENT_FOLDERS),
120
+ starter_files=_build_files(agent_content, test_content, env_content),
121
+ next_steps=[
122
+ "cd {project_name}",
123
+ "Rename .env.example to .env and fill in your API key",
124
+ "uv run python -m src.main",
125
+ ],
126
+ )
127
+
128
+ def get_readme_content(self, context: dict) -> str | None:
129
+ raw = make_readme(self.framework, self.provider)
130
+ return raw.format_map(context)
131
+
132
+ def get_agents_md_content(self, context: dict) -> str | None:
133
+ raw = make_agents_md(self.framework, self.provider)
134
+ return raw.format_map(context)
135
+
136
+ def get_dependencies(self) -> list[str]:
137
+ dep_map: dict[tuple[str, str], list[str]] = {
138
+ ("pydantic-ai", "openai"): ["pydantic-ai", "python-dotenv"],
139
+ ("pydantic-ai", "anthropic"): ["pydantic-ai", "python-dotenv"],
140
+ ("pydantic-ai", "gemini"): ["pydantic-ai", "python-dotenv"],
141
+ ("pydantic-ai", "openrouter"): ["pydantic-ai", "python-dotenv"],
142
+ ("pydantic-ai", "ollama"): ["pydantic-ai", "python-dotenv"],
143
+ ("pydantic-ai", "groq"): ["pydantic-ai[groq]", "python-dotenv"],
144
+ ("openai-agents", "openai"): ["openai-agents", "python-dotenv"],
145
+ ("openai-agents", "openrouter"): ["openai-agents", "python-dotenv"],
146
+ }
147
+
148
+ base = list(dep_map.get(
149
+ (self.framework, self.provider),
150
+ ["pydantic-ai", "python-dotenv"],
151
+ ))
152
+
153
+ if "pytest" in self.extras:
154
+ base.append("pytest")
155
+ if "ruff" in self.extras:
156
+ base.append("ruff")
157
+
158
+ return base
159
+
160
+ def post_install(self, project_path: Path) -> None:
161
+ pyproject = project_path / "pyproject.toml"
162
+ current = pyproject.read_text(encoding="utf-8")
163
+ additions = ""
164
+
165
+ if "pytest" in self.extras:
166
+ additions += "\n[tool.pytest.ini_options]\ntestpaths = [\"tests\"]\n"
167
+
168
+ if "ruff" in self.extras:
169
+ additions += "\n[tool.ruff]\nline-length = 88\n"
170
+
171
+ if additions:
172
+ pyproject.write_text(current + additions, encoding="utf-8")
173
+
174
+ if "github-actions" in self.extras:
175
+ workflows_path = project_path / ".github" / "workflows"
176
+ workflows_path.mkdir(parents=True, exist_ok=True)
177
+ ci = GITHUB_ACTIONS_CI_BASE
178
+ if "ruff" in self.extras:
179
+ ci += GITHUB_ACTIONS_CI_RUFF_STEP
180
+ if "pytest" in self.extras:
181
+ ci += GITHUB_ACTIONS_CI_PYTEST_STEP
182
+ (workflows_path / "ci.yml").write_text(ci, encoding="utf-8")