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.
Files changed (80) hide show
  1. lambda_forge_cli_aws-1.0.0.dist-info/METADATA +352 -0
  2. lambda_forge_cli_aws-1.0.0.dist-info/RECORD +80 -0
  3. lambda_forge_cli_aws-1.0.0.dist-info/WHEEL +4 -0
  4. lambda_forge_cli_aws-1.0.0.dist-info/entry_points.txt +2 -0
  5. lambda_forge_cli_aws-1.0.0.dist-info/licenses/LICENSE +189 -0
  6. lambdaforge/__init__.py +10 -0
  7. lambdaforge/bedrock_client.py +109 -0
  8. lambdaforge/cli.py +428 -0
  9. lambdaforge/deploy.py +308 -0
  10. lambdaforge/logs.py +185 -0
  11. lambdaforge/renderer.py +72 -0
  12. lambdaforge/spec_generator.py +171 -0
  13. lambdaforge/status.py +111 -0
  14. lambdaforge/templates.py +106 -0
  15. lambdaforge/upgrade.py +231 -0
  16. templates/python-api/cookiecutter.json +12 -0
  17. templates/python-api/{{cookiecutter.project_slug}}/.github/workflows/dependabot.yml +22 -0
  18. templates/python-api/{{cookiecutter.project_slug}}/.github/workflows/deploy.yml +90 -0
  19. templates/python-api/{{cookiecutter.project_slug}}/.gitignore +65 -0
  20. templates/python-api/{{cookiecutter.project_slug}}/Dockerfile +11 -0
  21. templates/python-api/{{cookiecutter.project_slug}}/README.md +130 -0
  22. templates/python-api/{{cookiecutter.project_slug}}/infrastructure/app.js +26 -0
  23. templates/python-api/{{cookiecutter.project_slug}}/infrastructure/cdk.json +20 -0
  24. templates/python-api/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +155 -0
  25. templates/python-api/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
  26. templates/python-api/{{cookiecutter.project_slug}}/pyproject.toml +66 -0
  27. templates/python-api/{{cookiecutter.project_slug}}/requirements-dev.txt +8 -0
  28. templates/python-api/{{cookiecutter.project_slug}}/requirements.txt +3 -0
  29. templates/python-api/{{cookiecutter.project_slug}}/src/handler.py +93 -0
  30. templates/python-api/{{cookiecutter.project_slug}}/src/utils/logger.py +39 -0
  31. templates/python-api/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
  32. templates/python-api/{{cookiecutter.project_slug}}/tests/test_handler.py +68 -0
  33. templates/python-cron/cookiecutter.json +13 -0
  34. templates/python-cron/{{cookiecutter.project_slug}}/.gitignore +65 -0
  35. templates/python-cron/{{cookiecutter.project_slug}}/Dockerfile +11 -0
  36. templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
  37. templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
  38. templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +55 -0
  39. templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
  40. templates/python-cron/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
  41. templates/python-cron/{{cookiecutter.project_slug}}/requirements.txt +2 -0
  42. templates/python-cron/{{cookiecutter.project_slug}}/src/handler.py +53 -0
  43. templates/python-cron/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
  44. templates/python-cron/{{cookiecutter.project_slug}}/tests/test_handler.py +48 -0
  45. templates/python-queue/cookiecutter.json +12 -0
  46. templates/python-queue/{{cookiecutter.project_slug}}/.gitignore +65 -0
  47. templates/python-queue/{{cookiecutter.project_slug}}/Dockerfile +11 -0
  48. templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
  49. templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
  50. templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +79 -0
  51. templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
  52. templates/python-queue/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
  53. templates/python-queue/{{cookiecutter.project_slug}}/requirements.txt +2 -0
  54. templates/python-queue/{{cookiecutter.project_slug}}/src/handler.py +64 -0
  55. templates/python-queue/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
  56. templates/python-queue/{{cookiecutter.project_slug}}/tests/test_handler.py +60 -0
  57. templates/python-s3/cookiecutter.json +13 -0
  58. templates/python-s3/{{cookiecutter.project_slug}}/.gitignore +65 -0
  59. templates/python-s3/{{cookiecutter.project_slug}}/Dockerfile +11 -0
  60. templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
  61. templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
  62. templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +89 -0
  63. templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
  64. templates/python-s3/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
  65. templates/python-s3/{{cookiecutter.project_slug}}/requirements.txt +2 -0
  66. templates/python-s3/{{cookiecutter.project_slug}}/src/handler.py +86 -0
  67. templates/python-s3/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
  68. templates/python-s3/{{cookiecutter.project_slug}}/tests/test_handler.py +71 -0
  69. templates/python-stream/cookiecutter.json +12 -0
  70. templates/python-stream/{{cookiecutter.project_slug}}/.gitignore +65 -0
  71. templates/python-stream/{{cookiecutter.project_slug}}/Dockerfile +11 -0
  72. templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
  73. templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
  74. templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +81 -0
  75. templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
  76. templates/python-stream/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
  77. templates/python-stream/{{cookiecutter.project_slug}}/requirements.txt +2 -0
  78. templates/python-stream/{{cookiecutter.project_slug}}/src/handler.py +69 -0
  79. templates/python-stream/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
  80. 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,3 @@
1
+ {
2
+ "app": "node app.js"
3
+ }
@@ -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,2 @@
1
+ aws-lambda-powertools[tracer,logger]>=3.0.0
2
+ boto3>=1.34.0
@@ -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();
@@ -0,0 +1,3 @@
1
+ {
2
+ "app": "node app.js"
3
+ }