Skip to content

Latest commit

 

History

History
executable file
·
208 lines (139 loc) · 7.25 KB

File metadata and controls

executable file
·
208 lines (139 loc) · 7.25 KB

AGENTS.md

This file provides guidance for AI agents working in this repository.

Project Overview

Python AWS Lambda project template using mise for tool management and uv for dependency management.

Setup

mise run install       # install dependencies
mise run precommit     # install pre-commit hooks

Common Commands

mise 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

Code Style

  • Line length: 120 characters (enforced by ruff)
  • Linting rules: E (pycodestyle errors) and I (isort) via ruff
  • Type checking: pyright in strict mode
  • All code must pass mise run lint before committing (enforced by pre-commit hooks)

Testing

Tests live in tests/. Run with:

mise run test

Coverage is measured with coverage and reported to stdout and coverage.xml. The source under test is templates/.

Project Structure

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

Initializing the Template

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"

Dependencies

  • Always use uv for dependency management (uv add <package>)
  • Use Pydantic for data models
  • Use Pydantic-settings for environment variable configuration in a settings.py file
  • Use AWS Lambda Powertools wherever applicable: logger, tracer, metrics, parameters, event types, event handlers, etc.

Infrastructure

  • Define infrastructure using AWS CDK under the infra/ folder.
  • Synthesize CloudFormation templates: mise run infra:synth [stack] (alias synth [stack], s [stack]).
  • Deploy to AWS: mise run deploy <stack> (alias d <stack>).
  • Deploy locally using LocalStack: copy .env.local.example to .env.local and set token from app.localstack.cloud, then mise run local:deploy <stack> (alias local <stack>, dl <stack>).
  • Destroy locally from LocalStack: mise run local:destroy <stack> (alias Dl <stack>).

Testing Guidelines

  • Use pytest, not unittest
  • Use pytest monkeypatch and pytest-mock for mocking instead of unittest.MagicMock
  • Use moto.mock_aws for 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

Mise Tasks

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.

Notes

  • Python 3.14+ required
  • Dependencies are managed via pyproject.toml and locked in uv.lock
  • Tooling is managed via mise.toml

Coding Conventions

Field descriptions

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.")

camelCase alias convention

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": ...}

No model_config class attribute

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")

Repository pattern for DynamoDB access

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)

Import style

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, Runtime

Test file main block

Every test file must end with:

if __name__ == "__main__":
    main()

Data Models

  • Always move all data models to a separate models.py file within the template directory.

Event Types

  • Always use AWS Lambda Powertools event types for parsing and type hinting incoming events.

SQS Interactions

  • Use a separate queue.py module with a Queue class to encapsulate all SQS interactions (initialization, publishing, etc.).

Handler Structure

  • Create a Handler class with a handle_record (or similar) method.
  • Pass the Handler instance 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.

Lambda Entry Point

  • The main Lambda entry point should be named main.