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,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,71 @@
1
+ """Tests for {{ cookiecutter.project_name }} S3 event processor 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 s3_put_event():
13
+ """Sample S3 PutObject event."""
14
+ return {
15
+ "Records": [
16
+ {
17
+ "eventVersion": "2.1",
18
+ "eventSource": "aws:s3",
19
+ "awsRegion": "us-east-1",
20
+ "eventTime": "2024-01-01T00:00:00.000Z",
21
+ "eventName": "ObjectCreated:Put",
22
+ "s3": {
23
+ "s3SchemaVersion": "1.0",
24
+ "configurationId": "test-config",
25
+ "bucket": {
26
+ "name": "test-bucket",
27
+ "arn": "arn:aws:s3:::test-bucket",
28
+ },
29
+ "object": {
30
+ "key": "uploads/test-file.csv",
31
+ "size": 1024,
32
+ "eTag": "test-etag",
33
+ },
34
+ },
35
+ }
36
+ ]
37
+ }
38
+
39
+
40
+ def test_handler_processes_s3_event(s3_put_event):
41
+ """Test handler processes S3 PutObject event without error."""
42
+ context = MagicMock()
43
+ context.function_name = "test-function"
44
+ context.aws_request_id = "test-request-id"
45
+
46
+ mock_response = {
47
+ "ContentLength": 1024,
48
+ "ContentType": "text/csv",
49
+ "Body": MagicMock(),
50
+ }
51
+
52
+ with patch("src.handler.s3_client.get_object", return_value=mock_response), \
53
+ patch("src.handler.dynamodb"):
54
+ response = handler(s3_put_event, context)
55
+
56
+ assert response["statusCode"] == 200
57
+ assert response["body"]["processed"] == 1
58
+ assert response["body"]["failed"] == 0
59
+
60
+
61
+ def test_handler_handles_empty_records():
62
+ """Test handler handles event with no records."""
63
+ event = {"Records": []}
64
+ context = MagicMock()
65
+ context.function_name = "test-function"
66
+ context.aws_request_id = "test-request-id"
67
+
68
+ response = handler(event, context)
69
+
70
+ assert response["statusCode"] == 200
71
+ assert response["body"]["processed"] == 0
@@ -0,0 +1,12 @@
1
+ {
2
+ "project_name": "my-stream-processor",
3
+ "project_slug": "{{ cookiecutter.project_name|replace('-', '_') }}",
4
+ "module_name": "{{ cookiecutter.project_slug }}",
5
+ "description": "Kinesis/DynamoDB Streams 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": 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
+ }
@@ -0,0 +1,81 @@
1
+ const cdk = require('aws-cdk-lib');
2
+ const lambda = require('aws-cdk-lib/aws-lambda');
3
+ const kinesis = require('aws-cdk-lib/aws-kinesis');
4
+ const dynamodb = require('aws-cdk-lib/aws-dynamodb');
5
+ const sqs = require('aws-cdk-lib/aws-sqs');
6
+ const logs = require('aws-cdk-lib/aws-logs');
7
+ const { KinesisEventSource } = require('aws-cdk-lib/aws-lambda-event-sources');
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
+ // Kinesis Data Stream
15
+ const stream = new kinesis.Stream(this, 'DataStream', {
16
+ streamName: `{{ cookiecutter.project_name }}-stream-${envName}`,
17
+ shardCount: 1,
18
+ retentionPeriod: cdk.Duration.hours(24),
19
+ });
20
+
21
+ // Dead Letter Queue for failed records
22
+ const dlq = new sqs.Queue(this, 'DeadLetterQueue', {
23
+ queueName: `{{ cookiecutter.project_name }}-dlq-${envName}`,
24
+ retentionPeriod: cdk.Duration.days(14),
25
+ });
26
+
27
+ // DynamoDB Table for processed results
28
+ const table = new dynamodb.Table(this, 'ProcessingTable', {
29
+ partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
30
+ billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
31
+ pointInTimeRecovery: true,
32
+ removalPolicy: cdk.RemovalPolicy.DESTROY,
33
+ });
34
+
35
+ // Lambda Function
36
+ const fn = new lambda.Function(this, 'StreamProcessorFunction', {
37
+ runtime: lambda.Runtime.PYTHON_3_12,
38
+ architecture: lambda.Architecture.ARM_64,
39
+ handler: 'handler.handler',
40
+ code: lambda.Code.fromAsset('../src'),
41
+ memorySize: {{ cookiecutter.memory_mb }},
42
+ timeout: cdk.Duration.seconds({{ cookiecutter.timeout_seconds }}),
43
+ tracing: lambda.Tracing.ACTIVE,
44
+ logRetention: logs.RetentionDays.ONE_MONTH,
45
+ environment: {
46
+ TABLE_NAME: table.tableName,
47
+ STAGE: envName,
48
+ POWERTOOLS_SERVICE_NAME: '{{ cookiecutter.project_slug }}',
49
+ },
50
+ });
51
+
52
+ // Grant permissions
53
+ table.grantReadWriteData(fn);
54
+ stream.grantRead(fn);
55
+ dlq.grantSendMessages(fn);
56
+
57
+ // Kinesis Event Source with batch processing
58
+ fn.addEventSource(new KinesisEventSource(stream, {
59
+ batchSize: 100,
60
+ maxBatchingWindow: cdk.Duration.seconds(5),
61
+ startingPosition: lambda.StartingPosition.TRIM_HORIZON,
62
+ bisectBatchOnError: true,
63
+ retryAttempts: 3,
64
+ reportBatchItemFailures: true,
65
+ }));
66
+
67
+ // Tags
68
+ cdk.Tags.of(this).add('Project', '{{ cookiecutter.project_name }}');
69
+ cdk.Tags.of(this).add('ManagedBy', 'LambdaForge');
70
+ cdk.Tags.of(this).add('Environment', envName);
71
+
72
+ // Outputs
73
+ new cdk.CfnOutput(this, 'FunctionName', { value: fn.functionName });
74
+ new cdk.CfnOutput(this, 'StreamName', { value: stream.streamName });
75
+ new cdk.CfnOutput(this, 'StreamArn', { value: stream.streamArn });
76
+ new cdk.CfnOutput(this, 'TableName', { value: table.tableName });
77
+ new cdk.CfnOutput(this, 'DlqUrl', { value: dlq.queueUrl });
78
+ }
79
+ }
80
+
81
+ 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", "kinesis", "stream"]
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,kinesis]>=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,69 @@
1
+ """{{ cookiecutter.project_name }} — Kinesis/DynamoDB Streams processor handler.
2
+
3
+ Processes records from a Kinesis data stream with batch checkpointing.
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.kinesis_stream_event import (
19
+ KinesisStreamRecord,
20
+ )
21
+ from aws_lambda_powertools.utilities.typing import LambdaContext
22
+
23
+ logger = Logger(service="{{ cookiecutter.project_slug }}")
24
+ tracer = Tracer(service="{{ cookiecutter.project_slug }}")
25
+
26
+ TABLE_NAME = os.environ.get("TABLE_NAME", "")
27
+ STAGE = os.environ.get("STAGE", "dev")
28
+
29
+ processor = BatchProcessor(event_type=EventType.KinesisDataStreams)
30
+ dynamodb = boto3.resource("dynamodb")
31
+
32
+
33
+ @tracer.capture_method
34
+ def process_record(record: KinesisStreamRecord) -> dict[str, Any]:
35
+ """Process a single stream record.
36
+
37
+ Replace this with your business logic.
38
+ """
39
+ payload = json.loads(record.kinesis.data_as_text())
40
+ sequence_number = record.kinesis.sequence_number
41
+
42
+ logger.info(
43
+ "processing_record",
44
+ extra={"sequence_number": sequence_number, "payload": payload},
45
+ )
46
+
47
+ result = {
48
+ "sequence_number": sequence_number,
49
+ "status": "processed",
50
+ }
51
+
52
+ if TABLE_NAME:
53
+ table = dynamodb.Table(TABLE_NAME)
54
+ table.put_item(Item={"id": sequence_number, **result})
55
+
56
+ logger.info("record_processed", extra=result)
57
+ return result
58
+
59
+
60
+ @logger.inject_lambda_context
61
+ @tracer.capture_lambda_handler
62
+ def handler(event: dict, context: LambdaContext) -> dict[str, Any]:
63
+ """Lambda handler — processes Kinesis stream batch with checkpointing."""
64
+ return process_partial_response(
65
+ event=event,
66
+ record_handler=process_record,
67
+ processor=processor,
68
+ context=context,
69
+ )
@@ -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,64 @@
1
+ """Tests for {{ cookiecutter.project_name }} Kinesis stream processor handler."""
2
+ from __future__ import annotations
3
+
4
+ import base64
5
+ import json
6
+ from unittest.mock import MagicMock, patch
7
+
8
+ import pytest
9
+
10
+ from src.handler import handler
11
+
12
+
13
+ @pytest.fixture
14
+ def kinesis_event():
15
+ """Sample Kinesis stream event."""
16
+ data = json.dumps({"action": "update", "entity_id": "test-123"})
17
+ encoded_data = base64.b64encode(data.encode()).decode()
18
+
19
+ return {
20
+ "Records": [
21
+ {
22
+ "kinesis": {
23
+ "kinesisSchemaVersion": "1.0",
24
+ "partitionKey": "partition-1",
25
+ "sequenceNumber": "49590338271490256608559692538361571095921575989136588898",
26
+ "data": encoded_data,
27
+ "approximateArrivalTimestamp": 1704067200.0,
28
+ },
29
+ "eventSource": "aws:kinesis",
30
+ "eventVersion": "1.0",
31
+ "eventID": "shardId-000000000000:49590338271490256608559692538361571095921575989136588898",
32
+ "eventName": "aws:kinesis:record",
33
+ "invokeIdentityArn": "arn:aws:iam::123456789012:role/test-role",
34
+ "awsRegion": "us-east-1",
35
+ "eventSourceARN": "arn:aws:kinesis:us-east-1:123456789012:stream/test-stream",
36
+ }
37
+ ]
38
+ }
39
+
40
+
41
+ def test_handler_processes_stream_batch(kinesis_event):
42
+ """Test handler processes Kinesis batch without error."""
43
+ context = MagicMock()
44
+ context.function_name = "test-function"
45
+ context.aws_request_id = "test-request-id"
46
+
47
+ with patch("src.handler.dynamodb"):
48
+ response = handler(kinesis_event, context)
49
+
50
+ assert "batchItemFailures" in response
51
+
52
+
53
+ def test_handler_handles_empty_batch():
54
+ """Test handler handles empty records list."""
55
+ event = {"Records": []}
56
+ context = MagicMock()
57
+ context.function_name = "test-function"
58
+ context.aws_request_id = "test-request-id"
59
+
60
+ with patch("src.handler.dynamodb"):
61
+ response = handler(event, context)
62
+
63
+ assert "batchItemFailures" in response
64
+ assert response["batchItemFailures"] == []