lambda-forge-cli-aws 1.0.0__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.
- lambda_forge_cli_aws-1.0.0.dist-info/METADATA +352 -0
- lambda_forge_cli_aws-1.0.0.dist-info/RECORD +80 -0
- lambda_forge_cli_aws-1.0.0.dist-info/WHEEL +4 -0
- lambda_forge_cli_aws-1.0.0.dist-info/entry_points.txt +2 -0
- lambda_forge_cli_aws-1.0.0.dist-info/licenses/LICENSE +189 -0
- lambdaforge/__init__.py +10 -0
- lambdaforge/bedrock_client.py +109 -0
- lambdaforge/cli.py +428 -0
- lambdaforge/deploy.py +308 -0
- lambdaforge/logs.py +185 -0
- lambdaforge/renderer.py +72 -0
- lambdaforge/spec_generator.py +171 -0
- lambdaforge/status.py +111 -0
- lambdaforge/templates.py +106 -0
- lambdaforge/upgrade.py +231 -0
- templates/python-api/cookiecutter.json +12 -0
- templates/python-api/{{cookiecutter.project_slug}}/.github/workflows/dependabot.yml +22 -0
- templates/python-api/{{cookiecutter.project_slug}}/.github/workflows/deploy.yml +90 -0
- templates/python-api/{{cookiecutter.project_slug}}/.gitignore +65 -0
- templates/python-api/{{cookiecutter.project_slug}}/Dockerfile +11 -0
- templates/python-api/{{cookiecutter.project_slug}}/README.md +130 -0
- templates/python-api/{{cookiecutter.project_slug}}/infrastructure/app.js +26 -0
- templates/python-api/{{cookiecutter.project_slug}}/infrastructure/cdk.json +20 -0
- templates/python-api/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +155 -0
- templates/python-api/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
- templates/python-api/{{cookiecutter.project_slug}}/pyproject.toml +66 -0
- templates/python-api/{{cookiecutter.project_slug}}/requirements-dev.txt +8 -0
- templates/python-api/{{cookiecutter.project_slug}}/requirements.txt +3 -0
- templates/python-api/{{cookiecutter.project_slug}}/src/handler.py +93 -0
- templates/python-api/{{cookiecutter.project_slug}}/src/utils/logger.py +39 -0
- templates/python-api/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
- templates/python-api/{{cookiecutter.project_slug}}/tests/test_handler.py +68 -0
- templates/python-cron/cookiecutter.json +13 -0
- templates/python-cron/{{cookiecutter.project_slug}}/.gitignore +65 -0
- templates/python-cron/{{cookiecutter.project_slug}}/Dockerfile +11 -0
- templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
- templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
- templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +55 -0
- templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
- templates/python-cron/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
- templates/python-cron/{{cookiecutter.project_slug}}/requirements.txt +2 -0
- templates/python-cron/{{cookiecutter.project_slug}}/src/handler.py +53 -0
- templates/python-cron/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
- templates/python-cron/{{cookiecutter.project_slug}}/tests/test_handler.py +48 -0
- templates/python-queue/cookiecutter.json +12 -0
- templates/python-queue/{{cookiecutter.project_slug}}/.gitignore +65 -0
- templates/python-queue/{{cookiecutter.project_slug}}/Dockerfile +11 -0
- templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
- templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
- templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +79 -0
- templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
- templates/python-queue/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
- templates/python-queue/{{cookiecutter.project_slug}}/requirements.txt +2 -0
- templates/python-queue/{{cookiecutter.project_slug}}/src/handler.py +64 -0
- templates/python-queue/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
- templates/python-queue/{{cookiecutter.project_slug}}/tests/test_handler.py +60 -0
- templates/python-s3/cookiecutter.json +13 -0
- templates/python-s3/{{cookiecutter.project_slug}}/.gitignore +65 -0
- templates/python-s3/{{cookiecutter.project_slug}}/Dockerfile +11 -0
- templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
- templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
- templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +89 -0
- templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
- templates/python-s3/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
- templates/python-s3/{{cookiecutter.project_slug}}/requirements.txt +2 -0
- templates/python-s3/{{cookiecutter.project_slug}}/src/handler.py +86 -0
- templates/python-s3/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
- templates/python-s3/{{cookiecutter.project_slug}}/tests/test_handler.py +71 -0
- templates/python-stream/cookiecutter.json +12 -0
- templates/python-stream/{{cookiecutter.project_slug}}/.gitignore +65 -0
- templates/python-stream/{{cookiecutter.project_slug}}/Dockerfile +11 -0
- templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
- templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
- templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +81 -0
- templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
- templates/python-stream/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
- templates/python-stream/{{cookiecutter.project_slug}}/requirements.txt +2 -0
- templates/python-stream/{{cookiecutter.project_slug}}/src/handler.py +69 -0
- templates/python-stream/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
- templates/python-stream/{{cookiecutter.project_slug}}/tests/test_handler.py +64 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Spec generator — creates .kiro/specs/ files using Amazon Bedrock."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from lambdaforge.bedrock_client import BedrockClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SpecGenerator:
|
|
12
|
+
"""Generates Kiro-compatible spec files for Lambda projects.
|
|
13
|
+
|
|
14
|
+
Uses Amazon Bedrock with Claude Sonnet 4.5 to generate
|
|
15
|
+
context-aware specs based on the template configuration.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
SYSTEM_PROMPT = """Eres un AWS Solutions Architect documentando una Lambda recién creada.
|
|
19
|
+
|
|
20
|
+
Recibes la configuración de la Lambda (runtime, triggers, databases, auth).
|
|
21
|
+
Tu trabajo es generar 4 archivos markdown en español que Kiro pueda usar como specs.
|
|
22
|
+
|
|
23
|
+
Output esperado (4 secciones separadas por '---SECTION---'):
|
|
24
|
+
|
|
25
|
+
SECTION 1: overview.md
|
|
26
|
+
- Título H1 con nombre del proyecto
|
|
27
|
+
- 1 párrafo describiendo QUÉ hace
|
|
28
|
+
- 1 párrafo describiendo POR QUÉ existe
|
|
29
|
+
- Lista de "Acceptance Criteria" iniciales
|
|
30
|
+
|
|
31
|
+
SECTION 2: api-design.md
|
|
32
|
+
- Solo si trigger es api-gateway
|
|
33
|
+
- Endpoints REST con method, path, description
|
|
34
|
+
- Request schema (JSON example)
|
|
35
|
+
- Response schema (JSON example)
|
|
36
|
+
- Status codes
|
|
37
|
+
|
|
38
|
+
SECTION 3: data-model.md
|
|
39
|
+
- Solo si database es dynamodb
|
|
40
|
+
- Tabla principal con partition key, sort key
|
|
41
|
+
- GSI/LSI si aplica
|
|
42
|
+
- Access patterns (cómo se consulta)
|
|
43
|
+
|
|
44
|
+
SECTION 4: acceptance-criteria.md
|
|
45
|
+
- 5-10 criterios testeable
|
|
46
|
+
- Formato: - [ ] descripción binaria (pass/fail)
|
|
47
|
+
- Cubren: happy path, errores, validaciones, performance
|
|
48
|
+
|
|
49
|
+
Reglas críticas:
|
|
50
|
+
- Escribe TODO en español
|
|
51
|
+
- Sé específico y accionable
|
|
52
|
+
- NO inventes endpoints que no estén en el template
|
|
53
|
+
- Acceptance criteria deben ser BINARIOS
|
|
54
|
+
- Usa formato markdown limpio (sin HTML)
|
|
55
|
+
- NO incluyas '```markdown' ni otros code fences (esos se añaden al escribir)"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
region: str = "us-east-1",
|
|
60
|
+
budget_usd: float = 0.50,
|
|
61
|
+
model_id: str | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
self.client = BedrockClient(region=region, model_id=model_id or BedrockClient.DEFAULT_MODEL)
|
|
64
|
+
self.budget_usd = budget_usd
|
|
65
|
+
self.total_cost = 0.0
|
|
66
|
+
|
|
67
|
+
def generate_all(
|
|
68
|
+
self,
|
|
69
|
+
project_path: Path,
|
|
70
|
+
template_name: str,
|
|
71
|
+
context: dict,
|
|
72
|
+
) -> dict[str, str]:
|
|
73
|
+
"""Generate all spec files for a Lambda project.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
project_path: Path to the generated project
|
|
77
|
+
template_name: Template used
|
|
78
|
+
context: Template context variables
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Dict of {filename: content}
|
|
82
|
+
"""
|
|
83
|
+
specs_dir = project_path / ".kiro" / "specs"
|
|
84
|
+
specs_dir.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
|
|
86
|
+
# Build user message
|
|
87
|
+
user_message = f"""Genera las specs para esta Lambda:
|
|
88
|
+
|
|
89
|
+
Nombre: {context.get("project_name")}
|
|
90
|
+
Template: {template_name}
|
|
91
|
+
Runtime: {context.get("runtime", "python3.12")}
|
|
92
|
+
Trigger: {context.get("trigger", "api-gateway")}
|
|
93
|
+
Database: {context.get("database", "none")}
|
|
94
|
+
Auth: {context.get("auth", "none")}
|
|
95
|
+
Observability: {context.get("observability", "xray")}
|
|
96
|
+
Region: {context.get("aws_region", "us-east-1")}
|
|
97
|
+
Memory: {context.get("memory_mb", 512)}MB
|
|
98
|
+
Timeout: {context.get("timeout_seconds", 30)}s
|
|
99
|
+
|
|
100
|
+
Genera las 4 secciones separadas por '---SECTION---'."""
|
|
101
|
+
|
|
102
|
+
# Check budget
|
|
103
|
+
if self.total_cost >= self.budget_usd:
|
|
104
|
+
return {"error": "Budget exhausted, skipping specs generation"}
|
|
105
|
+
|
|
106
|
+
# Call Bedrock
|
|
107
|
+
try:
|
|
108
|
+
response = self.client.generate(
|
|
109
|
+
system_prompt=self.SYSTEM_PROMPT,
|
|
110
|
+
user_message=user_message,
|
|
111
|
+
max_tokens=3000,
|
|
112
|
+
temperature=0.4,
|
|
113
|
+
)
|
|
114
|
+
# Rough cost estimate
|
|
115
|
+
self.total_cost += 0.05 # ~5 cents per call typical
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
return {"error": str(e)}
|
|
119
|
+
|
|
120
|
+
# Parse sections
|
|
121
|
+
sections = self._parse_sections(response)
|
|
122
|
+
|
|
123
|
+
# Write files
|
|
124
|
+
written: dict[str, str] = {}
|
|
125
|
+
for filename, content in sections.items():
|
|
126
|
+
if content and self._is_relevant(filename, context):
|
|
127
|
+
filepath = specs_dir / filename
|
|
128
|
+
filepath.write_text(content, encoding="utf-8")
|
|
129
|
+
written[filename] = content
|
|
130
|
+
|
|
131
|
+
return written
|
|
132
|
+
|
|
133
|
+
def _parse_sections(self, response: str) -> dict[str, str]:
|
|
134
|
+
"""Parse the response into sections."""
|
|
135
|
+
sections = {
|
|
136
|
+
"overview.md": "",
|
|
137
|
+
"api-design.md": "",
|
|
138
|
+
"data-model.md": "",
|
|
139
|
+
"acceptance-criteria.md": "",
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Split by SECTION marker (supports: ---SECTION---, ---SECTION 1:, etc.)
|
|
143
|
+
parts = re.split(r"---\s*SECTION\s*\d*\s*[:.]?\s*---?\s*", response)
|
|
144
|
+
|
|
145
|
+
if len(parts) >= 5: # 4 sections + preamble
|
|
146
|
+
sections["overview.md"] = parts[1].strip()
|
|
147
|
+
sections["api-design.md"] = parts[2].strip()
|
|
148
|
+
sections["data-model.md"] = parts[3].strip()
|
|
149
|
+
sections["acceptance-criteria.md"] = parts[4].strip()
|
|
150
|
+
elif len(parts) >= 4:
|
|
151
|
+
sections["overview.md"] = parts[1].strip()
|
|
152
|
+
sections["api-design.md"] = parts[2].strip()
|
|
153
|
+
sections["data-model.md"] = parts[3].strip()
|
|
154
|
+
elif len(parts) >= 3:
|
|
155
|
+
sections["overview.md"] = parts[1].strip()
|
|
156
|
+
sections["api-design.md"] = parts[2].strip()
|
|
157
|
+
elif len(parts) >= 2:
|
|
158
|
+
sections["overview.md"] = parts[1].strip()
|
|
159
|
+
else:
|
|
160
|
+
# No markers found — treat entire response as overview
|
|
161
|
+
sections["overview.md"] = response.strip()
|
|
162
|
+
|
|
163
|
+
return sections
|
|
164
|
+
|
|
165
|
+
def _is_relevant(self, filename: str, context: dict) -> bool:
|
|
166
|
+
"""Check if a spec file is relevant to the configuration."""
|
|
167
|
+
if filename == "api-design.md":
|
|
168
|
+
return context.get("trigger") == "api-gateway"
|
|
169
|
+
if filename == "data-model.md":
|
|
170
|
+
return context.get("database") == "dynamodb"
|
|
171
|
+
return True
|
lambdaforge/status.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Status command — shows the current deployment state of a Lambda project."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def show_status(project_path: Path, env: str = "dev") -> None:
|
|
17
|
+
"""Show the deployment status of a Lambda project.
|
|
18
|
+
|
|
19
|
+
Queries CloudFormation for the stack status and outputs.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
project_path: Path to the Lambda project
|
|
23
|
+
env: Target environment
|
|
24
|
+
"""
|
|
25
|
+
project_name = project_path.name.replace("_", "-")
|
|
26
|
+
stack_name = f"{project_name}-{env}"
|
|
27
|
+
|
|
28
|
+
stack_info = _get_stack_info(stack_name)
|
|
29
|
+
|
|
30
|
+
if stack_info is None:
|
|
31
|
+
console.print(
|
|
32
|
+
Panel(
|
|
33
|
+
f"[yellow]Stack '{stack_name}' not found.[/yellow]\n\n"
|
|
34
|
+
f"Not deployed to '{env}' yet.\n"
|
|
35
|
+
f"Run: [bold]lambdaforge deploy --env {env}[/bold]",
|
|
36
|
+
title=f"[bold]{project_name}[/bold]",
|
|
37
|
+
border_style="yellow",
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
status = stack_info.get("StackStatus", "UNKNOWN")
|
|
43
|
+
status_color = _status_color(status)
|
|
44
|
+
|
|
45
|
+
outputs = stack_info.get("Outputs", [])
|
|
46
|
+
outputs_dict = {o["OutputKey"]: o["OutputValue"] for o in outputs}
|
|
47
|
+
|
|
48
|
+
lines = [
|
|
49
|
+
f"[bold]Stack:[/bold] {stack_name}",
|
|
50
|
+
f"[bold]Status:[/bold] [{status_color}]{status}[/{status_color}]",
|
|
51
|
+
f"[bold]Environment:[/bold] {env}",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
if "Description" in stack_info:
|
|
55
|
+
lines.append(f"[bold]Description:[/bold] {stack_info['Description']}")
|
|
56
|
+
|
|
57
|
+
updated = stack_info.get("LastUpdatedTime", stack_info.get("CreationTime", ""))
|
|
58
|
+
if updated:
|
|
59
|
+
lines.append(f"[bold]Last deployed:[/bold] {updated}")
|
|
60
|
+
|
|
61
|
+
if outputs_dict:
|
|
62
|
+
lines.append("")
|
|
63
|
+
lines.append("[bold]Outputs:[/bold]")
|
|
64
|
+
for key, value in outputs_dict.items():
|
|
65
|
+
lines.append(f" {key}: [cyan]{value}[/cyan]")
|
|
66
|
+
|
|
67
|
+
tags = stack_info.get("Tags", [])
|
|
68
|
+
if tags:
|
|
69
|
+
lines.append("")
|
|
70
|
+
lines.append("[bold]Tags:[/bold]")
|
|
71
|
+
for tag in tags:
|
|
72
|
+
lines.append(f" {tag['Key']}: {tag['Value']}")
|
|
73
|
+
|
|
74
|
+
console.print(
|
|
75
|
+
Panel("\n".join(lines), title=f"[bold]{project_name}[/bold]", border_style=status_color)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _get_stack_info(stack_name: str) -> dict[str, Any] | None:
|
|
80
|
+
"""Query CloudFormation for stack information."""
|
|
81
|
+
try:
|
|
82
|
+
result = subprocess.run(
|
|
83
|
+
[
|
|
84
|
+
"aws",
|
|
85
|
+
"cloudformation",
|
|
86
|
+
"describe-stacks",
|
|
87
|
+
"--stack-name",
|
|
88
|
+
stack_name,
|
|
89
|
+
"--output",
|
|
90
|
+
"json",
|
|
91
|
+
],
|
|
92
|
+
capture_output=True,
|
|
93
|
+
text=True,
|
|
94
|
+
check=True,
|
|
95
|
+
)
|
|
96
|
+
data = json.loads(result.stdout)
|
|
97
|
+
stacks = data.get("Stacks", [])
|
|
98
|
+
return stacks[0] if stacks else None
|
|
99
|
+
except (subprocess.CalledProcessError, json.JSONDecodeError, FileNotFoundError):
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _status_color(status: str) -> str:
|
|
104
|
+
"""Map CloudFormation status to a Rich color."""
|
|
105
|
+
if "COMPLETE" in status and "DELETE" not in status:
|
|
106
|
+
return "green"
|
|
107
|
+
elif "IN_PROGRESS" in status:
|
|
108
|
+
return "yellow"
|
|
109
|
+
elif "FAILED" in status or "DELETE" in status:
|
|
110
|
+
return "red"
|
|
111
|
+
return "dim"
|
lambdaforge/templates.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Template catalog — defines all available Lambda templates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
TEMPLATES: dict[str, dict[str, Any]] = {
|
|
8
|
+
"python-api": {
|
|
9
|
+
"description": "Lambda triggered by API Gateway with Python 3.12",
|
|
10
|
+
"runtime": "python3.12",
|
|
11
|
+
"triggers": ["api-gateway", "alb", "function-url"],
|
|
12
|
+
"features": [
|
|
13
|
+
"cors",
|
|
14
|
+
"auth-cognito",
|
|
15
|
+
"request-validation",
|
|
16
|
+
"error-handler",
|
|
17
|
+
"structured-logging",
|
|
18
|
+
"xray-tracing",
|
|
19
|
+
],
|
|
20
|
+
"dependencies": [
|
|
21
|
+
"pydantic>=2.5.0",
|
|
22
|
+
"aws-lambda-powertools>=2.30.0",
|
|
23
|
+
"aws-xray-sdk>=2.12.0",
|
|
24
|
+
],
|
|
25
|
+
"default_memory": 512,
|
|
26
|
+
"default_timeout": 30,
|
|
27
|
+
},
|
|
28
|
+
"python-cron": {
|
|
29
|
+
"description": "Lambda triggered by EventBridge schedule (cron job)",
|
|
30
|
+
"runtime": "python3.12",
|
|
31
|
+
"triggers": ["eventbridge-schedule"],
|
|
32
|
+
"features": [
|
|
33
|
+
"retry-config",
|
|
34
|
+
"dlq",
|
|
35
|
+
"structured-logging",
|
|
36
|
+
"xray-tracing",
|
|
37
|
+
"timeout-config",
|
|
38
|
+
],
|
|
39
|
+
"dependencies": [
|
|
40
|
+
"aws-lambda-powertools>=2.30.0",
|
|
41
|
+
"boto3>=1.34.0",
|
|
42
|
+
],
|
|
43
|
+
"default_memory": 512,
|
|
44
|
+
"default_timeout": 300,
|
|
45
|
+
},
|
|
46
|
+
"python-queue": {
|
|
47
|
+
"description": "Lambda triggered by SQS queue (with DLQ + batch)",
|
|
48
|
+
"runtime": "python3.12",
|
|
49
|
+
"triggers": ["sqs", "sns"],
|
|
50
|
+
"features": [
|
|
51
|
+
"batch-processing",
|
|
52
|
+
"partial-batch-response",
|
|
53
|
+
"dlq",
|
|
54
|
+
"idempotency",
|
|
55
|
+
"xray-tracing",
|
|
56
|
+
],
|
|
57
|
+
"dependencies": [
|
|
58
|
+
"aws-lambda-powertools>=2.30.0",
|
|
59
|
+
],
|
|
60
|
+
"default_memory": 1024,
|
|
61
|
+
"default_timeout": 60,
|
|
62
|
+
},
|
|
63
|
+
"python-stream": {
|
|
64
|
+
"description": "Lambda triggered by Kinesis or DynamoDB Streams",
|
|
65
|
+
"runtime": "python3.12",
|
|
66
|
+
"triggers": ["kinesis", "dynamodb-streams"],
|
|
67
|
+
"features": [
|
|
68
|
+
"checkpointing",
|
|
69
|
+
"iterator-age-alarm",
|
|
70
|
+
"dlq",
|
|
71
|
+
"xray-tracing",
|
|
72
|
+
],
|
|
73
|
+
"dependencies": [
|
|
74
|
+
"aws-lambda-powertools>=2.30.0",
|
|
75
|
+
],
|
|
76
|
+
"default_memory": 1024,
|
|
77
|
+
"default_timeout": 60,
|
|
78
|
+
},
|
|
79
|
+
"python-s3": {
|
|
80
|
+
"description": "Lambda triggered by S3 events (file processing)",
|
|
81
|
+
"runtime": "python3.12",
|
|
82
|
+
"triggers": ["s3"],
|
|
83
|
+
"features": [
|
|
84
|
+
"event-filtering",
|
|
85
|
+
"large-file-handling",
|
|
86
|
+
"multipart-download",
|
|
87
|
+
"xray-tracing",
|
|
88
|
+
],
|
|
89
|
+
"dependencies": [
|
|
90
|
+
"boto3>=1.34.0",
|
|
91
|
+
"smart-open>=6.4.0",
|
|
92
|
+
],
|
|
93
|
+
"default_memory": 1024,
|
|
94
|
+
"default_timeout": 120,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_template(name: str) -> dict[str, Any] | None:
|
|
100
|
+
"""Get a template by name."""
|
|
101
|
+
return TEMPLATES.get(name)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def list_template_names() -> list[str]:
|
|
105
|
+
"""List all available template names."""
|
|
106
|
+
return list(TEMPLATES.keys())
|
lambdaforge/upgrade.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Upgrade mode — adds LambdaForge best practices to existing Lambdas."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
|
|
12
|
+
from lambdaforge.spec_generator import SpecGenerator
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def upgrade_lambda(project_path: Path) -> None:
|
|
18
|
+
"""Upgrade an existing Lambda with LambdaForge best practices.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
project_path: Path to the Lambda project
|
|
22
|
+
"""
|
|
23
|
+
console.print(
|
|
24
|
+
Panel(
|
|
25
|
+
f"[bold cyan]Upgrading: {project_path}[/bold cyan]",
|
|
26
|
+
title="[bold]🔥 LambdaForge Upgrade[/bold]",
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# 1. Create backup
|
|
31
|
+
backup_dir = project_path / ".lambdaforge-backup" / datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
32
|
+
console.print(f"[dim]Creating backup at {backup_dir}...[/dim]")
|
|
33
|
+
_backup_project(project_path, backup_dir)
|
|
34
|
+
|
|
35
|
+
# 2. Detect what's missing
|
|
36
|
+
missing = _detect_missing(project_path)
|
|
37
|
+
console.print(f"[dim]Detected missing features: {', '.join(missing)}[/dim]")
|
|
38
|
+
|
|
39
|
+
# 3. Add what's missing
|
|
40
|
+
if "xray" in missing:
|
|
41
|
+
console.print("[dim]Adding X-Ray tracing...[/dim]")
|
|
42
|
+
_add_xray(project_path)
|
|
43
|
+
|
|
44
|
+
if "structured-logging" in missing:
|
|
45
|
+
console.print("[dim]Adding structured logging...[/dim]")
|
|
46
|
+
_add_structured_logging(project_path)
|
|
47
|
+
|
|
48
|
+
if "tests" in missing:
|
|
49
|
+
console.print("[dim]Adding basic tests...[/dim]")
|
|
50
|
+
_add_basic_tests(project_path)
|
|
51
|
+
|
|
52
|
+
if ".kiro/specs/" in missing or "specs" in missing:
|
|
53
|
+
console.print("[dim]Generating .kiro/specs/ via Bedrock...[/dim]")
|
|
54
|
+
try:
|
|
55
|
+
spec_gen = SpecGenerator()
|
|
56
|
+
spec_gen.generate_all(
|
|
57
|
+
project_path=project_path,
|
|
58
|
+
template_name="upgraded",
|
|
59
|
+
context={
|
|
60
|
+
"project_name": project_path.name,
|
|
61
|
+
"runtime": "python3.12",
|
|
62
|
+
"trigger": "unknown",
|
|
63
|
+
"database": "none",
|
|
64
|
+
"auth": "none",
|
|
65
|
+
"observability": "xray",
|
|
66
|
+
"aws_region": "us-east-1",
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
console.print("[green]✓ Specs generated[/green]")
|
|
70
|
+
except Exception as e:
|
|
71
|
+
console.print(f"[yellow]⚠ Specs generation failed: {e}[/yellow]")
|
|
72
|
+
|
|
73
|
+
# 4. Add CI/CD if missing
|
|
74
|
+
if "github-actions" in missing:
|
|
75
|
+
console.print("[dim]Adding GitHub Actions...[/dim]")
|
|
76
|
+
_add_github_actions(project_path)
|
|
77
|
+
|
|
78
|
+
console.print(
|
|
79
|
+
Panel(
|
|
80
|
+
f"[bold green]✓ Upgrade complete[/bold green]\n\n"
|
|
81
|
+
f"Backup: {backup_dir}\n"
|
|
82
|
+
f"Review changes with: git diff",
|
|
83
|
+
title="[bold]Done[/bold]",
|
|
84
|
+
border_style="green",
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _backup_project(project_path: Path, backup_dir: Path) -> None:
|
|
90
|
+
"""Create a backup of the project before upgrade."""
|
|
91
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
for item in project_path.iterdir():
|
|
93
|
+
if item.name.startswith(".lambdaforge-backup"):
|
|
94
|
+
continue
|
|
95
|
+
if item.is_file():
|
|
96
|
+
shutil.copy2(item, backup_dir / item.name)
|
|
97
|
+
elif item.is_dir():
|
|
98
|
+
shutil.copytree(item, backup_dir / item.name, dirs_exist_ok=True)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _detect_missing(project_path: Path) -> list[str]:
|
|
102
|
+
"""Detect what features are missing from the project."""
|
|
103
|
+
missing = []
|
|
104
|
+
|
|
105
|
+
# Check X-Ray
|
|
106
|
+
handler_files = list(project_path.glob("**/handler.py")) + list(
|
|
107
|
+
project_path.glob("**/lambda_function.py")
|
|
108
|
+
)
|
|
109
|
+
has_xray = any("@xray_recorder" in f.read_text() for f in handler_files if f.exists())
|
|
110
|
+
if not has_xray:
|
|
111
|
+
missing.append("xray")
|
|
112
|
+
|
|
113
|
+
# Check structured logging
|
|
114
|
+
has_logging = any("logger" in f.name.lower() for f in project_path.rglob("*.py"))
|
|
115
|
+
if not has_logging:
|
|
116
|
+
missing.append("structured-logging")
|
|
117
|
+
|
|
118
|
+
# Check tests
|
|
119
|
+
has_tests = (project_path / "tests").exists() and any(project_path.glob("tests/test_*.py"))
|
|
120
|
+
if not has_tests:
|
|
121
|
+
missing.append("tests")
|
|
122
|
+
|
|
123
|
+
# Check .kiro/specs/
|
|
124
|
+
specs_dir = project_path / ".kiro" / "specs"
|
|
125
|
+
if not specs_dir.exists() or not any(specs_dir.glob("*.md")):
|
|
126
|
+
missing.append(".kiro/specs/")
|
|
127
|
+
|
|
128
|
+
# Check GitHub Actions
|
|
129
|
+
gh_actions = project_path / ".github" / "workflows"
|
|
130
|
+
if not gh_actions.exists() or not any(gh_actions.glob("*.yml")):
|
|
131
|
+
missing.append("github-actions")
|
|
132
|
+
|
|
133
|
+
return missing
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _add_xray(project_path: Path) -> None:
|
|
137
|
+
"""Add X-Ray tracing to handler files."""
|
|
138
|
+
# Add to requirements.txt
|
|
139
|
+
req_file = project_path / "requirements.txt"
|
|
140
|
+
if req_file.exists():
|
|
141
|
+
content = req_file.read_text()
|
|
142
|
+
if "aws-xray-sdk" not in content:
|
|
143
|
+
with req_file.open("a") as f:
|
|
144
|
+
f.write("\n# LambdaForge: X-Ray tracing\naws-xray-sdk>=2.12.0\n")
|
|
145
|
+
|
|
146
|
+
# Add decorator to handler
|
|
147
|
+
for handler in project_path.glob("**/handler.py"):
|
|
148
|
+
if handler.exists():
|
|
149
|
+
content = handler.read_text()
|
|
150
|
+
if "@xray_recorder.capture" not in content:
|
|
151
|
+
# Add import at top
|
|
152
|
+
content = "from aws_xray_sdk.core import xray_recorder\n" + content
|
|
153
|
+
handler.write_text(content)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _add_structured_logging(project_path: Path) -> None:
|
|
157
|
+
"""Add a basic logger module."""
|
|
158
|
+
utils_dir = project_path / "src" / "utils"
|
|
159
|
+
if not utils_dir.exists():
|
|
160
|
+
utils_dir.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
|
|
162
|
+
logger_file = utils_dir / "logger.py"
|
|
163
|
+
if not logger_file.exists():
|
|
164
|
+
logger_file.write_text(
|
|
165
|
+
'"""Structured logger for the Lambda."""\n'
|
|
166
|
+
"import logging\n"
|
|
167
|
+
"import json\n\n\n"
|
|
168
|
+
"logger = logging.getLogger()\n"
|
|
169
|
+
"logger.setLevel(logging.INFO)\n\n\n"
|
|
170
|
+
"def log_event(event_type: str, **kwargs):\n"
|
|
171
|
+
' """Log a structured event."""\n'
|
|
172
|
+
' payload = {"type": event_type, **kwargs}\n'
|
|
173
|
+
" logger.info(json.dumps(payload))\n"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _add_basic_tests(project_path: Path) -> None:
|
|
178
|
+
"""Add a basic test file."""
|
|
179
|
+
tests_dir = project_path / "tests"
|
|
180
|
+
tests_dir.mkdir(exist_ok=True)
|
|
181
|
+
|
|
182
|
+
test_file = tests_dir / "test_handler.py"
|
|
183
|
+
if not test_file.exists():
|
|
184
|
+
test_file.write_text(
|
|
185
|
+
'"""Basic tests for the Lambda handler."""\n'
|
|
186
|
+
"import json\n"
|
|
187
|
+
"from src.handler import main\n\n\n"
|
|
188
|
+
"def test_handler_returns_200():\n"
|
|
189
|
+
' event = {"httpMethod": "GET", "path": "/"}\n'
|
|
190
|
+
" context = {}\n"
|
|
191
|
+
" response = main(event, context)\n"
|
|
192
|
+
' assert response["statusCode"] == 200\n'
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Add pytest config
|
|
196
|
+
pytest_ini = project_path / "pytest.ini"
|
|
197
|
+
if not pytest_ini.exists():
|
|
198
|
+
pytest_ini.write_text(
|
|
199
|
+
"[pytest]\n"
|
|
200
|
+
"testpaths = tests\n"
|
|
201
|
+
"python_files = test_*.py\n"
|
|
202
|
+
"python_classes = Test*\n"
|
|
203
|
+
"python_functions = test_*\n"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _add_github_actions(project_path: Path) -> None:
|
|
208
|
+
"""Add a basic GitHub Actions workflow."""
|
|
209
|
+
workflows_dir = project_path / ".github" / "workflows"
|
|
210
|
+
workflows_dir.mkdir(parents=True, exist_ok=True)
|
|
211
|
+
|
|
212
|
+
workflow_file = workflows_dir / "ci.yml"
|
|
213
|
+
if not workflow_file.exists():
|
|
214
|
+
workflow_file.write_text(
|
|
215
|
+
"name: CI\n\n"
|
|
216
|
+
"on:\n"
|
|
217
|
+
" push:\n"
|
|
218
|
+
" branches: [main]\n"
|
|
219
|
+
" pull_request:\n"
|
|
220
|
+
" branches: [main]\n\n"
|
|
221
|
+
"jobs:\n"
|
|
222
|
+
" test:\n"
|
|
223
|
+
" runs-on: ubuntu-latest\n"
|
|
224
|
+
" steps:\n"
|
|
225
|
+
" - uses: actions/checkout@v4\n"
|
|
226
|
+
" - uses: actions/setup-python@v5\n"
|
|
227
|
+
" with:\n"
|
|
228
|
+
" python-version: '3.12'\n"
|
|
229
|
+
" - run: pip install -r requirements.txt\n"
|
|
230
|
+
" - run: pytest\n"
|
|
231
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project_name": "my-lambda",
|
|
3
|
+
"project_slug": "{{ cookiecutter.project_name|replace('-', '_') }}",
|
|
4
|
+
"module_name": "{{ cookiecutter.project_slug }}",
|
|
5
|
+
"description": "Serverless API built with AWS Lambda",
|
|
6
|
+
"author_name": "LambdaForge User",
|
|
7
|
+
"author_email": "user@example.com",
|
|
8
|
+
"runtime": "python3.12",
|
|
9
|
+
"aws_region": "us-east-1",
|
|
10
|
+
"memory_mb": 512,
|
|
11
|
+
"timeout_seconds": 30
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{% raw %}
|
|
2
|
+
name: Dependabot
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
pull_request:
|
|
6
|
+
push:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
dependabot:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
if: github.actor == 'dependabot[bot]'
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Approve Dependabot PR
|
|
16
|
+
run: |
|
|
17
|
+
gh pr review --approve "$PR_URL"
|
|
18
|
+
gh pr merge --auto --squash "$PR_URL"
|
|
19
|
+
env:
|
|
20
|
+
PR_URL: ${{ github.event.pull_request.html_url }}
|
|
21
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
22
|
+
{% endraw %}
|