Skip to content

✨ Repository.open() unified entry point with namespace-first UX and auto-provision #404

Description

@sodre

Problem or Use Case

The current Repository API has three entry points with overlapping concerns:

  1. Repository.connect(name, region, namespace=...) — connect to existing infrastructure
  2. Repository.builder(name, region).namespace(...).build() — provision + connect
  3. Repository(name, region, ...) — deprecated direct constructor

This creates onboarding friction:

  • Three competing paths — developers don't know which to use
  • Namespace is buried as a keyword arg or builder method, but it's the most important parameter for most users
  • No auto-provision — users must know whether infrastructure exists before choosing connect() vs builder()
  • Version check skipped for local endpoints — blunt workaround instead of checking the version record

Proposed Solution

1. Repository.open() — the one path

Namespace is the primary positional argument. Stack name defaults to "zae-limiter" and is rarely needed:

# Most users
repo = await Repository.open("my-app")

# Multi-tenant
repo_alpha = await Repository.open("tenant-alpha")
repo_beta = await Repository.open("tenant-beta")

# Explicit stack
repo = await Repository.open("my-app", stack="custom-stack", region="eu-west-1")

# Absolute simplest (stack="zae-limiter", namespace="default")
repo = await Repository.open()

# LocalStack
repo = await Repository.open("my-app", endpoint_url="http://localhost:4566")

Signature:

@classmethod
async def open(
    cls,
    namespace: str = "default",
    *,
    stack: str | None = None,
    region: str | None = None,
    endpoint_url: str | None = None,
    config_cache_ttl: int = 60,
    auto_update: bool = True,
) -> "Repository":

Stack resolution: stack arg → ZAE_LIMITER_STACK env var → "zae-limiter"

Auto-provision behavior (always, no flag):

  1. Try to resolve namespace → done (fast path)
  2. InfrastructureNotFoundError → deploy stack with defaults, register namespace
  3. NamespaceNotFoundError → register namespace, resolve
  4. Version check + auto-update (always, including local endpoints)

2. Fully fluent builder() — zero args

For power users who need custom infrastructure options (permission boundary, Lambda memory, etc.):

# Minimal
repo = await Repository.builder().namespace("my-app").build()

# Enterprise
repo = await (
    Repository.builder()
    .stack("custom-stack")
    .region("us-east-1")
    .namespace("my-app")
    .permission_boundary("arn:aws:iam::aws:policy/PowerUserAccess")
    .role_name_format("PowerUserPB-{}")
    .policy_name_format("PowerUserPB-{}")
    .lambda_memory(512)
    .build()
)

New builder methods: .stack(), .region(), .endpoint_url() (previously positional args).

Defaults mirror open(): stack from env var or "zae-limiter", namespace "default".

3. Remove local endpoint special-casing

Current: if not endpoint_url: guards around version check and Lambda auto-update.

Fix: Remove all endpoint_url guards. The version record already handles this:

  • lambda_version: None → no Lambda → requires_lambda_update = False (already works)
  • Version check runs everywhere (AWS, LocalStack, any endpoint)

4. connect() removal

connect() was added in #381 and has not been released. No deprecation needed — just remove it and replace with open().

Migration path

Before After
Repository.connect("app", "us-east-1") Repository.open(stack="app")
Repository.connect("app", "us-east-1", namespace="x") Repository.open("x", stack="app")
Repository.builder("app", "us-east-1").build() Repository.open(stack="app")
Repository.builder("app", "us-east-1").namespace("x").build() Repository.open("x", stack="app")
Repository.builder("app", "us-east-1").lambda_memory(512).build() Repository.builder().stack("app").lambda_memory(512).build()

Acceptance Criteria

Repository.open()

  • open(namespace="default", *, stack=None, region=None, endpoint_url=None, config_cache_ttl=60, auto_update=True) exists
  • Stack resolved from: stack arg → ZAE_LIMITER_STACK env var → "zae-limiter"
  • Auto-provisions infrastructure with default StackOptions if table doesn't exist
  • Auto-registers namespace if not found
  • Version check + Lambda auto-update runs always (no endpoint_url guard)
  • SyncRepository.open() generated with matching signature

Repository.builder()

  • builder() takes zero args
  • New chain methods: .stack(), .region(), .endpoint_url()
  • Defaults mirror open(): stack from env/default, namespace "default"
  • SyncRepositoryBuilder generated

Cleanup

  • connect() removed from Repository (unreleased, no deprecation)
  • connect() removed from SyncRepository (generated)
  • repository.py:220-221 — remove if not endpoint_url guard on version check
  • repository.py:1106 — remove not self.endpoint_url guard on Lambda update
  • repository.py:1146 — remove can_auto_update=not self.endpoint_url
  • __init__.py module docstring updated with open() examples
  • CLAUDE.md updated: open() as recommended, remove "skip for local" references, remove .enable_aggregator(False) from LocalStack example
  • Constructor deprecation message updated to point to open()

Tests

  • Unit tests for open() fast path (infra + namespace exist)
  • Unit tests for open() auto-provision (infra missing)
  • Unit tests for open() auto-register namespace (infra exists, namespace missing)
  • Unit tests for stack name resolution (arg → env var → default)
  • Unit tests for builder zero-arg construction with .stack(), .region(), .endpoint_url()
  • Unit tests for version check running on local endpoints
  • Existing connect() tests migrated to open()

Documentation

  • docs/getting-started.md updated with open() as primary entry point
  • docs/api/repository.md updated: open() reference, builder zero-arg signature
  • docs/api/index.md updated with open() examples
  • docs/guide/basic-usage.md updated with open() examples
  • docs/infra/deployment.md updated: open() vs builder() guidance, remove connect()
  • docs/infra/production.md updated with open() multi-tenant examples
  • docs/contributing/localstack.md updated: remove aggregator special-casing
  • docs/performance.md updated if it references connect() or builder(name, region)

Metadata

Metadata

Assignees

Labels

Fields

No fields configured for Feature.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions