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,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
|
spawn/github/__init__.py
ADDED
|
File without changes
|
|
@@ -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")
|