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,79 @@
1
+ const cdk = require('aws-cdk-lib');
2
+ const lambda = require('aws-cdk-lib/aws-lambda');
3
+ const sqs = require('aws-cdk-lib/aws-sqs');
4
+ const dynamodb = require('aws-cdk-lib/aws-dynamodb');
5
+ const logs = require('aws-cdk-lib/aws-logs');
6
+ const { SqsEventSource } = require('aws-cdk-lib/aws-lambda-event-sources');
7
+
8
+ class {{ cookiecutter.project_name|replace('-', '')|capitalize }}Stack extends cdk.Stack {
9
+ constructor(scope, id, props) {
10
+ super(scope, id, props);
11
+ const envName = props.envName || 'dev';
12
+
13
+ // Dead Letter Queue
14
+ const dlq = new sqs.Queue(this, 'DeadLetterQueue', {
15
+ queueName: `{{ cookiecutter.project_name }}-dlq-${envName}`,
16
+ retentionPeriod: cdk.Duration.days(14),
17
+ });
18
+
19
+ // Main SQS Queue
20
+ const queue = new sqs.Queue(this, 'ProcessingQueue', {
21
+ queueName: `{{ cookiecutter.project_name }}-queue-${envName}`,
22
+ visibilityTimeout: cdk.Duration.seconds({{ cookiecutter.timeout_seconds }} * 6),
23
+ deadLetterQueue: {
24
+ queue: dlq,
25
+ maxReceiveCount: 3,
26
+ },
27
+ });
28
+
29
+ // DynamoDB Table
30
+ const table = new dynamodb.Table(this, 'ProcessingTable', {
31
+ partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
32
+ billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
33
+ pointInTimeRecovery: true,
34
+ removalPolicy: cdk.RemovalPolicy.DESTROY,
35
+ });
36
+
37
+ // Lambda Function
38
+ const fn = new lambda.Function(this, 'ProcessorFunction', {
39
+ runtime: lambda.Runtime.PYTHON_3_12,
40
+ architecture: lambda.Architecture.ARM_64,
41
+ handler: 'handler.handler',
42
+ code: lambda.Code.fromAsset('../src'),
43
+ memorySize: {{ cookiecutter.memory_mb }},
44
+ timeout: cdk.Duration.seconds({{ cookiecutter.timeout_seconds }}),
45
+ tracing: lambda.Tracing.ACTIVE,
46
+ logRetention: logs.RetentionDays.ONE_MONTH,
47
+ environment: {
48
+ TABLE_NAME: table.tableName,
49
+ STAGE: envName,
50
+ POWERTOOLS_SERVICE_NAME: '{{ cookiecutter.project_slug }}',
51
+ },
52
+ });
53
+
54
+ // Grant permissions
55
+ table.grantReadWriteData(fn);
56
+ queue.grantConsumeMessages(fn);
57
+
58
+ // SQS Event Source with partial batch response
59
+ fn.addEventSource(new SqsEventSource(queue, {
60
+ batchSize: 10,
61
+ maxBatchingWindow: cdk.Duration.seconds(5),
62
+ reportBatchItemFailures: true,
63
+ }));
64
+
65
+ // Tags
66
+ cdk.Tags.of(this).add('Project', '{{ cookiecutter.project_name }}');
67
+ cdk.Tags.of(this).add('ManagedBy', 'LambdaForge');
68
+ cdk.Tags.of(this).add('Environment', envName);
69
+
70
+ // Outputs
71
+ new cdk.CfnOutput(this, 'FunctionName', { value: fn.functionName });
72
+ new cdk.CfnOutput(this, 'QueueUrl', { value: queue.queueUrl });
73
+ new cdk.CfnOutput(this, 'QueueArn', { value: queue.queueArn });
74
+ new cdk.CfnOutput(this, 'TableName', { value: table.tableName });
75
+ new cdk.CfnOutput(this, 'DlqUrl', { value: dlq.queueUrl });
76
+ }
77
+ }
78
+
79
+ 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", "sqs", "queue"]
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,sqs]>=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,64 @@
1
+ """{{ cookiecutter.project_name }} — SQS batch processor handler.
2
+
3
+ Processes messages from SQS with partial batch failure support.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import os
9
+ from typing import Any
10
+
11
+ import boto3
12
+ from aws_lambda_powertools import Logger, Tracer
13
+ from aws_lambda_powertools.utilities.batch import (
14
+ BatchProcessor,
15
+ EventType,
16
+ process_partial_response,
17
+ )
18
+ from aws_lambda_powertools.utilities.data_classes.sqs_event import SQSRecord
19
+ from aws_lambda_powertools.utilities.typing import LambdaContext
20
+
21
+ logger = Logger(service="{{ cookiecutter.project_slug }}")
22
+ tracer = Tracer(service="{{ cookiecutter.project_slug }}")
23
+
24
+ TABLE_NAME = os.environ.get("TABLE_NAME", "")
25
+ STAGE = os.environ.get("STAGE", "dev")
26
+
27
+ processor = BatchProcessor(event_type=EventType.SQS)
28
+ dynamodb = boto3.resource("dynamodb")
29
+
30
+
31
+ @tracer.capture_method
32
+ def process_record(record: SQSRecord) -> dict[str, Any]:
33
+ """Process a single SQS record.
34
+
35
+ Replace this with your business logic.
36
+ """
37
+ body = json.loads(record.body)
38
+ message_id = record.message_id
39
+
40
+ logger.info("processing_record", extra={"message_id": message_id, "body": body})
41
+
42
+ result = {
43
+ "message_id": message_id,
44
+ "status": "processed",
45
+ }
46
+
47
+ if TABLE_NAME:
48
+ table = dynamodb.Table(TABLE_NAME)
49
+ table.put_item(Item={"id": message_id, **result})
50
+
51
+ logger.info("record_processed", extra=result)
52
+ return result
53
+
54
+
55
+ @logger.inject_lambda_context
56
+ @tracer.capture_lambda_handler
57
+ def handler(event: dict, context: LambdaContext) -> dict[str, Any]:
58
+ """Lambda handler — processes SQS batch with partial failure support."""
59
+ return process_partial_response(
60
+ event=event,
61
+ record_handler=process_record,
62
+ processor=processor,
63
+ context=context,
64
+ )
@@ -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,60 @@
1
+ """Tests for {{ cookiecutter.project_name }} SQS processor handler."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from unittest.mock import MagicMock, patch
6
+
7
+ import pytest
8
+
9
+ from src.handler import handler
10
+
11
+
12
+ @pytest.fixture
13
+ def sqs_event():
14
+ """Sample SQS batch event."""
15
+ return {
16
+ "Records": [
17
+ {
18
+ "messageId": "msg-001",
19
+ "receiptHandle": "test-receipt-handle",
20
+ "body": json.dumps({"action": "process", "data": "test-payload"}),
21
+ "attributes": {
22
+ "ApproximateReceiveCount": "1",
23
+ "SentTimestamp": "1704067200000",
24
+ "SenderId": "123456789012",
25
+ "ApproximateFirstReceiveTimestamp": "1704067200001",
26
+ },
27
+ "messageAttributes": {},
28
+ "md5OfBody": "test-md5",
29
+ "eventSource": "aws:sqs",
30
+ "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:test-queue",
31
+ "awsRegion": "us-east-1",
32
+ }
33
+ ]
34
+ }
35
+
36
+
37
+ def test_handler_processes_batch(sqs_event):
38
+ """Test handler processes SQS batch without error."""
39
+ context = MagicMock()
40
+ context.function_name = "test-function"
41
+ context.aws_request_id = "test-request-id"
42
+
43
+ with patch("src.handler.dynamodb"):
44
+ response = handler(sqs_event, context)
45
+
46
+ assert "batchItemFailures" in response
47
+
48
+
49
+ def test_handler_handles_empty_batch():
50
+ """Test handler handles empty records list."""
51
+ event = {"Records": []}
52
+ context = MagicMock()
53
+ context.function_name = "test-function"
54
+ context.aws_request_id = "test-request-id"
55
+
56
+ with patch("src.handler.dynamodb"):
57
+ response = handler(event, context)
58
+
59
+ assert "batchItemFailures" in response
60
+ assert response["batchItemFailures"] == []
@@ -0,0 +1,13 @@
1
+ {
2
+ "project_name": "my-s3-processor",
3
+ "project_slug": "{{ cookiecutter.project_name|replace('-', '_') }}",
4
+ "module_name": "{{ cookiecutter.project_slug }}",
5
+ "description": "S3 event processor 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": 1024,
11
+ "timeout_seconds": 120,
12
+ "source_bucket_prefix": "uploads/"
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,89 @@
1
+ const cdk = require('aws-cdk-lib');
2
+ const lambda = require('aws-cdk-lib/aws-lambda');
3
+ const s3 = require('aws-cdk-lib/aws-s3');
4
+ const s3n = require('aws-cdk-lib/aws-s3-notifications');
5
+ const dynamodb = require('aws-cdk-lib/aws-dynamodb');
6
+ const sqs = require('aws-cdk-lib/aws-sqs');
7
+ const logs = require('aws-cdk-lib/aws-logs');
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
+ // S3 Source Bucket
15
+ const bucket = new s3.Bucket(this, 'SourceBucket', {
16
+ bucketName: `{{ cookiecutter.project_name }}-source-${envName}-${this.account}`,
17
+ blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
18
+ enforceSSL: true,
19
+ versioned: true,
20
+ removalPolicy: cdk.RemovalPolicy.DESTROY,
21
+ autoDeleteObjects: true,
22
+ lifecycleRules: [
23
+ {
24
+ expiration: cdk.Duration.days(90),
25
+ noncurrentVersionExpiration: cdk.Duration.days(30),
26
+ },
27
+ ],
28
+ });
29
+
30
+ // Dead Letter Queue
31
+ const dlq = new sqs.Queue(this, 'DeadLetterQueue', {
32
+ queueName: `{{ cookiecutter.project_name }}-dlq-${envName}`,
33
+ retentionPeriod: cdk.Duration.days(14),
34
+ });
35
+
36
+ // DynamoDB Table for tracking processed objects
37
+ const table = new dynamodb.Table(this, 'ProcessingTable', {
38
+ partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
39
+ billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
40
+ pointInTimeRecovery: true,
41
+ removalPolicy: cdk.RemovalPolicy.DESTROY,
42
+ });
43
+
44
+ // Lambda Function
45
+ const fn = new lambda.Function(this, 'S3ProcessorFunction', {
46
+ runtime: lambda.Runtime.PYTHON_3_12,
47
+ architecture: lambda.Architecture.ARM_64,
48
+ handler: 'handler.handler',
49
+ code: lambda.Code.fromAsset('../src'),
50
+ memorySize: {{ cookiecutter.memory_mb }},
51
+ timeout: cdk.Duration.seconds({{ cookiecutter.timeout_seconds }}),
52
+ tracing: lambda.Tracing.ACTIVE,
53
+ deadLetterQueue: dlq,
54
+ retryAttempts: 2,
55
+ logRetention: logs.RetentionDays.ONE_MONTH,
56
+ environment: {
57
+ TABLE_NAME: table.tableName,
58
+ SOURCE_BUCKET: bucket.bucketName,
59
+ STAGE: envName,
60
+ POWERTOOLS_SERVICE_NAME: '{{ cookiecutter.project_slug }}',
61
+ },
62
+ });
63
+
64
+ // Grant permissions
65
+ table.grantReadWriteData(fn);
66
+ bucket.grantRead(fn);
67
+
68
+ // S3 Event Notification — trigger on PutObject with prefix
69
+ bucket.addEventNotification(
70
+ s3.EventType.OBJECT_CREATED_PUT,
71
+ new s3n.LambdaDestination(fn),
72
+ { prefix: '{{ cookiecutter.source_bucket_prefix }}' }
73
+ );
74
+
75
+ // Tags
76
+ cdk.Tags.of(this).add('Project', '{{ cookiecutter.project_name }}');
77
+ cdk.Tags.of(this).add('ManagedBy', 'LambdaForge');
78
+ cdk.Tags.of(this).add('Environment', envName);
79
+
80
+ // Outputs
81
+ new cdk.CfnOutput(this, 'FunctionName', { value: fn.functionName });
82
+ new cdk.CfnOutput(this, 'BucketName', { value: bucket.bucketName });
83
+ new cdk.CfnOutput(this, 'BucketArn', { value: bucket.bucketArn });
84
+ new cdk.CfnOutput(this, 'TableName', { value: table.tableName });
85
+ new cdk.CfnOutput(this, 'DlqUrl', { value: dlq.queueUrl });
86
+ }
87
+ }
88
+
89
+ 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", "s3", "file-processing"]
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,s3]>=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,86 @@
1
+ """{{ cookiecutter.project_name }} — S3 event processor handler.
2
+
3
+ Processes S3 PutObject events for objects matching prefix: {{ cookiecutter.source_bucket_prefix }}
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import urllib.parse
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
+ s3_client = boto3.client("s3")
22
+ dynamodb = boto3.resource("dynamodb")
23
+
24
+
25
+ @tracer.capture_method
26
+ def process_s3_object(bucket: str, key: str) -> dict[str, Any]:
27
+ """Process a single S3 object.
28
+
29
+ Downloads the object and performs processing.
30
+ Replace this with your business logic.
31
+ """
32
+ logger.info("processing_object", extra={"bucket": bucket, "key": key})
33
+
34
+ # Download the object
35
+ response = s3_client.get_object(Bucket=bucket, Key=key)
36
+ content_length = response["ContentLength"]
37
+ content_type = response.get("ContentType", "unknown")
38
+
39
+ result = {
40
+ "bucket": bucket,
41
+ "key": key,
42
+ "content_length": content_length,
43
+ "content_type": content_type,
44
+ "status": "processed",
45
+ }
46
+
47
+ if TABLE_NAME:
48
+ table = dynamodb.Table(TABLE_NAME)
49
+ table.put_item(Item={"id": f"{bucket}/{key}", **result})
50
+
51
+ logger.info("object_processed", extra=result)
52
+ return result
53
+
54
+
55
+ @logger.inject_lambda_context
56
+ @tracer.capture_lambda_handler
57
+ def handler(event: dict, context: LambdaContext) -> dict[str, Any]:
58
+ """Lambda handler — processes S3 PutObject events."""
59
+ results = []
60
+ errors = []
61
+
62
+ for record in event.get("Records", []):
63
+ try:
64
+ bucket = record["s3"]["bucket"]["name"]
65
+ key = urllib.parse.unquote_plus(record["s3"]["object"]["key"])
66
+
67
+ result = process_s3_object(bucket, key)
68
+ results.append(result)
69
+ except Exception:
70
+ logger.exception("record_processing_failed", extra={"record": record})
71
+ errors.append(record)
72
+
73
+ response = {
74
+ "statusCode": 200,
75
+ "body": {
76
+ "processed": len(results),
77
+ "failed": len(errors),
78
+ "results": results,
79
+ },
80
+ }
81
+
82
+ if errors:
83
+ response["statusCode"] = 207 # Multi-Status
84
+ logger.warning("partial_failure", extra={"failed_count": len(errors)})
85
+
86
+ return response