Skip to content

✨ Add Repository.connect() factory classmethod as recommended entry point #381

Description

@sodre

Problem or Use Case

After #375 introduces the builder pattern (Repository.builder(...).build()), the most common use case — connecting to an existing table — requires two chained calls:

# Current (post-#375): builder pattern for everything
repo = await Repository.builder("my-app", region="us-east-1").build()
limiter = RateLimiter(repository=repo)

# With namespace
repo = await Repository.builder("my-app", region="us-east-1").namespace("tenant-a").build()

This is verbose for the 80% case (connect to existing infrastructure, no stack creation). A connect() classmethod provides a more Pythonic, one-call entry point:

# Proposed: connect() for the common case
repo = await Repository.connect("my-app", region="us-east-1")
limiter = RateLimiter(repository=repo)

# With namespace
repo = await Repository.connect("my-app", region="us-east-1", namespace="tenant-a")

The builder remains available for advanced/programmatic use (infrastructure provisioning, custom IAM, Lambda configuration), but connect() becomes the recommended API for documentation and tutorials.

Proposed Solution

Repository.connect() / SyncRepository.connect()

@classmethod
async def connect(
    cls,
    name: str,
    region: str | None = None,
    *,
    endpoint_url: str | None = None,
    namespace: str = "default",
    config_cache_ttl: int = 60,
    auto_update: bool = True,
) -> "Repository":
    """Connect to existing zae-limiter infrastructure.

    This is the recommended entry point for most applications.
    For infrastructure provisioning, use Repository.builder() instead.
    """
    repo = cls(
        name=name,
        region=region,
        endpoint_url=endpoint_url,
        config_cache_ttl=config_cache_ttl,
        _skip_deprecation_warning=True,
    )
    repo._auto_update = auto_update

    # Resolve namespace (must already exist)
    namespace_id = await repo._resolve_namespace(namespace)
    if namespace_id is None:
        raise NamespaceNotFoundError(namespace)

    repo._namespace_id = namespace_id
    repo._namespace_name = namespace
    repo._reinitialize_config_cache(namespace_id)

    # Version check (skip for local endpoints)
    if not endpoint_url:
        if auto_update:
            await repo._check_and_update_version_auto()
        else:
            await repo._check_version_strict()

    repo._builder_initialized = True
    return repo

connect() is a thin classmethod that resolves the namespace and performs a version check — it does not provision infrastructure or register namespaces.

Migration scope

All documentation examples, test fixtures, and CLAUDE.md references should migrate from direct __init__ or builder to connect():

Location Current pattern New pattern
CLAUDE.md examples Repository(name=..., region=...) await Repository.connect(...)
docs/ guides Repository(name=..., region=...) await Repository.connect(...)
Unit test fixtures Repository(name=..., region=...) await Repository.connect(...) or builder
E2E test fixtures Repository(name=..., region=...) await Repository.connect(...)
Benchmark fixtures Repository(name=..., region=...) await Repository.connect(...)
Doctests Repository(name=..., region=...) await Repository.connect(...)

Direct __init__ deprecation

Repository.__init__() emits a DeprecationWarning directing users to connect() or builder(). The constructor remains functional for backward compatibility but is no longer the documented path.

When to use which

Pattern Use case
Repository.connect(...) Connect to existing table (80% case)
Repository.builder(...).build() Provision infrastructure, custom IAM, advanced config
Repository(...) Deprecated — backward compatibility only

Alternatives Considered

  1. Only builder, no connect(): Rejected — builder().build() is verbose for the common case and unfamiliar to Python developers who expect factory classmethods (e.g., datetime.fromtimestamp(), Path.cwd())
  2. Rename builder to connect(): Rejected — the builder pattern is needed for the infrastructure provisioning use case where multiple configuration options are chained
  3. Keep init as recommended: Rejected — __init__ cannot do async initialization (namespace resolution, version check), forcing a two-step pattern

Acceptance Criteria

connect() classmethod

  • Repository.connect(name, region, *, endpoint_url, namespace, config_cache_ttl, auto_update) is an async classmethod
  • SyncRepository.connect(...) is a sync classmethod (generated via hatch run generate-sync)
  • connect() has its own thin implementation (resolves namespace, version check; no infrastructure provisioning)
  • connect() returns a fully initialized Repository (namespace resolved, version checked)
  • connect() accepts namespace parameter (default: "default")
  • connect() docstring directs users to builder() for infrastructure provisioning

Deprecation

  • Repository.__init__() emits DeprecationWarning with message directing to connect() or builder()
  • SyncRepository.__init__() emits same DeprecationWarning
  • Deprecation warning includes the replacement pattern in the message text

Documentation migration

  • CLAUDE.md "Repository Pattern" section uses connect() as primary example
  • CLAUDE.md "Declarative Infrastructure" section uses builder() for stack provisioning
  • docs/getting-started.md uses connect() for first example
  • docs/guide/basic-usage.md uses connect()
  • docs/infra/deployment.md uses builder() for provisioning, connect() for runtime
  • All other docs pages updated to use connect() where direct __init__ is currently shown (remaining occurrences are in historical plans, ADRs, and migration scripts)

Test migration

  • Unit test fixtures in tests/unit/ use connect() or builder instead of direct __init__
  • E2E test fixtures in tests/e2e/ use connect() or builder
  • Benchmark fixtures in tests/benchmark/ use connect() or builder (AWS fixtures use builder; moto fixtures use _skip_deprecation_warning)
  • No test uses Repository(name=..., region=...) directly (grep verification: grep -rn "Repository(" tests/ --include="*.py" returns only connect() or builder() calls)

Sync parity

  • SyncRepository.connect() generated via hatch run generate-sync, produces no diff
  • Sync tests use SyncRepository.connect() pattern

Tests for connect()

  • Unit test verifies connect() returns initialized Repository
  • Unit test verifies connect() passes namespace to builder
  • Unit test verifies connect() passes config_cache_ttl to builder
  • Unit test verifies connect() passes auto_update to builder
  • Unit test verifies Repository.__init__() emits DeprecationWarning
  • pytest tests/unit/ passes
  • mypy src/zae_limiter/ passes with no new errors

Dependencies

Metadata

Metadata

Assignees

Labels

api-designAPI surface changesarea/limiterCore rate limiting logicdocumentationImprovements or additions to documentationtestingTest coverage

Fields

No fields configured for Feature.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions