This file provides guidance for AI agents working in this repository.
Python AWS Lambda project template using mise for tool management and uv for dependency management.
mise run install # install dependencies
mise run precommit # install pre-commit hooksmise run lint # run ruff (check + format) and pyright
mise run test # run pytest with coverage
mise run infra:synth # synthesize CDK stack(s) (alias: synth, s)
mise run docs # build and deploy docs to GitHub Pages
mise run docs-local # serve docs locally
mise run local:up # start LocalStack via Docker Compose
mise run local:deploy # deploy CDK stack to LocalStack (alias: local, dl)
mise run local:destroy # destroy CDK stack from LocalStack (alias: Dl)
mise run local:down # stop LocalStack via Docker Compose- Line length: 120 characters (enforced by ruff)
- Linting rules:
E(pycodestyle errors) andI(isort) via ruff - Type checking: pyright in strict mode
- All code must pass
mise run lintbefore committing (enforced by pre-commit hooks)
Tests live in tests/. Run with:
mise run testCoverage is measured with coverage and reported to stdout and coverage.xml. The source under test is templates/.
templates/ # main package
api/ # API Gateway + DynamoDB scenario
stream/ # DynamoDB Streams scenario
tests/ # pytest tests
docs/ # MkDocs documentation
infra/ # AWS CDK infrastructure stacks
compose.yml # LocalStack container configuration
To initialize the project from the default templates name, run:
mise run init --name="my-project" --description="My description" --author="Name" --email="handle" --github="username"- Always use uv for dependency management (
uv add <package>) - Use Pydantic for data models
- Use Pydantic-settings for environment variable configuration in a
settings.pyfile - Use AWS Lambda Powertools wherever applicable: logger, tracer, metrics, parameters, event types, event handlers, etc.
- Define infrastructure using AWS CDK under the
infra/folder. - Synthesize CloudFormation templates:
mise run infra:synth [stack](aliassynth [stack],s [stack]). - Deploy to AWS:
mise run deploy <stack>(aliasd <stack>). - Deploy locally using LocalStack: copy
.env.local.exampleto.env.localand set token from app.localstack.cloud, thenmise run local:deploy <stack>(aliaslocal <stack>,dl <stack>). - Destroy locally from LocalStack:
mise run local:destroy <stack>(aliasDl <stack>).
- Use pytest, not unittest
- Use
pytestmonkeypatch andpytest-mockfor mocking instead ofunittest.MagicMock - Use
moto.mock_awsfor mocking AWS services in tests (e.g. DynamoDB, S3, Secrets Manager) - Do not cheat! Never modify source code just to make a failing test pass. Fix real bugs in source code and fix incorrect assertions in tests
Use mise run <task> for all common workflows: lint, test, run locally, and deploy. Refer to docs/README.md for currently available tasks. Add new tasks to mise.toml as needed.
- Python 3.14+ required
- Dependencies are managed via
pyproject.tomland locked inuv.lock - Tooling is managed via
mise.toml
Every field in a Pydantic model or pydantic-settings class must be documented using Field(description="..."). This makes descriptions machine-readable and visible in generated JSON schemas.
from pydantic import BaseModel, Field
class Item(BaseModel, populate_by_name=True, alias_generator=to_camel):
id: str = Field(description="Unique item identifier.")
name: str = Field(description="Human-readable item name.")All BaseModel subclasses must be defined with populate_by_name=True and alias_generator=to_camel so that JSON payloads can use camelCase while Python attributes use snake_case. Always serialise with model_dump(by_alias=True, exclude_none=True) to produce camelCase JSON output and omit unset optional fields.
from uuid import uuid4
from pydantic import BaseModel, Field
from pydantic.alias_generators import to_camel
class Item(BaseModel, populate_by_name=True, alias_generator=to_camel):
item_id: str = Field(description="Unique item identifier.", default_factory=str(uuid4()))
# Accepts {"itemId": "..."} from JSON; attribute is item.item_id
# model_dump() → {"item_id": ...}
# model_dump(by_alias=True, exclude_none=True) → {"itemId": ...}Do not use model_config = ConfigDict(...) or model_config = SettingsConfigDict(...). Pass configuration options as keyword arguments to the base class instead.
# Good
class Item(BaseModel, extra="allow", populate_by_name=True, alias_generator=to_camel): ...
class Settings(BaseSettings, case_sensitive=False): ...
# Bad
class Item(BaseModel):
model_config = ConfigDict(extra="allow")Each scenario defines a Repository class in repository.py that owns all boto3 DynamoDB calls. The Handler class calls repository methods rather than calling boto3 directly. Tests mock the Repository instance rather than patching boto3.resource.
from boto3 import resource
class Repository:
def __init__(self, table_name: str) -> None:
self._table = resource("dynamodb").Table(table_name)
def get_item(self, item_id: str) -> dict | None:
return self._table.get_item(Key={"id": item_id}).get("Item")
def put_item(self, item: dict) -> None:
self._table.put_item(Item=item)Do not add unnecessary imports like from __future__ import annotations. Always use explicit from x import y form:
from json import dumps, loads
from pytest import fixture, main, raises
from aws_cdk.aws_lambda import Code, Function, RuntimeEvery test file must end with:
if __name__ == "__main__":
main()- Always move all data models to a separate
models.pyfile within the template directory.
- Always use AWS Lambda Powertools event types for parsing and type hinting incoming events.
- Use a separate
queue.pymodule with aQueueclass to encapsulate all SQS interactions (initialization, publishing, etc.).
- Create a
Handlerclass with ahandle_record(or similar) method. - Pass the
Handlerinstance to the main Lambda entry point. - Decorate handler methods with
@tracer.capture_method. - For batch events (SQS, DynamoDB Streams), use the Lambda Powertools
BatchProcessor. - For S3 events, manually iterate over records and raise an exception if any record fails to ensure the entire batch is retried by the S3 event source.
- The main Lambda entry point should be named
main.