Skip to main content

Local development gateway for AWS SAM Lambda functions with FastAPI + Swagger UI

Project description

local-lambda-playground

Local development gateway for AWS SAM Lambda functions.
Reads your template.yml, discovers every HTTP route automatically, starts a FastAPI server with Swagger UI, assumes IAM roles, and hot-reloads on every file change — all from a single CLI command.

llp start --all
╭──────────────────────────────────────────────────────────────────────────────╮
│                          Registered Routes                                   │
├────────────┬────────┬───────────────────────────────┬────────────────────────┤
│ Group      │ Method │ Path                          │ IAM Role               │
├────────────┼────────┼───────────────────────────────┼────────────────────────┤
│ buyer-api  │ POST   │ /api/v1/auth/signup           │ ambient                │
│ buyer-api  │ POST   │ /api/v1/auth/login            │ ambient                │
│ buyer-api  │ POST   │ /api/v1/auth/refresh          │ ambient                │
│ buyer-api  │ POST   │ /api/v1/otp/send              │ ambient                │
│ seller-api │ POST   │ /api/v1/seller/register       │ agarmart-seller-role   │
│ admin-api  │ POST   │ /api/v1/admin/auth/login      │ agarmart-admin-role    │
╰────────────┴────────┴───────────────────────────────┴────────────────────────╯

Swagger UI:   http://localhost:8000/docs
ReDoc:        http://localhost:8000/redoc

Table of Contents

  1. What Is local-lambda-playground?
  2. Why Use It?
  3. How It Compares to sam local start-api
  4. Requirements
  5. Installation
  6. Quick Start
  7. CLI Commands
  8. Filtering & Targeting Functions
  9. IAM Role Support
  10. Auth Mock
  11. Environment Variables & Configuration
  12. Hot Reload
  13. Swagger UI & API Documentation
  14. Dependency Isolation
  15. How It Works Internally
  16. Project Structure
  17. Testing
  18. Troubleshooting

What Is local-lambda-playground?

llp is a developer tool that makes it fast and painless to run AWS SAM Lambda functions on your local machine — without Docker, without SAM CLI, and without slow cold starts.

It reads your template.yml, discovers all AWS::Serverless::Function resources that have API Gateway events, and dynamically registers them as routes in a live FastAPI web server. Every route is immediately browsable in Swagger UI, hot-reloads when you change any source file, and runs with the exact same IAM permissions your function would have in production.


Why Use It?

The problem with sam local start-api

  • Slow — spins up a Docker container for every request (3–10 seconds cold start)
  • Opaque — no Swagger UI, no structured request/response logging
  • Brittle — Docker dependency, port conflicts, environment variable setup is tedious
  • No IAM — cannot assume real IAM roles locally

What local-lambda-playground gives you

Benefit Detail
Instant startup Python import, no Docker — functions start in milliseconds
📖 Auto Swagger UI Every route documented at /docs the moment the server starts
🔄 Hot reload Edit any handler .py or template.yml — server reloads automatically
🔑 Real IAM roles Calls STS AssumeRole using the Role: ARN from your template
🧪 Auth mock Bypass JWT auth in local testing with --mock-user-id
🌿 Smart filtering Run only buyer-api, or only Buyer* functions, or everything except Admin*
📦 Zero extra config Points at your existing template.yml — nothing else needed
🗂 Grouped Swagger Routes are tagged by folder group (buyer-api, seller-api, admin-api)
🔒 Credential safety IAM credentials injected per-invocation and cleaned up immediately after

How It Compares to sam local start-api

Capability llp sam local start-api
Startup speed < 1 second 5–30 seconds (Docker pull + container)
Requires Docker No Yes
Swagger UI Yes — auto-generated No
Route grouping in docs Yes — by folder No
Hot reload Yes — uvicorn + watchdog No (manual restart)
IAM role assumption Yes — STS AssumeRole No
Auth mock Yes — --mock-user-id No
Per-function env vars Yes — env.json per logical ID Yes — env.json
CF intrinsic handling Graceful (strips/resolves) Full CloudFormation engine
Parity with Lambda runtime High (same Python interpreter) Exact (Docker image match)
Offline usage Yes Yes (with container cached)

Rule of thumb: Use llp during active development for fast feedback loops.
Use sam local for final integration testing before deployment.


Requirements

  • Python 3.10+
  • AWS credentials configured (only needed for IAM role assumption — optional)

Installation

From PyPI

pip install local-lambda-playground

From source (development mode)

cd lambda-local/
pip install -e ".[dev]"

Verify

llp --help

Quick Start

# 1. Navigate to your SAM project
cd AgarMart-serverless-api/

# 2. Create a local env config (copy from example)
cp .env.example .env

# Fill in local values:
# MONGODB_URI=mongodb://localhost:27017
# REDIS_URL=redis://localhost:6379
# JWT_SECRET=my-dev-secret

# 3. Start ALL Lambda functions
llp start --all

# 4. Open Swagger UI
open http://localhost:8000/docs

That's it. Every route from your template.yml is live and documented.


CLI Commands

start — Launch the gateway

llp start [OPTIONS]
Option Short Default Description
--all -a false Run ALL functions (ignores other filters)
--function NAME -f Include only this function (repeatable)
--group NAME -g Include functions in this folder group (repeatable)
--match PATTERN -m Glob pattern for logical IDs e.g. Buyer* (repeatable)
--exclude PATTERN -e Glob pattern to exclude e.g. Admin* (repeatable)
--template PATH -t template.yml Path to SAM template
--host HOST 127.0.0.1 Bind address
--port PORT 8000 Bind port
--reload / --no-reload --reload Enable / disable hot reload
--env-file PATH auto Path to .env file
--env-json PATH auto Path to env.json
--mock-user-id ID Inject a fake user into requestContext.authorizer
--mock-role ROLE buyer Role for the mock user
--mock-email EMAIL dev@localhost Email for the mock user
--role-arn ARN Assume this IAM role for ALL functions
--no-role false Skip role assumption; use ambient AWS credentials

list — Print all function IDs

llp list [--template PATH]

Lists every AWS::Serverless::Function logical ID in the template (alphabetically sorted).

AdminAuthFunction
AdminCategoriesFunction
BuyerAuthFunction
BuyerOtpFunction
SellerAuthFunction
SellerProductsFunction
...

routes — Print all HTTP routes

llp routes [--template PATH] [--group NAME]
 Method  Path                              Group       Handler
 POST    /api/v1/auth/signup               buyer-api   auth/handler.lambda_handler
 POST    /api/v1/auth/login                buyer-api   auth/handler.lambda_handler
 GET     /api/v1/buyer/profile             buyer-api   profile/handler.lambda_handler
 POST    /api/v1/seller/register           seller-api  auth/handler.lambda_handler
 GET     /api/v1/seller/products           seller-api  products/handler.lambda_handler
 POST    /api/v1/admin/auth/login          admin-api   auth/handler.lambda_handler

Filter by group:

llp routes --group seller-api

invoke — Call a function directly

llp invoke FUNCTION_ID [OPTIONS]
Option Description
--event PATH JSON file with the full Lambda event payload
--template PATH Path to SAM template
--env-json PATH Path to env.json
--role-arn ARN Assume this IAM role before invoking
--no-role Skip role assumption

Basic invoke (event auto-built from route metadata):

llp invoke BuyerAuthFunction

Invoke with a custom event file:

llp invoke BuyerAuthFunction --event events/login.json

Example events/login.json:

{
  "httpMethod": "POST",
  "path": "/api/v1/auth/login",
  "headers": { "Content-Type": "application/json" },
  "body": "{\"email\": \"user@example.com\", \"password\": \"secret123\"}",
  "queryStringParameters": null,
  "pathParameters": null,
  "requestContext": {
    "stage": "local",
    "identity": { "sourceIp": "127.0.0.1" },
    "authorizer": {}
  }
}

Output:

{
  "statusCode": 200,
  "headers": { "Content-Type": "application/json" },
  "body": {
    "success": true,
    "data": {
      "access_token": "eyJ...",
      "refresh_token": "eyJ...",
      "token_type": "Bearer"
    }
  }
}

install — Set up per-function venvs

llp install [OPTIONS]
Option Description
--group NAME Install only this group (repeatable)
--force Recreate venvs from scratch
--template PATH Path to SAM template

Creates an isolated virtual environment for each function group:

.lambda-local/
├── buyer-api/    ← buyer-api/requirements.txt installed here
├── seller-api/
└── admin-api/
llp install           # all groups
llp install -g buyer-api
llp install --force   # rebuild from scratch

Add .lambda-local/ to your .gitignore.


Filtering & Targeting Functions

Run everything

llp start --all

Single function

llp start -f BuyerAuthFunction

Multiple specific functions

llp start -f BuyerAuthFunction -f BuyerOtpFunction

By folder group

The group comes from the first segment of CodeUri:

BuyerAuthFunction:
  Properties:
    CodeUri: buyer-api/auth/   # group = "buyer-api"
llp start --group buyer-api
llp start --group buyer-api --group seller-api

Wildcard pattern

llp start --match "Buyer*"      # everything starting with Buyer
llp start --match "*Auth*"      # everything containing Auth
llp start --match "Admin*"      # all admin functions

Exclude

llp start --all --exclude "Admin*"
llp start --all --exclude "*Job*"

Combine

# buyer-api but not the social auth endpoint
llp start --group buyer-api --exclude "BuyerSocialAuthFunction"

IAM Role Support

In production, each Lambda function runs under an IAM execution role:

BuyerAuthFunction:
  Type: AWS::Serverless::Function
  Properties:
    Role: arn:aws:iam::123456789012:role/agarmart-buyer-role
    # or with CloudFormation Sub:
    Role: !Sub 'arn:aws:iam::${AWS::AccountId}:role/agarmart-${StageName}'

llp replicates this by calling STS AssumeRole before each function invocation and injecting the temporary credentials as environment variables — so your function can call S3, SQS, DynamoDB, or any AWS service with real permissions.

How credentials are resolved

Request arrives
      │
      ▼
  Read role_arn from FunctionRoute
  (extracted from template.yml Role: property)
      │
      ▼
  --role-arn override? → use that instead
      │
      ▼
  Resolve CloudFormation Sub variables:
    ${AWS::AccountId}  →  real account ID via STS GetCallerIdentity
    ${AWS::Region}     →  boto3 default region
    ${AWS::Partition}  →  aws
      │
      ▼
  Check credential cache (cached for 1 hour)
  Cache hit → use cached credentials
  Cache miss → STS AssumeRole(...)
      │
      ▼
  Inject into environment:
    AWS_ACCESS_KEY_ID     = <temp>
    AWS_SECRET_ACCESS_KEY = <temp>
    AWS_SESSION_TOKEN     = <temp>
      │
      ▼
  Call lambda_handler(event, context)
      │
      ▼
  Restore original credentials

CLI options

# Per-function roles from template.yml (default behaviour)
llp start --all

# Override: assume one role for ALL functions
llp start --all \
  --role-arn arn:aws:iam::123456789012:role/my-dev-role

# Skip role assumption entirely
llp start --all --no-role

# Set via environment variable (persists across reloads)
export LAMBDA_LOCAL_ROLE_ARN=arn:aws:iam::123456789012:role/my-dev-role
llp start --all

# invoke command also supports role options
llp invoke BuyerAuthFunction \
  --role-arn arn:aws:iam::123456789012:role/agarmart-dev

CloudFormation Sub variables resolved

Variable Resolved to
${AWS::AccountId} Real account ID via sts:GetCallerIdentity
${AWS::Region} boto3 default region or AWS_REGION env var
${AWS::Partition} aws
${StageName} (custom) Stripped (empty string — override with --role-arn)

Required developer permissions

To assume a role, your IAM user/role needs:

{
  "Effect": "Allow",
  "Action": "sts:AssumeRole",
  "Resource": "arn:aws:iam::123456789012:role/agarmart-*"
}

And the target role must list your IAM principal in its Trust Policy.

Graceful fallback

If AssumeRole fails (no permission, unresolvable ARN, no AWS credentials), llp logs a warning and continues with ambient credentials. The server never crashes on a role assumption failure. Use --no-role to suppress the warning.


Auth Mock

Most Lambda functions check event["requestContext"]["authorizer"] for user identity (injected by the Lambda Authorizer in production). In local dev you don't want to generate real JWTs.

Use --mock-user-id to inject a fake user into every request:

llp start --all \
  --mock-user-id user_abc123 \
  --mock-role buyer \
  --mock-email test@example.com

Every request will contain:

{
  "requestContext": {
    "authorizer": {
      "user_id": "user_abc123",
      "role":    "buyer",
      "email":   "test@example.com"
    }
  }
}

Your handler's get_current_user(event) succeeds without any JWT validation.

Test different roles simultaneously:

# Terminal 1 — buyer on port 8001
llp start --group buyer-api --port 8001 \
  --mock-user-id buyer_001 --mock-role buyer

# Terminal 2 — admin on port 8002
llp start --group admin-api --port 8002 \
  --mock-user-id admin_001 --mock-role admin

Environment Variables & Configuration

Auto-detection

llp scans the template's directory automatically:

AgarMart-serverless-api/
├── template.yml
├── .env.local       ← checked first
├── .env             ← checked second
└── env.json         ← checked third

Explicit paths

llp start --env-file .env.staging --env-json env.staging.json

.env format

MONGODB_URI=mongodb://localhost:27017
MONGODB_DB_NAME=agarmart
REDIS_URL=redis://localhost:6379
JWT_SECRET=dev-only-secret-256-bits
JWT_ACCESS_EXPIRE_MINUTES=15
JWT_REFRESH_EXPIRE_DAYS=30
AWS_S3_BUCKET=agarmart-dev-media
AWS_REGION=ap-south-1
CDN_BASE_URL=https://cdn.innobuz.com/app
BREVO_API_KEY=xkeysib-...
FAST2SMS_API_KEY=...
GOOGLE_CLIENT_ID=...
SQS_EMAIL_QUEUE_URL=https://sqs.ap-south-1.amazonaws.com/123456789012/agarmart-email-dev
SQS_SMS_QUEUE_URL=https://sqs.ap-south-1.amazonaws.com/123456789012/agarmart-sms-dev
SQS_PUSH_QUEUE_URL=https://sqs.ap-south-1.amazonaws.com/123456789012/agarmart-push-dev

env.json — SAM Parameters format

{
  "Parameters": {
    "MONGODB_URI": "mongodb://localhost:27017",
    "JWT_SECRET": "dev-secret",
    "REDIS_URL": "redis://localhost:6379"
  }
}

env.json — Per-function format

Different values per Lambda function:

{
  "BuyerAuthFunction": {
    "MONGODB_URI": "mongodb://localhost:27017/buyer_test"
  },
  "AdminAuthFunction": {
    "MONGODB_URI": "mongodb://localhost:27017/admin_test",
    "JWT_SECRET": "admin-specific-secret"
  }
}

CloudFormation intrinsic handling

template.yml often contains SSM references that can't be resolved locally:

JWT_SECRET: !Sub '{{resolve:ssm:/agarmart/prod/jwt_secret}}'

llp strips these automatically and replaces them with empty strings — the server starts without errors. Override with real local values in your .env.

Priority order (highest wins)

1. Actual process environment (export MY_VAR=x before starting)
2. --env-file / .env.local / .env
3. --env-json / env.json (Parameters key)
4. Per-function env.json block

Hot Reload

Hot reload is on by default. The server watches:

File change Effect
Any **/*.py handler file Module reloaded; next request uses new code
template.yml Routes re-discovered; Swagger UI updates
env.json / .env Environment re-loaded
requirements.txt No auto-install; run llp install then restart
# Reload is on by default
llp start --all

# Disable (useful in CI)
llp start --all --no-reload

Swagger UI & API Documentation

URL Interface
http://localhost:8000/docs Swagger UI — interactive API explorer
http://localhost:8000/redoc ReDoc — clean reference docs
http://localhost:8000/openapi.json Raw OpenAPI 3.0 schema

Routes are tagged by group (folder prefix) so Swagger UI shows:

► buyer-api
    POST  /api/v1/auth/signup
    POST  /api/v1/auth/login
    GET   /api/v1/buyer/profile

► seller-api
    POST  /api/v1/seller/register
    GET   /api/v1/seller/products

► admin-api
    POST  /api/v1/admin/auth/login
    GET   /api/v1/admin/dashboard/stats

If you started with --mock-user-id, protected routes work directly in the Swagger UI without needing a Bearer token.


How It Works Internally

llp start --all
        │
        ▼
┌─────────────────────────────────────────────────────────┐
│  CLI  (cli.py)                                          │
│  Parse args → preview table → set LAMBDA_LOCAL_* env   │
│  → start uvicorn with factory=True reload=True          │
└────────────────────┬────────────────────────────────────┘
                     │ on every (re)start
                     ▼
┌─────────────────────────────────────────────────────────┐
│  Gateway  (gateway.py)  create_app()                    │
│  Read LAMBDA_LOCAL_* env vars                           │
│  → discover_routes()                                    │
│  → detect Lambda layer paths (shared/python/)           │
│  → create FastAPI app with Swagger tags                 │
│  → register one route per HTTP event                    │
└────────────────────┬────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────┐
│  Discovery  (discovery.py)                              │
│  CF-aware YAML loader (handles !Ref, !Sub, !GetAtt)     │
│  Extract: logical_id handler code_uri path method       │
│           group role_arn                                │
│  Apply --function / --group / --match / --exclude       │
└─────────────────────────────────────────────────────────┘

HTTP request arrives
        │
        ▼
┌─────────────────────────────────────────────────────────┐
│  Route handler closure  (gateway.py)                    │
│  → Invoker.invoke(route, request)                       │
└────────────────────┬────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────┐
│  Invoker  (invoker.py)                                  │
│  Build API Gateway event dict from FastAPI request      │
│  Submit to ThreadPoolExecutor                           │
│  (asyncio.run() inside handlers is safe in threads)     │
└────────────────────┬────────────────────────────────────┘
                     │ in thread
                     ▼
┌─────────────────────────────────────────────────────────┐
│  _invoke_in_thread                                      │
│                                                         │
│  1. Inject per-function env vars (env.json)             │
│                                                         │
│  2. IAM credential injection  (iam.py)                  │
│     • Resolve ${AWS::AccountId} etc. in role ARN        │
│     • STS AssumeRole (cached 1 hr, refresh 5 min early) │
│     • Set AWS_ACCESS_KEY_ID / SECRET / SESSION_TOKEN    │
│                                                         │
│  3. sys.path: prepend code_uri + layer paths            │
│                                                         │
│  4. importlib.util.spec_from_file_location              │
│     unique module key per logical_id                    │
│     (no naming conflicts across handlers)               │
│                                                         │
│  5. handler_fn(event, context)                          │
│                                                         │
│  6. Restore IAM credentials + env vars                  │
└─────────────────────────────────────────────────────────┘

Project Structure

lambda-local/
├── pyproject.toml              ← packaging, deps, tool config (Ruff, MyPy, pytest)
├── README.md
│
├── lambda_local/
│   ├── __init__.py
│   ├── cli.py                  ← Typer CLI (start, list, routes, invoke, install)
│   ├── config.py               ← .env / env.json loading + CF intrinsic stripping
│   ├── discovery.py            ← SAM template parser + FunctionRoute builder
│   ├── gateway.py              ← FastAPI app factory (create_app)
│   ├── iam.py                  ← STS AssumeRole + credential caching + injection
│   ├── installer.py            ← Per-group venv creator (llp install)
│   ├── invoker.py              ← Lambda handler runner (thread pool executor)
│   ├── models.py               ← Pydantic models: FunctionRoute, InvocationResult
│   └── watcher.py              ← watchdog FileWatcher (debounced 500ms)
│
└── tests/
    ├── conftest.py             ← Fixtures: templates, handler files
    ├── test_discovery.py       ← Route discovery + filtering
    ├── test_invoker.py         ← Event building + handler invocation
    ├── test_gateway.py         ← FastAPI app + HTTP integration tests
    └── test_cli.py             ← CLI command tests (Typer TestClient)

Testing

pip install -e ".[dev]"

# Full suite with coverage
pytest

# Coverage HTML report
pytest --cov=lambda_local --cov-report=html
open htmlcov/index.html

# Specific file
pytest tests/test_discovery.py -v

# Single test
pytest tests/test_invoker.py::TestBuildEvent::test_mock_user_injected -v

Target coverage: > 90% (enforced by pytest config).


Troubleshooting

requires a different Python: 3.x not in '>=3.10'

Switch to Python 3.10+:

pyenv install 3.12.0 && pyenv local 3.12.0
pip install local-lambda-playground

Template not found: template.yml

Run from the directory containing your template, or pass --template:

llp start --all --template /path/to/AgarMart-serverless-api/template.yml

Handler file not found

Check that CodeUri is correct relative to template.yml and that Handler matches the actual filename:

CodeUri: buyer-api/auth/     # directory must exist
Handler: handler.lambda_handler  # buyer-api/auth/handler.py must exist

ImportError: No module named 'agarmart'

The shared layer isn't on the Python path. Declare it in template.yml:

SharedLayer:
  Type: AWS::Serverless::LayerVersion
  Properties:
    ContentUri: shared/   # shared/python/ is added to sys.path automatically

Or install it directly:

pip install -r shared/requirements.txt

ModuleNotFoundError: No module named 'motor'

Install the function's runtime deps:

pip install -r buyer-api/requirements.txt
pip install -r shared/requirements.txt

Or use the isolation approach:

llp install --group buyer-api

iam_assume_role_failed warning

Causes and fixes:

Cause Fix
No AWS credentials aws configure or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
No sts:AssumeRole permission Add the permission to your IAM user/role
Unresolvable ARN (${StageName} etc.) Pass --role-arn arn:aws:iam::ACCOUNT:role/NAME explicitly

To suppress the warning and use ambient credentials:

llp start --all --no-role

Routes not updating after template edit

  1. Confirm --reload is active (it is by default)
  2. Check you're not in the .lambda-local/ directory (excluded from watching)
  3. Save the file again — some editors write atomically without triggering inotify
  4. Hard restart: Ctrl+Cllp start --all

License

MIT

[pypi] username = innux password = pypi-AgEIcHlwaS5vcmcCJDc5MjE1YTQ5LTEwNWItNDdhMS05MTI4LWFjNzY3MWVlYTFjMAACKlszLCJkMTI3ZGYyNS0wYjZhLTRiNjItODE4Yi1lODI5ODczOGRkMDUiXQAABiApc8NZEWfv0eaHXv1t1DeZv1Yix46pSmTkLPMq9a1CMA

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

local_lambda_playground-0.1.1.tar.gz (42.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

local_lambda_playground-0.1.1-py3-none-any.whl (35.9 kB view details)

Uploaded Python 3

File details

Details for the file local_lambda_playground-0.1.1.tar.gz.

File metadata

  • Download URL: local_lambda_playground-0.1.1.tar.gz
  • Upload date:
  • Size: 42.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.17

File hashes

Hashes for local_lambda_playground-0.1.1.tar.gz
Algorithm Hash digest
SHA256 7184d779957ac49ebdc8b3161c815cb07fe0f161421846010d1753692db28a07
MD5 15221a3eef50621e622dfe741fcd8b64
BLAKE2b-256 567767720fbbba7b64691c1a6d9abb68ca171b88fff48ccadc8255499985779f

See more details on using hashes here.

File details

Details for the file local_lambda_playground-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for local_lambda_playground-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 6d3f5ba624f0244e93d9df816697a6916ba434b5246832ab2c7297055cf56ec3
MD5 a6efeb8f0eb33e79720bf5918cf9473d
BLAKE2b-256 0b52cd96d97078522aab69d78fe199a253e16f22a505bfcef983d4c9d4629773

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page