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,39 @@
|
|
|
1
|
+
"""Structured logger utilities."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger()
|
|
9
|
+
logger.setLevel(logging.INFO)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def log_event(event_type: str, **kwargs: Any) -> None:
|
|
13
|
+
"""Log a structured event as JSON.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
event_type: A short identifier for the event
|
|
17
|
+
**kwargs: Additional fields to log
|
|
18
|
+
"""
|
|
19
|
+
payload = {"type": event_type, **kwargs}
|
|
20
|
+
logger.info(json.dumps(payload))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def log_error(error: Exception, context: dict[str, Any] | None = None) -> None:
|
|
24
|
+
"""Log an error with full context.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
error: The exception
|
|
28
|
+
context: Optional context dict
|
|
29
|
+
"""
|
|
30
|
+
import traceback
|
|
31
|
+
|
|
32
|
+
payload = {
|
|
33
|
+
"type": "error",
|
|
34
|
+
"error_type": type(error).__name__,
|
|
35
|
+
"error_message": str(error),
|
|
36
|
+
"traceback": traceback.format_exc(),
|
|
37
|
+
"context": context or {},
|
|
38
|
+
}
|
|
39
|
+
logger.error(json.dumps(payload))
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Test configuration and fixtures."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture(autouse=True)
|
|
10
|
+
def aws_credentials(monkeypatch):
|
|
11
|
+
"""Mock AWS credentials for moto."""
|
|
12
|
+
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
|
|
13
|
+
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
|
|
14
|
+
monkeypatch.setenv("AWS_SECURITY_TOKEN", "testing")
|
|
15
|
+
monkeypatch.setenv("AWS_SESSION_TOKEN", "testing")
|
|
16
|
+
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def table_name():
|
|
21
|
+
"""Provide a test table name."""
|
|
22
|
+
return os.environ.get("TABLE_NAME", "test-table")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Tests for {{ cookiecutter.project_name }} Lambda handler."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from unittest.mock import MagicMock
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from src.handler import handler
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def api_gateway_event():
|
|
14
|
+
"""Sample API Gateway GET event."""
|
|
15
|
+
return {
|
|
16
|
+
"httpMethod": "GET",
|
|
17
|
+
"path": "/health",
|
|
18
|
+
"headers": {},
|
|
19
|
+
"multiValueHeaders": {},
|
|
20
|
+
"queryStringParameters": None,
|
|
21
|
+
"pathParameters": None,
|
|
22
|
+
"stageVariables": None,
|
|
23
|
+
"requestContext": {
|
|
24
|
+
"requestId": "test-request-id",
|
|
25
|
+
"stage": "test",
|
|
26
|
+
},
|
|
27
|
+
"body": None,
|
|
28
|
+
"isBase64Encoded": False,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_health_check(api_gateway_event):
|
|
33
|
+
"""Test the health check endpoint returns 200."""
|
|
34
|
+
context = MagicMock()
|
|
35
|
+
context.function_name = "test-function"
|
|
36
|
+
context.aws_request_id = "test-request-id"
|
|
37
|
+
|
|
38
|
+
response = handler(api_gateway_event, context)
|
|
39
|
+
|
|
40
|
+
assert response["statusCode"] == 200
|
|
41
|
+
body = json.loads(response["body"])
|
|
42
|
+
assert body["status"] == "ok"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_list_items_without_table(api_gateway_event):
|
|
46
|
+
"""Test listing items when TABLE_NAME is not set."""
|
|
47
|
+
api_gateway_event["path"] = "/items"
|
|
48
|
+
api_gateway_event["httpMethod"] = "GET"
|
|
49
|
+
|
|
50
|
+
context = MagicMock()
|
|
51
|
+
response = handler(api_gateway_event, context)
|
|
52
|
+
|
|
53
|
+
assert response["statusCode"] == 200
|
|
54
|
+
body = json.loads(response["body"])
|
|
55
|
+
assert "items" in body
|
|
56
|
+
assert body["count"] == 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_create_item_missing_name(api_gateway_event):
|
|
60
|
+
"""Test creating an item without name returns 400."""
|
|
61
|
+
api_gateway_event["path"] = "/items"
|
|
62
|
+
api_gateway_event["httpMethod"] = "POST"
|
|
63
|
+
api_gateway_event["body"] = json.dumps({"description": "no name"})
|
|
64
|
+
|
|
65
|
+
context = MagicMock()
|
|
66
|
+
response = handler(api_gateway_event, context)
|
|
67
|
+
|
|
68
|
+
assert response["statusCode"] == 400
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project_name": "my-cron-job",
|
|
3
|
+
"project_slug": "{{ cookiecutter.project_name|replace('-', '_') }}",
|
|
4
|
+
"module_name": "{{ cookiecutter.project_slug }}",
|
|
5
|
+
"description": "Scheduled Lambda triggered by EventBridge",
|
|
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": 300,
|
|
12
|
+
"schedule_expression": "rate(5 minutes)"
|
|
13
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib64/
|
|
14
|
+
parts/
|
|
15
|
+
sdist/
|
|
16
|
+
var/
|
|
17
|
+
wheels/
|
|
18
|
+
*.egg-info/
|
|
19
|
+
.installed.cfg
|
|
20
|
+
*.egg
|
|
21
|
+
MANIFEST
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
env/
|
|
27
|
+
ENV/
|
|
28
|
+
|
|
29
|
+
# Testing
|
|
30
|
+
.pytest_cache/
|
|
31
|
+
.coverage
|
|
32
|
+
.coverage.*
|
|
33
|
+
htmlcov/
|
|
34
|
+
.tox/
|
|
35
|
+
.nox/
|
|
36
|
+
coverage.xml
|
|
37
|
+
*.cover
|
|
38
|
+
|
|
39
|
+
# Type checking
|
|
40
|
+
.mypy_cache/
|
|
41
|
+
.ruff_cache/
|
|
42
|
+
|
|
43
|
+
# IDEs
|
|
44
|
+
.idea/
|
|
45
|
+
.vscode/
|
|
46
|
+
*.swp
|
|
47
|
+
*.swo
|
|
48
|
+
.DS_Store
|
|
49
|
+
|
|
50
|
+
# CDK
|
|
51
|
+
cdk.out/
|
|
52
|
+
cdk.context.json
|
|
53
|
+
|
|
54
|
+
# Lambda
|
|
55
|
+
*.zip
|
|
56
|
+
|
|
57
|
+
# Environment
|
|
58
|
+
.env
|
|
59
|
+
.env.local
|
|
60
|
+
.env.*.local
|
|
61
|
+
|
|
62
|
+
# Secrets
|
|
63
|
+
*.pem
|
|
64
|
+
*.key
|
|
65
|
+
secrets.yml
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
FROM public.ecr.aws/lambda/python:3.12-arm64
|
|
2
|
+
|
|
3
|
+
# Install dependencies first for better layer caching
|
|
4
|
+
COPY requirements.txt ${LAMBDA_TASK_ROOT}/
|
|
5
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
6
|
+
|
|
7
|
+
# Copy function code
|
|
8
|
+
COPY src/ ${LAMBDA_TASK_ROOT}/
|
|
9
|
+
|
|
10
|
+
# Set the handler
|
|
11
|
+
CMD [ "handler.handler" ]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const cdk = require('aws-cdk-lib');
|
|
2
|
+
const { {{ cookiecutter.project_name|replace('-', '')|capitalize }}Stack } = require('./lib/stack');
|
|
3
|
+
|
|
4
|
+
const app = new cdk.App();
|
|
5
|
+
|
|
6
|
+
// Environment from CDK context (default: dev)
|
|
7
|
+
const envName = app.node.tryGetContext('env') || 'dev';
|
|
8
|
+
|
|
9
|
+
new {{ cookiecutter.project_name|replace('-', '')|capitalize }}Stack(app, `{{ cookiecutter.project_name }}-${envName}`, {
|
|
10
|
+
stackName: `{{ cookiecutter.project_name }}-${envName}`,
|
|
11
|
+
env: {
|
|
12
|
+
account: process.env.CDK_DEFAULT_ACCOUNT,
|
|
13
|
+
region: process.env.CDK_DEFAULT_REGION || '{{ cookiecutter.aws_region }}',
|
|
14
|
+
},
|
|
15
|
+
description: `{{ cookiecutter.description }} (${envName})`,
|
|
16
|
+
tags: {
|
|
17
|
+
Project: '{{ cookiecutter.project_name }}',
|
|
18
|
+
Environment: envName,
|
|
19
|
+
ManagedBy: 'LambdaForge',
|
|
20
|
+
CostCenter: '{{ cookiecutter.project_name }}',
|
|
21
|
+
},
|
|
22
|
+
envName: envName,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
app.synth();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const cdk = require('aws-cdk-lib');
|
|
2
|
+
const lambda = require('aws-cdk-lib/aws-lambda');
|
|
3
|
+
const events = require('aws-cdk-lib/aws-events');
|
|
4
|
+
const targets = require('aws-cdk-lib/aws-events-targets');
|
|
5
|
+
const dynamodb = require('aws-cdk-lib/aws-dynamodb');
|
|
6
|
+
const logs = require('aws-cdk-lib/aws-logs');
|
|
7
|
+
const sqs = require('aws-cdk-lib/aws-sqs');
|
|
8
|
+
|
|
9
|
+
class {{ cookiecutter.project_name|replace('-', '')|capitalize }}Stack extends cdk.Stack {
|
|
10
|
+
constructor(scope, id, props) {
|
|
11
|
+
super(scope, id, props);
|
|
12
|
+
const envName = props.envName || 'dev';
|
|
13
|
+
|
|
14
|
+
const table = new dynamodb.Table(this, 'RunHistoryTable', {
|
|
15
|
+
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
|
|
16
|
+
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
17
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const dlq = new sqs.Queue(this, 'DLQ', { retentionPeriod: cdk.Duration.days(14) });
|
|
21
|
+
|
|
22
|
+
const fn = new lambda.Function(this, 'CronFunction', {
|
|
23
|
+
runtime: lambda.Runtime.PYTHON_3_12,
|
|
24
|
+
architecture: lambda.Architecture.ARM_64,
|
|
25
|
+
handler: 'handler.handler',
|
|
26
|
+
code: lambda.Code.fromAsset('../src'),
|
|
27
|
+
memorySize: {{ cookiecutter.memory_mb }},
|
|
28
|
+
timeout: cdk.Duration.seconds({{ cookiecutter.timeout_seconds }}),
|
|
29
|
+
tracing: lambda.Tracing.ACTIVE,
|
|
30
|
+
deadLetterQueue: dlq,
|
|
31
|
+
retryAttempts: 2,
|
|
32
|
+
environment: {
|
|
33
|
+
TABLE_NAME: table.tableName,
|
|
34
|
+
STAGE: envName,
|
|
35
|
+
POWERTOOLS_SERVICE_NAME: '{{ cookiecutter.project_slug }}',
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
table.grantReadWriteData(fn);
|
|
40
|
+
|
|
41
|
+
new events.Rule(this, 'ScheduleRule', {
|
|
42
|
+
schedule: events.Schedule.expression('{{ cookiecutter.schedule_expression }}'),
|
|
43
|
+
targets: [new targets.LambdaFunction(fn)],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
cdk.Tags.of(this).add('Project', '{{ cookiecutter.project_name }}');
|
|
47
|
+
cdk.Tags.of(this).add('ManagedBy', 'LambdaForge');
|
|
48
|
+
cdk.Tags.of(this).add('Environment', envName);
|
|
49
|
+
|
|
50
|
+
new cdk.CfnOutput(this, 'FunctionName', { value: fn.functionName });
|
|
51
|
+
new cdk.CfnOutput(this, 'TableName', { value: table.tableName });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { {{ cookiecutter.project_name|replace('-', '')|capitalize }}Stack };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{ cookiecutter.project_slug }}-infrastructure",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AWS CDK infrastructure for {{ cookiecutter.project_name }}",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"cdk": "cdk",
|
|
8
|
+
"synth": "cdk synth",
|
|
9
|
+
"deploy": "cdk deploy --require-approval never",
|
|
10
|
+
"destroy": "cdk destroy --force",
|
|
11
|
+
"diff": "cdk diff"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"aws-cdk-lib": "^2.170.0",
|
|
15
|
+
"constructs": "^10.4.0"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "{{ cookiecutter.project_slug }}"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "{{ cookiecutter.description }}"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "{{ cookiecutter.author_name }}", email = "{{ cookiecutter.author_email }}"}
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
keywords = ["aws", "lambda", "serverless", "cron", "scheduled"]
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
"aws-lambda-powertools[tracer,logger]>=3.0.0",
|
|
15
|
+
"boto3>=1.34.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
dev = [
|
|
20
|
+
"pytest>=8.0.0",
|
|
21
|
+
"pytest-cov>=4.1.0",
|
|
22
|
+
"moto[dynamodb]>=5.0.0",
|
|
23
|
+
"ruff>=0.1.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["hatchling"]
|
|
28
|
+
build-backend = "hatchling.build"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src"]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
testpaths = ["tests"]
|
|
35
|
+
python_files = ["test_*.py"]
|
|
36
|
+
python_functions = ["test_*"]
|
|
37
|
+
addopts = "-v --strict-markers --cov=src --cov-report=term-missing"
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
line-length = 100
|
|
41
|
+
target-version = "py312"
|
|
42
|
+
|
|
43
|
+
[tool.ruff.lint]
|
|
44
|
+
select = ["E", "F", "I", "B", "UP", "N", "S", "RUF"]
|
|
45
|
+
ignore = ["S101"]
|
|
46
|
+
|
|
47
|
+
[tool.coverage.run]
|
|
48
|
+
source = ["src"]
|
|
49
|
+
omit = ["tests/*"]
|
|
50
|
+
|
|
51
|
+
[tool.coverage.report]
|
|
52
|
+
exclude_lines = [
|
|
53
|
+
"pragma: no cover",
|
|
54
|
+
"if __name__ == .__main__.:",
|
|
55
|
+
"raise NotImplementedError",
|
|
56
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""{{ cookiecutter.project_name }} — Scheduled Lambda handler.
|
|
2
|
+
|
|
3
|
+
Triggered by EventBridge Schedule ({{ cookiecutter.schedule_expression }}).
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import boto3
|
|
12
|
+
from aws_lambda_powertools import Logger, Tracer
|
|
13
|
+
from aws_lambda_powertools.utilities.typing import LambdaContext
|
|
14
|
+
|
|
15
|
+
logger = Logger(service="{{ cookiecutter.project_slug }}")
|
|
16
|
+
tracer = Tracer(service="{{ cookiecutter.project_slug }}")
|
|
17
|
+
|
|
18
|
+
TABLE_NAME = os.environ.get("TABLE_NAME", "")
|
|
19
|
+
STAGE = os.environ.get("STAGE", "dev")
|
|
20
|
+
|
|
21
|
+
dynamodb = boto3.resource("dynamodb")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@tracer.capture_method
|
|
25
|
+
def run_job() -> dict[str, Any]:
|
|
26
|
+
"""Main job logic — replace with your business logic."""
|
|
27
|
+
now = datetime.now(UTC)
|
|
28
|
+
logger.info("job_started", extra={"timestamp": now.isoformat()})
|
|
29
|
+
|
|
30
|
+
result = {
|
|
31
|
+
"status": "completed",
|
|
32
|
+
"executed_at": now.isoformat(),
|
|
33
|
+
"items_processed": 0,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if TABLE_NAME:
|
|
37
|
+
table = dynamodb.Table(TABLE_NAME)
|
|
38
|
+
table.put_item(Item={"id": f"run#{now.strftime('%Y%m%d-%H%M%S')}", **result})
|
|
39
|
+
|
|
40
|
+
logger.info("job_completed", extra=result)
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@logger.inject_lambda_context
|
|
45
|
+
@tracer.capture_lambda_handler
|
|
46
|
+
def handler(event: dict, context: LambdaContext) -> dict[str, Any]:
|
|
47
|
+
"""Lambda handler — triggered by EventBridge schedule."""
|
|
48
|
+
try:
|
|
49
|
+
result = run_job()
|
|
50
|
+
return {"statusCode": 200, "body": result}
|
|
51
|
+
except Exception:
|
|
52
|
+
logger.exception("job_failed")
|
|
53
|
+
return {"statusCode": 500, "body": {"error": "Job failed"}}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Test configuration and fixtures."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture(autouse=True)
|
|
10
|
+
def aws_credentials(monkeypatch):
|
|
11
|
+
"""Mock AWS credentials for moto."""
|
|
12
|
+
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
|
|
13
|
+
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
|
|
14
|
+
monkeypatch.setenv("AWS_SECURITY_TOKEN", "testing")
|
|
15
|
+
monkeypatch.setenv("AWS_SESSION_TOKEN", "testing")
|
|
16
|
+
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def table_name():
|
|
21
|
+
"""Provide a test table name."""
|
|
22
|
+
return os.environ.get("TABLE_NAME", "test-table")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Tests for {{ cookiecutter.project_name }} scheduled Lambda handler."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from src.handler import handler
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def scheduled_event():
|
|
13
|
+
"""Sample EventBridge scheduled event."""
|
|
14
|
+
return {
|
|
15
|
+
"version": "0",
|
|
16
|
+
"id": "test-event-id",
|
|
17
|
+
"detail-type": "Scheduled Event",
|
|
18
|
+
"source": "aws.events",
|
|
19
|
+
"time": "2024-01-01T00:00:00Z",
|
|
20
|
+
"region": "us-east-1",
|
|
21
|
+
"resources": ["arn:aws:events:us-east-1:123456789012:rule/test-rule"],
|
|
22
|
+
"detail": {},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_handler_invocation(scheduled_event):
|
|
27
|
+
"""Test scheduled handler executes without error."""
|
|
28
|
+
context = MagicMock()
|
|
29
|
+
context.function_name = "test-function"
|
|
30
|
+
context.aws_request_id = "test-request-id"
|
|
31
|
+
|
|
32
|
+
with patch("src.handler.dynamodb"):
|
|
33
|
+
response = handler(scheduled_event, context)
|
|
34
|
+
|
|
35
|
+
assert response["statusCode"] == 200
|
|
36
|
+
assert response["body"]["status"] == "completed"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_handler_returns_execution_timestamp(scheduled_event):
|
|
40
|
+
"""Test that handler response includes execution timestamp."""
|
|
41
|
+
context = MagicMock()
|
|
42
|
+
context.function_name = "test-function"
|
|
43
|
+
context.aws_request_id = "test-request-id"
|
|
44
|
+
|
|
45
|
+
with patch("src.handler.dynamodb"):
|
|
46
|
+
response = handler(scheduled_event, context)
|
|
47
|
+
|
|
48
|
+
assert "executed_at" in response["body"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project_name": "my-queue-processor",
|
|
3
|
+
"project_slug": "{{ cookiecutter.project_name|replace('-', '_') }}",
|
|
4
|
+
"module_name": "{{ cookiecutter.project_slug }}",
|
|
5
|
+
"description": "SQS queue processor Lambda with partial batch response",
|
|
6
|
+
"author_name": "LambdaForge User",
|
|
7
|
+
"author_email": "user@example.com",
|
|
8
|
+
"runtime": "python3.12",
|
|
9
|
+
"aws_region": "us-east-1",
|
|
10
|
+
"memory_mb": 1024,
|
|
11
|
+
"timeout_seconds": 60
|
|
12
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib64/
|
|
14
|
+
parts/
|
|
15
|
+
sdist/
|
|
16
|
+
var/
|
|
17
|
+
wheels/
|
|
18
|
+
*.egg-info/
|
|
19
|
+
.installed.cfg
|
|
20
|
+
*.egg
|
|
21
|
+
MANIFEST
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
env/
|
|
27
|
+
ENV/
|
|
28
|
+
|
|
29
|
+
# Testing
|
|
30
|
+
.pytest_cache/
|
|
31
|
+
.coverage
|
|
32
|
+
.coverage.*
|
|
33
|
+
htmlcov/
|
|
34
|
+
.tox/
|
|
35
|
+
.nox/
|
|
36
|
+
coverage.xml
|
|
37
|
+
*.cover
|
|
38
|
+
|
|
39
|
+
# Type checking
|
|
40
|
+
.mypy_cache/
|
|
41
|
+
.ruff_cache/
|
|
42
|
+
|
|
43
|
+
# IDEs
|
|
44
|
+
.idea/
|
|
45
|
+
.vscode/
|
|
46
|
+
*.swp
|
|
47
|
+
*.swo
|
|
48
|
+
.DS_Store
|
|
49
|
+
|
|
50
|
+
# CDK
|
|
51
|
+
cdk.out/
|
|
52
|
+
cdk.context.json
|
|
53
|
+
|
|
54
|
+
# Lambda
|
|
55
|
+
*.zip
|
|
56
|
+
|
|
57
|
+
# Environment
|
|
58
|
+
.env
|
|
59
|
+
.env.local
|
|
60
|
+
.env.*.local
|
|
61
|
+
|
|
62
|
+
# Secrets
|
|
63
|
+
*.pem
|
|
64
|
+
*.key
|
|
65
|
+
secrets.yml
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
FROM public.ecr.aws/lambda/python:3.12-arm64
|
|
2
|
+
|
|
3
|
+
# Install dependencies first for better layer caching
|
|
4
|
+
COPY requirements.txt ${LAMBDA_TASK_ROOT}/
|
|
5
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
6
|
+
|
|
7
|
+
# Copy function code
|
|
8
|
+
COPY src/ ${LAMBDA_TASK_ROOT}/
|
|
9
|
+
|
|
10
|
+
# Set the handler
|
|
11
|
+
CMD [ "handler.handler" ]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const cdk = require('aws-cdk-lib');
|
|
2
|
+
const { {{ cookiecutter.project_name|replace('-', '')|capitalize }}Stack } = require('./lib/stack');
|
|
3
|
+
|
|
4
|
+
const app = new cdk.App();
|
|
5
|
+
|
|
6
|
+
// Environment from CDK context (default: dev)
|
|
7
|
+
const envName = app.node.tryGetContext('env') || 'dev';
|
|
8
|
+
|
|
9
|
+
new {{ cookiecutter.project_name|replace('-', '')|capitalize }}Stack(app, `{{ cookiecutter.project_name }}-${envName}`, {
|
|
10
|
+
stackName: `{{ cookiecutter.project_name }}-${envName}`,
|
|
11
|
+
env: {
|
|
12
|
+
account: process.env.CDK_DEFAULT_ACCOUNT,
|
|
13
|
+
region: process.env.CDK_DEFAULT_REGION || '{{ cookiecutter.aws_region }}',
|
|
14
|
+
},
|
|
15
|
+
description: `{{ cookiecutter.description }} (${envName})`,
|
|
16
|
+
tags: {
|
|
17
|
+
Project: '{{ cookiecutter.project_name }}',
|
|
18
|
+
Environment: envName,
|
|
19
|
+
ManagedBy: 'LambdaForge',
|
|
20
|
+
CostCenter: '{{ cookiecutter.project_name }}',
|
|
21
|
+
},
|
|
22
|
+
envName: envName,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
app.synth();
|