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,374 @@
|
|
|
1
|
+
INIT_CONTENT = ""
|
|
2
|
+
|
|
3
|
+
# ─── Shared content ───────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
AGENT_PROMPT_TXT_CONTENT = """\
|
|
6
|
+
You are a helpful AI assistant with access to tools.
|
|
7
|
+
Use tools when they are helpful to answer the user's request.
|
|
8
|
+
Be concise and accurate in your responses.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
SETTINGS_CONTENT = """\
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from dotenv import load_dotenv
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_env() -> None:
|
|
19
|
+
load_dotenv()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_agent_prompt() -> str:
|
|
23
|
+
prompt_path = Path(__file__).parent.parent / "prompts" / "agent_prompt.txt"
|
|
24
|
+
return prompt_path.read_text(encoding="utf-8").strip()
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
CALCULATOR_TOOL_CONTENT = """\
|
|
28
|
+
import re
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def calculate(expression: str) -> str:
|
|
32
|
+
\"\"\"
|
|
33
|
+
Evaluate a mathematical expression safely.
|
|
34
|
+
Supports: +, -, *, /, //, **, (, ), and decimal numbers.
|
|
35
|
+
Returns the result as a string, or an error message.
|
|
36
|
+
\"\"\"
|
|
37
|
+
# Strip whitespace and validate characters
|
|
38
|
+
cleaned = expression.strip()
|
|
39
|
+
if not re.match(r'^[\\d\\s\\+\\-\\*\\/\\(\\)\\.\\*\\*\\/\\/]+$', cleaned):
|
|
40
|
+
return f"Invalid expression: only numbers and operators (+,-,*,/,**,//) are allowed"
|
|
41
|
+
try:
|
|
42
|
+
result = eval(cleaned, {{"__builtins__": {{}}}}, {{}})
|
|
43
|
+
return str(result)
|
|
44
|
+
except ZeroDivisionError:
|
|
45
|
+
return "Error: division by zero"
|
|
46
|
+
except Exception as e:
|
|
47
|
+
return f"Error: {{e}}"
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
MAIN_CONTENT = """\
|
|
51
|
+
from src.agent.run import run_agent
|
|
52
|
+
from src.config.settings import load_env
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main() -> None:
|
|
56
|
+
load_env()
|
|
57
|
+
print("{project_name} Agent ready. Type 'quit' to exit.\\n")
|
|
58
|
+
while True:
|
|
59
|
+
user_input = input("You: ").strip()
|
|
60
|
+
if not user_input:
|
|
61
|
+
continue
|
|
62
|
+
if user_input.lower() in ("quit", "exit"):
|
|
63
|
+
break
|
|
64
|
+
print("\\nThinking...\\n")
|
|
65
|
+
response = run_agent(user_input)
|
|
66
|
+
print(f"Agent: {{response}}\\n")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
main()
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
CONFTEST_CONTENT = """\
|
|
74
|
+
# conftest.py — no shared fixtures needed for agent tests
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
# ─── PydanticAI agent ─────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
PYDANTIC_AI_AGENT_CONTENT = """\
|
|
80
|
+
import os
|
|
81
|
+
|
|
82
|
+
from pydantic_ai import Agent
|
|
83
|
+
|
|
84
|
+
from src.config.settings import get_agent_prompt
|
|
85
|
+
from src.tools.calculator import calculate as _calculate
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def create_agent() -> Agent:
|
|
89
|
+
model = os.getenv("MODEL", "openai:gpt-4o-mini")
|
|
90
|
+
system_prompt = get_agent_prompt()
|
|
91
|
+
agent = Agent(model, system_prompt=system_prompt)
|
|
92
|
+
|
|
93
|
+
@agent.tool_plain
|
|
94
|
+
def calculate(expression: str) -> str:
|
|
95
|
+
\"\"\"Evaluate a mathematical expression. Supports +, -, *, /, **, //.\"\"\"
|
|
96
|
+
return _calculate(expression)
|
|
97
|
+
|
|
98
|
+
return agent
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def run_agent(user_input: str) -> str:
|
|
102
|
+
agent = create_agent()
|
|
103
|
+
result = agent.run_sync(user_input)
|
|
104
|
+
return result.output
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
PYDANTIC_AI_TEST_CONTENT = """\
|
|
108
|
+
from unittest.mock import patch, MagicMock
|
|
109
|
+
|
|
110
|
+
from src.tools.calculator import calculate
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_calculator_addition():
|
|
114
|
+
assert calculate("2 + 3") == "5"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_calculator_multiplication():
|
|
118
|
+
assert calculate("6 * 7") == "42"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_calculator_division():
|
|
122
|
+
result = calculate("10 / 4")
|
|
123
|
+
assert "2.5" in result
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_calculator_invalid_expression():
|
|
127
|
+
result = calculate("import os")
|
|
128
|
+
assert "Invalid" in result or "Error" in result
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_run_agent_mock():
|
|
132
|
+
mock_result = MagicMock()
|
|
133
|
+
mock_result.output = "The answer is 42"
|
|
134
|
+
|
|
135
|
+
with patch("src.agent.run.Agent") as MockAgent:
|
|
136
|
+
mock_instance = MagicMock()
|
|
137
|
+
mock_instance.run_sync.return_value = mock_result
|
|
138
|
+
MockAgent.return_value = mock_instance
|
|
139
|
+
|
|
140
|
+
from src.agent.run import run_agent
|
|
141
|
+
|
|
142
|
+
result = run_agent("What is 6 * 7?")
|
|
143
|
+
|
|
144
|
+
assert isinstance(result, str)
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
# ─── OpenAI Agents agent ──────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
OPENAI_AGENTS_AGENT_CONTENT = """\
|
|
150
|
+
import os
|
|
151
|
+
|
|
152
|
+
from agents import Agent, Runner, function_tool
|
|
153
|
+
|
|
154
|
+
from src.config.settings import get_agent_prompt
|
|
155
|
+
from src.tools.calculator import calculate as _calculate
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@function_tool
|
|
159
|
+
def calculate(expression: str) -> str:
|
|
160
|
+
\"\"\"Evaluate a mathematical expression. Supports +, -, *, /, **, //.\"\"\"
|
|
161
|
+
return _calculate(expression)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def create_agent() -> Agent:
|
|
165
|
+
system_prompt = get_agent_prompt()
|
|
166
|
+
model = os.getenv("MODEL", "gpt-4o-mini")
|
|
167
|
+
return Agent(
|
|
168
|
+
name="{project_name}",
|
|
169
|
+
instructions=system_prompt,
|
|
170
|
+
model=model,
|
|
171
|
+
tools=[calculate],
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def run_agent(user_input: str) -> str:
|
|
176
|
+
agent = create_agent()
|
|
177
|
+
result = Runner.run_sync(agent, user_input)
|
|
178
|
+
return result.final_output
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
OPENAI_AGENTS_TEST_CONTENT = """\
|
|
182
|
+
from unittest.mock import patch, MagicMock
|
|
183
|
+
|
|
184
|
+
from src.tools.calculator import calculate
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_calculator_addition():
|
|
188
|
+
assert calculate("2 + 3") == "5"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_calculator_multiplication():
|
|
192
|
+
assert calculate("6 * 7") == "42"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_calculator_division():
|
|
196
|
+
result = calculate("10 / 4")
|
|
197
|
+
assert "2.5" in result
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_calculator_invalid_expression():
|
|
201
|
+
result = calculate("import os")
|
|
202
|
+
assert "Invalid" in result or "Error" in result
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_run_agent_mock():
|
|
206
|
+
mock_result = MagicMock()
|
|
207
|
+
mock_result.final_output = "The answer is 42"
|
|
208
|
+
|
|
209
|
+
with patch("src.agent.run.Runner") as MockRunner:
|
|
210
|
+
MockRunner.run_sync.return_value = mock_result
|
|
211
|
+
|
|
212
|
+
from src.agent.run import run_agent
|
|
213
|
+
|
|
214
|
+
result = run_agent("What is 6 * 7?")
|
|
215
|
+
|
|
216
|
+
assert isinstance(result, str)
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
# ─── Env examples ─────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
ENV_PYDANTIC_OPENAI = """\
|
|
222
|
+
APP_NAME={project_name}
|
|
223
|
+
OPENAI_API_KEY=
|
|
224
|
+
MODEL=openai:gpt-4o-mini
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
ENV_PYDANTIC_ANTHROPIC = """\
|
|
228
|
+
APP_NAME={project_name}
|
|
229
|
+
ANTHROPIC_API_KEY=
|
|
230
|
+
MODEL=anthropic:claude-3-5-haiku-latest
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
ENV_PYDANTIC_GEMINI = """\
|
|
234
|
+
APP_NAME={project_name}
|
|
235
|
+
GOOGLE_API_KEY=
|
|
236
|
+
MODEL=google:gemini-2.0-flash
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
ENV_PYDANTIC_OPENROUTER = """\
|
|
240
|
+
APP_NAME={project_name}
|
|
241
|
+
OPENROUTER_API_KEY=
|
|
242
|
+
MODEL=openai/gpt-4o-mini
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
ENV_PYDANTIC_OLLAMA = """\
|
|
246
|
+
APP_NAME={project_name}
|
|
247
|
+
OLLAMA_BASE_URL=http://localhost:11434
|
|
248
|
+
MODEL=llama3.2
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
ENV_PYDANTIC_GROQ = """\
|
|
252
|
+
APP_NAME={project_name}
|
|
253
|
+
GROQ_API_KEY=
|
|
254
|
+
MODEL=groq:llama-3.1-8b-instant
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
ENV_OPENAI_AGENTS_OPENAI = """\
|
|
258
|
+
APP_NAME={project_name}
|
|
259
|
+
OPENAI_API_KEY=
|
|
260
|
+
MODEL=gpt-4o-mini
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
ENV_OPENAI_AGENTS_OPENROUTER = """\
|
|
264
|
+
APP_NAME={project_name}
|
|
265
|
+
OPENAI_API_KEY=
|
|
266
|
+
OPENAI_BASE_URL=https://openrouter.ai/api/v1
|
|
267
|
+
MODEL=openai/gpt-4o-mini
|
|
268
|
+
OPENAI_AGENTS_DISABLE_TRACING=1
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
# ─── README ───────────────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def make_readme(framework: str, provider: str) -> str:
|
|
275
|
+
provider_key_map = {
|
|
276
|
+
"openai": "OPENAI_API_KEY=your-key",
|
|
277
|
+
"anthropic": "ANTHROPIC_API_KEY=your-key",
|
|
278
|
+
"gemini": "GOOGLE_API_KEY=your-key",
|
|
279
|
+
"openrouter": "OPENROUTER_API_KEY=your-key",
|
|
280
|
+
"ollama": "OLLAMA_BASE_URL=http://localhost:11434",
|
|
281
|
+
"groq": "GROQ_API_KEY=your-key",
|
|
282
|
+
}
|
|
283
|
+
key_line = provider_key_map.get(provider, "API_KEY=your-key")
|
|
284
|
+
return (
|
|
285
|
+
"# {project_name}\n\n"
|
|
286
|
+
f"An AI agent generated with Spawn, using {framework.title()} + {provider.title()}.\n\n"
|
|
287
|
+
"## Getting Started\n\n"
|
|
288
|
+
"1. Rename `.env.example` to `.env` and fill in your API key:\n\n"
|
|
289
|
+
"```env\n"
|
|
290
|
+
f"{key_line}\n"
|
|
291
|
+
"```\n\n"
|
|
292
|
+
"2. Run the agent:\n\n"
|
|
293
|
+
"```bash\n"
|
|
294
|
+
"uv run python -m src.main\n"
|
|
295
|
+
"```\n\n"
|
|
296
|
+
"## Example\n\n"
|
|
297
|
+
"```\n"
|
|
298
|
+
"You: What is 145 * 28?\n"
|
|
299
|
+
"Thinking...\n"
|
|
300
|
+
"Agent: 4060\n"
|
|
301
|
+
"```\n\n"
|
|
302
|
+
"## Project Structure\n\n"
|
|
303
|
+
"```\n"
|
|
304
|
+
"{project_name}/\n"
|
|
305
|
+
"├── src/\n"
|
|
306
|
+
"│ ├── agent/ # Agent definition and runner\n"
|
|
307
|
+
"│ ├── tools/ # Tool implementations\n"
|
|
308
|
+
"│ ├── prompts/ # agent_prompt.txt\n"
|
|
309
|
+
"│ ├── config/ # Settings and env loading\n"
|
|
310
|
+
"│ └── main.py\n"
|
|
311
|
+
"├── tests/\n"
|
|
312
|
+
"├── .env.example\n"
|
|
313
|
+
"└── README.md\n"
|
|
314
|
+
"```\n\n"
|
|
315
|
+
"## Running Tests\n\n"
|
|
316
|
+
"```bash\n"
|
|
317
|
+
"uv run pytest\n"
|
|
318
|
+
"```\n"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def make_agents_md(framework: str, provider: str) -> str:
|
|
323
|
+
if framework == "openai-agents" and provider == "openrouter":
|
|
324
|
+
key_name = "OPENAI_API_KEY` and `OPENAI_BASE_URL"
|
|
325
|
+
else:
|
|
326
|
+
provider_key_map = {
|
|
327
|
+
"openai": "OPENAI_API_KEY",
|
|
328
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
329
|
+
"gemini": "GOOGLE_API_KEY",
|
|
330
|
+
"openrouter": "OPENROUTER_API_KEY",
|
|
331
|
+
"ollama": "OLLAMA_BASE_URL",
|
|
332
|
+
"groq": "GROQ_API_KEY",
|
|
333
|
+
}
|
|
334
|
+
key_name = provider_key_map.get(provider, "OPENAI_API_KEY")
|
|
335
|
+
return (
|
|
336
|
+
"# {project_name}\n\n"
|
|
337
|
+
f"AI agent using {framework.title()} with {provider.title()} provider.\n\n"
|
|
338
|
+
"## Required Environment Variable\n\n"
|
|
339
|
+
f"Set `{key_name}` in your `.env` file.\n"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# ─── GitHub Actions ───────────────────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
GITHUB_ACTIONS_CI_BASE = """\
|
|
345
|
+
name: CI
|
|
346
|
+
|
|
347
|
+
on:
|
|
348
|
+
push:
|
|
349
|
+
branches: [main]
|
|
350
|
+
pull_request:
|
|
351
|
+
branches: [main]
|
|
352
|
+
|
|
353
|
+
jobs:
|
|
354
|
+
ci:
|
|
355
|
+
runs-on: ubuntu-latest
|
|
356
|
+
|
|
357
|
+
steps:
|
|
358
|
+
- uses: actions/checkout@v4
|
|
359
|
+
|
|
360
|
+
- uses: astral-sh/setup-uv@v5
|
|
361
|
+
|
|
362
|
+
- name: Install dependencies
|
|
363
|
+
run: uv sync
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
GITHUB_ACTIONS_CI_RUFF_STEP = """\
|
|
367
|
+
- name: Lint
|
|
368
|
+
run: uv run ruff check .
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
GITHUB_ACTIONS_CI_PYTEST_STEP = """\
|
|
372
|
+
- name: Test
|
|
373
|
+
run: uv run pytest
|
|
374
|
+
"""
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from spawn.templates.base import BaseTemplate
|
|
4
|
+
from spawn.templates.automation.content import (
|
|
5
|
+
INIT_CONTENT,
|
|
6
|
+
MAIN_CONTENT,
|
|
7
|
+
LOGGER_CONTENT,
|
|
8
|
+
REPORT_WORKFLOW_CONTENT,
|
|
9
|
+
DATA_TASK_CONTENT,
|
|
10
|
+
REPORT_TASK_CONTENT,
|
|
11
|
+
TEST_CONTENT,
|
|
12
|
+
ENV_EXAMPLE_CONTENT,
|
|
13
|
+
README_CONTENT,
|
|
14
|
+
GITHUB_ACTIONS_CI_BASE,
|
|
15
|
+
GITHUB_ACTIONS_CI_RUFF_STEP,
|
|
16
|
+
GITHUB_ACTIONS_CI_PYTEST_STEP,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
AUTOMATION_FOLDERS = [
|
|
20
|
+
"src/workflows",
|
|
21
|
+
"src/tasks",
|
|
22
|
+
"src/integrations",
|
|
23
|
+
"src/utils",
|
|
24
|
+
"logs",
|
|
25
|
+
"tests",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
AUTOMATION_FILES = [
|
|
29
|
+
("src/__init__.py", INIT_CONTENT),
|
|
30
|
+
("src/workflows/__init__.py", INIT_CONTENT),
|
|
31
|
+
("src/workflows/report_workflow.py", REPORT_WORKFLOW_CONTENT),
|
|
32
|
+
("src/tasks/__init__.py", INIT_CONTENT),
|
|
33
|
+
("src/tasks/data_task.py", DATA_TASK_CONTENT),
|
|
34
|
+
("src/tasks/report_task.py", REPORT_TASK_CONTENT),
|
|
35
|
+
("src/integrations/__init__.py", INIT_CONTENT),
|
|
36
|
+
("src/utils/__init__.py", INIT_CONTENT),
|
|
37
|
+
("src/utils/logger.py", LOGGER_CONTENT),
|
|
38
|
+
("src/main.py", MAIN_CONTENT),
|
|
39
|
+
("logs/.gitkeep", INIT_CONTENT),
|
|
40
|
+
("tests/__init__.py", INIT_CONTENT),
|
|
41
|
+
("tests/test_automation.py", TEST_CONTENT),
|
|
42
|
+
(".env.example", ENV_EXAMPLE_CONTENT),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AutomationTemplate(BaseTemplate):
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
extras: list[str] | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
self.extras = extras or []
|
|
52
|
+
super().__init__(
|
|
53
|
+
name="Automation Tool",
|
|
54
|
+
folders=AUTOMATION_FOLDERS,
|
|
55
|
+
starter_files=AUTOMATION_FILES,
|
|
56
|
+
next_steps=[
|
|
57
|
+
"cd {project_name}",
|
|
58
|
+
"uv run python -m src.main",
|
|
59
|
+
],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def get_readme_content(self, context: dict) -> str | None:
|
|
63
|
+
return README_CONTENT.format_map(context)
|
|
64
|
+
|
|
65
|
+
def get_dependencies(self) -> list[str]:
|
|
66
|
+
base = ["requests", "python-dotenv"]
|
|
67
|
+
if "pytest" in self.extras:
|
|
68
|
+
base += ["pytest"]
|
|
69
|
+
if "ruff" in self.extras:
|
|
70
|
+
base += ["ruff"]
|
|
71
|
+
return base
|
|
72
|
+
|
|
73
|
+
def post_install(self, project_path: Path) -> None:
|
|
74
|
+
pyproject = project_path / "pyproject.toml"
|
|
75
|
+
current = pyproject.read_text(encoding="utf-8")
|
|
76
|
+
additions = ""
|
|
77
|
+
|
|
78
|
+
if "pytest" in self.extras:
|
|
79
|
+
additions += (
|
|
80
|
+
"\n[tool.pytest.ini_options]\n"
|
|
81
|
+
'testpaths = ["tests"]\n'
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if "ruff" in self.extras:
|
|
85
|
+
additions += "\n[tool.ruff]\nline-length = 88\n"
|
|
86
|
+
|
|
87
|
+
if additions:
|
|
88
|
+
pyproject.write_text(current + additions, encoding="utf-8")
|
|
89
|
+
|
|
90
|
+
if "github-actions" in self.extras:
|
|
91
|
+
workflows_path = project_path / ".github" / "workflows"
|
|
92
|
+
workflows_path.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
ci_content = GITHUB_ACTIONS_CI_BASE
|
|
94
|
+
if "ruff" in self.extras:
|
|
95
|
+
ci_content += GITHUB_ACTIONS_CI_RUFF_STEP
|
|
96
|
+
if "pytest" in self.extras:
|
|
97
|
+
ci_content += GITHUB_ACTIONS_CI_PYTEST_STEP
|
|
98
|
+
(workflows_path / "ci.yml").write_text(
|
|
99
|
+
ci_content, encoding="utf-8"
|
|
100
|
+
)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
INIT_CONTENT = ""
|
|
2
|
+
|
|
3
|
+
MAIN_CONTENT = """\
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from src.utils.logger import setup_logger
|
|
7
|
+
from src.workflows.report_workflow import run_report_workflow
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main() -> None:
|
|
11
|
+
setup_logger()
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
logger.info("Starting {project_name}")
|
|
14
|
+
run_report_workflow()
|
|
15
|
+
logger.info("{project_name} finished")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
main()
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
LOGGER_CONTENT = """\
|
|
23
|
+
import logging
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def setup_logger(log_file: str = "logs/app.log") -> None:
|
|
28
|
+
Path("logs").mkdir(exist_ok=True)
|
|
29
|
+
logging.basicConfig(
|
|
30
|
+
level=logging.INFO,
|
|
31
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
32
|
+
handlers=[
|
|
33
|
+
logging.FileHandler(log_file),
|
|
34
|
+
logging.StreamHandler(),
|
|
35
|
+
],
|
|
36
|
+
)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
REPORT_WORKFLOW_CONTENT = """\
|
|
40
|
+
import logging
|
|
41
|
+
|
|
42
|
+
from src.tasks.data_task import load_sample_data
|
|
43
|
+
from src.tasks.report_task import generate_report
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def run_report_workflow() -> None:
|
|
49
|
+
logger.info("Workflow started")
|
|
50
|
+
data = load_sample_data()
|
|
51
|
+
report = generate_report(data)
|
|
52
|
+
logger.info("Report: %s", report)
|
|
53
|
+
logger.info("Workflow complete")
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
DATA_TASK_CONTENT = """\
|
|
57
|
+
import logging
|
|
58
|
+
|
|
59
|
+
logger = logging.getLogger(__name__)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def load_sample_data() -> dict:
|
|
63
|
+
logger.info("Loading sample data")
|
|
64
|
+
return {{"records": 42, "source": "local"}}
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
REPORT_TASK_CONTENT = """\
|
|
68
|
+
import logging
|
|
69
|
+
|
|
70
|
+
logger = logging.getLogger(__name__)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def generate_report(data: dict) -> str:
|
|
74
|
+
logger.info("Generating report")
|
|
75
|
+
return f"Records processed: {{data['records']}}"
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
TEST_CONTENT = """\
|
|
79
|
+
from src.tasks.data_task import load_sample_data
|
|
80
|
+
from src.tasks.report_task import generate_report
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_load_sample_data():
|
|
84
|
+
data = load_sample_data()
|
|
85
|
+
assert isinstance(data, dict)
|
|
86
|
+
assert "records" in data
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_generate_report():
|
|
90
|
+
data = {{"records": 10, "source": "test"}}
|
|
91
|
+
report = generate_report(data)
|
|
92
|
+
assert "10" in report
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
ENV_EXAMPLE_CONTENT = """\
|
|
96
|
+
APP_NAME={project_name}
|
|
97
|
+
DEBUG=True
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
README_CONTENT = """\
|
|
101
|
+
# {project_name}
|
|
102
|
+
|
|
103
|
+
An automation project generated with Spawn.
|
|
104
|
+
|
|
105
|
+
## Getting Started
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
uv run python -m src.main
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Project Structure
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
{project_name}/
|
|
115
|
+
├── src/
|
|
116
|
+
│ ├── workflows/ # Business workflows
|
|
117
|
+
│ ├── tasks/ # Reusable task modules
|
|
118
|
+
│ ├── integrations/ # External service connectors
|
|
119
|
+
│ ├── utils/ # Shared helpers
|
|
120
|
+
│ └── main.py # Entry point
|
|
121
|
+
├── logs/ # Application logs
|
|
122
|
+
├── tests/
|
|
123
|
+
├── .env.example
|
|
124
|
+
└── README.md
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Running Tests
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
uv run pytest
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Logs
|
|
134
|
+
|
|
135
|
+
Logs are written to `logs/app.log` and printed to the console.
|
|
136
|
+
|
|
137
|
+
## Workflow Overview
|
|
138
|
+
|
|
139
|
+
Load sample data → Generate report → Log output → Workflow complete
|
|
140
|
+
|
|
141
|
+
## Future Expansions
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
spawn add scheduler
|
|
145
|
+
spawn add openai
|
|
146
|
+
spawn add slack
|
|
147
|
+
spawn add database
|
|
148
|
+
```
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
GITHUB_ACTIONS_CI_BASE = """\
|
|
152
|
+
name: CI
|
|
153
|
+
|
|
154
|
+
on:
|
|
155
|
+
push:
|
|
156
|
+
branches: [main]
|
|
157
|
+
pull_request:
|
|
158
|
+
branches: [main]
|
|
159
|
+
|
|
160
|
+
jobs:
|
|
161
|
+
ci:
|
|
162
|
+
runs-on: ubuntu-latest
|
|
163
|
+
|
|
164
|
+
steps:
|
|
165
|
+
- uses: actions/checkout@v4
|
|
166
|
+
|
|
167
|
+
- uses: astral-sh/setup-uv@v5
|
|
168
|
+
|
|
169
|
+
- name: Install dependencies
|
|
170
|
+
run: uv sync
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
GITHUB_ACTIONS_CI_RUFF_STEP = """\
|
|
174
|
+
- name: Lint
|
|
175
|
+
run: uv run ruff check .
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
GITHUB_ACTIONS_CI_PYTEST_STEP = """\
|
|
179
|
+
- name: Test
|
|
180
|
+
run: uv run pytest
|
|
181
|
+
"""
|