Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
776029a
📝 docs(limiter): add Repository.open() design document (#404)
sodre Feb 19, 2026
b67b904
✨ feat(repository): add Repository.open() unified entry point (#404)
sodre Feb 20, 2026
8179372
✅ test(repository): add test for open() re-raising non-ResourceNotFou…
sodre Feb 20, 2026
378aef7
♻️ refactor(test): migrate e2e/integration/benchmark tests from conne…
sodre Feb 20, 2026
060e9eb
🐛 fix(test): pass parallel_mode after open() since it's a sync-only c…
sodre Feb 20, 2026
d8ad337
🐛 fix(test): inject parallel_mode into SyncRepository.open() via gene…
sodre Feb 20, 2026
0ebb35b
📝 docs: migrate remaining connect()/builder(args) references to open(…
sodre Feb 20, 2026
90f5068
✅ test(repository): add version check test for local endpoints and fi…
sodre Feb 20, 2026
6eb1712
📝 docs: simplify examples to use Repository.open() without stack/regi…
sodre Feb 20, 2026
390c376
📝 docs: migrate README from connect() to open() and simplify examples…
sodre Feb 20, 2026
b38baab
📝 docs(guide): use builder pattern for per-namespace on_unavailable e…
sodre Feb 20, 2026
038ca1d
📝 docs(infra): use builder pattern for namespace registration in depl…
sodre Feb 20, 2026
2f3f7ef
Revert "📝 docs(infra): use builder pattern for namespace registration…
sodre Feb 20, 2026
2f58d24
📝 docs(infra): mention builder().build() also registers default names…
sodre Feb 20, 2026
7739654
🐛 fix(docs): correct CLI namespace note — open() auto-registers names…
sodre Feb 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 49 additions & 51 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Native sync code is generated from async source via AST transformation (see ADR-
All explicit modes warn (not error) when conditions are suboptimal. Auto mode silently selects the best strategy without warnings. Resolution happens once at `SyncRepository.__init__` time (not per-call). Usage:

```python
repo = SyncRepository.connect("my-app", "us-east-1", parallel_mode="gevent")
repo = SyncRepository.open(parallel_mode="gevent")
limiter = SyncRateLimiter(repository=repo)
```

Expand Down Expand Up @@ -145,33 +145,26 @@ zae-limiter lambda-export --output lambda.zip

### Declarative Infrastructure

Use `Repository.builder()` or CLI to provision infrastructure, then `Repository.connect()` in application code:
Use `Repository.open()` for application code (auto-provisions if needed), or `Repository.builder()` / CLI for enterprise deployments:

```python
from zae_limiter import RateLimiter, Repository

# Application code — connect to existing infrastructure
repo = await Repository.connect("my-app", "us-east-1")
# Application code — open handles everything (auto-provisions if needed)
repo = await Repository.open("my-app")
limiter = RateLimiter(repository=repo)

# Infrastructure provisioning — builder handles infra + namespace in one call
# Enterprise deployment — builder for permission boundaries and custom config
repo = await (
Repository.builder("my-app", "us-east-1")
.namespace("default")
.build()
)

# Enterprise deployment with permission boundary and custom role naming
repo = await (
Repository.builder("my-app", "us-east-1")
Repository.builder()
.permission_boundary("arn:aws:iam::aws:policy/PowerUserAccess")
.role_name_format("pb-{}-PowerUser")
.policy_name_format("pb-{}-PowerUser")
.build()
)
```

Other builder methods: `.lambda_memory()`, `.usage_retention_days()`, `.audit_retention_days()`, `.enable_alarms()`, `.alarm_sns_topic()`, `.enable_audit_archival()`, `.audit_archive_glacier_days()`, `.enable_tracing()`, `.create_iam_roles()`, `.create_iam()`, `.aggregator_role_arn()`, `.enable_deletion_protection()`, `.tags()`.
Other builder methods: `.stack()`, `.region()`, `.endpoint_url()`, `.namespace()`, `.lambda_memory()`, `.usage_retention_days()`, `.audit_retention_days()`, `.enable_alarms()`, `.alarm_sns_topic()`, `.enable_audit_archival()`, `.audit_archive_glacier_days()`, `.enable_tracing()`, `.create_iam_roles()`, `.create_iam()`, `.aggregator_role_arn()`, `.enable_deletion_protection()`, `.tags()`.

**IAM Resource Defaults (ADR-117):**
- **Managed policies** are **created by default** — both table-level (`acq`, `full`, `read`) and namespace-scoped (`ns-acq`, `ns-full`, `ns-read`)
Expand All @@ -180,9 +173,9 @@ Other builder methods: `.lambda_memory()`, `.usage_retention_days()`, `.audit_re
- **Skip all IAM** with `create_iam=False` or `--no-iam` for restricted IAM environments
- **External Lambda role** with `aggregator_role_arn` or `--aggregator-role-arn` to use pre-existing role

**When to use `connect()` vs `builder()` vs CLI:**
- **`connect()`**: Application code at startup, connecting to pre-existing infrastructure
- **`builder().build()`**: Self-contained apps, serverless deployments, minimal onboarding friction
**When to use `open()` vs `builder()` vs CLI:**
- **`open()`**: 90% of users. Application code, prototyping, LocalStack dev. Auto-provisions infrastructure if missing
- **`builder().build()`**: Enterprise deployments needing permission boundaries, custom Lambda config, IAM role naming
- **CLI**: Strict infra/app separation, audit requirements, Terraform/CDK integration

### Local Development with LocalStack
Expand Down Expand Up @@ -312,49 +305,54 @@ src/zae_limiter_aggregator/ # Lambda aggregator (top-level package)

The `Repository` class owns data access and infrastructure management. `RateLimiter` owns business logic.

#### Repository.connect() (Recommended)
#### Repository.open() (Recommended)

Use `Repository.connect()` for application code connecting to existing infrastructure:
Use `Repository.open()` for application code. It auto-provisions infrastructure and registers namespaces as needed:

```python
from zae_limiter import RateLimiter, Repository

# Basic usage — connect to existing infrastructure
repo = await Repository.connect("my-app", "us-east-1")
# Basic usage — namespace defaults via ZAEL_NAMESPACE env var or "default"
# Stack defaults via ZAEL_STACK env var or "zae-limiter"
repo = await Repository.open()
limiter = RateLimiter(repository=repo)

# Explicit namespace (positional arg)
repo = await Repository.open("my-app")
limiter = RateLimiter(repository=repo)

# Multi-tenant — each tenant gets an isolated namespace
repo_alpha = await Repository.connect("my-app", "us-east-1", namespace="tenant-alpha")
repo_alpha = await Repository.open("tenant-alpha")
limiter_alpha = RateLimiter(repository=repo_alpha)

# With custom config cache TTL
repo = await Repository.connect("my-app", "us-east-1", config_cache_ttl=120)
repo = await Repository.open(config_cache_ttl=120)

# LocalStack development
repo = await Repository.connect(
"my-app", "us-east-1",
endpoint_url="http://localhost:4566",
)
repo = await Repository.open(endpoint_url="http://localhost:4566")
```

**`connect()` steps:**
1. Create Repository instance (no deprecation warning)
2. Resolve namespace name to opaque ID (must already exist)
3. Reinitialize config cache with resolved namespace ID
4. Version check and Lambda auto-update (skip for local endpoints)
**`open()` signature:** `Repository.open(namespace, *, stack=..., region=..., endpoint_url=..., config_cache_ttl=...)`
- `namespace`: positional arg, defaults via `ZAEL_NAMESPACE` env var or `"default"`
- `stack`: defaults via `ZAEL_STACK` env var or `"zae-limiter"`

**`connect()` does NOT:** create tables, register namespaces, or provision infrastructure. Raises `NamespaceNotFoundError` if the namespace is not registered.
**`open()` steps:**
1. Try to resolve namespace name to opaque ID
2. If table is missing, deploy stack with defaults
3. If namespace is missing, register it (always registers "default" on new stack)
4. Reinitialize config cache with resolved namespace ID
5. Version check and Lambda auto-update

#### RepositoryBuilder (Infrastructure Provisioning)

Use `Repository.builder()` for infrastructure provisioning (like `terraform deploy`):
Use `Repository.builder()` for enterprise infrastructure provisioning (like `terraform deploy`):

```python
from zae_limiter import RateLimiter, Repository

# Provision infrastructure + register namespace
repo = await (
Repository.builder("my-app", "us-east-1")
Repository.builder()
.namespace("default") # Resolve namespace (default: "default")
.config_cache_ttl(120) # Config cache TTL in seconds
.build() # Async: creates infra, registers default ns, resolves namespace
Expand All @@ -363,7 +361,7 @@ limiter = RateLimiter(repository=repo)

# With infrastructure options
repo = await (
Repository.builder("my-app", "us-east-1")
Repository.builder()
.lambda_memory(512)
.enable_alarms(False)
.permission_boundary("arn:aws:iam::aws:policy/PowerUserAccess")
Expand All @@ -374,9 +372,8 @@ repo = await (

# LocalStack development
repo = await (
Repository.builder("my-app", "us-east-1", endpoint_url="http://localhost:4566")
.enable_aggregator(False)
.enable_alarms(False)
Repository.builder()
.endpoint_url("http://localhost:4566")
.build()
)
```
Expand All @@ -387,28 +384,29 @@ repo = await (
3. Register the "default" namespace (conditional PutItem, no-op if exists)
4. Resolve the requested namespace name to an opaque ID
5. Reinitialize config cache with resolved namespace ID
6. Version check and Lambda auto-update (skip for local endpoints)
6. Version check and Lambda auto-update

**When to use `connect()` vs `builder()`:**
- **`connect()`**: Application code at startup, connecting to pre-existing infrastructure
- **`builder().build()`**: Infrastructure provisioning, first-time setup, CI/CD pipelines
**When to use `open()` vs `builder()`:**
- **`open()`**: 90% of users. Application code, prototyping, LocalStack dev. Auto-provisions infrastructure
- **`builder().build()`**: Enterprise deployments needing permission boundaries, custom Lambda config, IAM role naming

**Config ownership (connect/builder vs deprecated RateLimiter params):**
**Config ownership (open/builder vs deprecated RateLimiter params):**

| Parameter | `connect()` / `builder()` | RateLimiter (deprecated) |
|-----------|---------------------------|--------------------------|
| `namespace` | `connect(namespace=...)` / `.namespace("tenant-a")` | N/A |
| `config_cache_ttl` | `connect(config_cache_ttl=...)` / `.config_cache_ttl(120)` | N/A (was on Repository constructor) |
| `auto_update` | `connect(auto_update=...)` / `.auto_update(True)` | `auto_update=True` (deprecated) |
| Parameter | `open()` / `builder()` | RateLimiter (deprecated) |
|-----------|------------------------|--------------------------|
| `namespace` | `open("tenant-a")` or `ZAEL_NAMESPACE` env var / `.namespace("tenant-a")` | N/A |
| `stack` | `open(stack=...)` or `ZAEL_STACK` env var / `.stack("my-app")` | N/A |
| `config_cache_ttl` | `open(config_cache_ttl=...)` / `.config_cache_ttl(120)` | N/A (was on Repository constructor) |
| `auto_update` | `open(auto_update=...)` / `.auto_update(True)` | `auto_update=True` (deprecated) |
| `bucket_ttl_multiplier` | `.bucket_ttl_multiplier(7)` | `bucket_ttl_refill_multiplier=7` (deprecated) |
| `on_unavailable` | `.on_unavailable("allow")` | `on_unavailable="allow"` (deprecated) |
| `name/region/endpoint_url` | `connect(name, region)` / `builder(name, region)` | `RateLimiter(name=..., region=...)` (deprecated) |
| `region/endpoint_url` | `open(region=..., endpoint_url=...)` / `.region()`, `.endpoint_url()` | `RateLimiter(name=..., region=...)` (deprecated) |
| `stack_options` | Individual builder methods | `RateLimiter(stack_options=...)` (deprecated) |
| Infrastructure options | `.lambda_memory()`, `.enable_alarms()`, etc. | Via `StackOptions` dataclass |

#### Scoped Repositories (Namespace Switching)

After connecting or building, use `repo.namespace()` to get a scoped Repository for a different namespace:
After opening or building, use `repo.namespace()` to get a scoped Repository for a different namespace:

```python
# Register additional namespaces (requires builder or admin access)
Expand Down
8 changes: 4 additions & 4 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ The main components of the API are:
```python
from zae_limiter import RateLimiter, SyncRateLimiter, Repository, SyncRepository

# Async — connect to existing infrastructure (recommended)
repo = await Repository.connect("my-app", "us-east-1")
# Async — open repository (auto-provisions if needed, recommended)
repo = await Repository.open()
limiter = RateLimiter(repository=repo)

# Sync
repo = SyncRepository.connect("my-app", "us-east-1")
repo = SyncRepository.open()
limiter = SyncRateLimiter(repository=repo)
```

Expand All @@ -57,7 +57,7 @@ Limit.custom("requests", capacity=50, refill_amount=50, refill_period_seconds=30
```python
from zae_limiter import RateLimiter, Limit, RateLimitExceeded

repo = await Repository.connect("limiter", "us-east-1")
repo = await Repository.open()
limiter = RateLimiter(repository=repo)

try:
Expand Down
8 changes: 4 additions & 4 deletions docs/api/repository.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ The `Repository` class owns all DynamoDB data access and infrastructure manageme
```python
from zae_limiter import RateLimiter, Repository

# Connect to existing infrastructure (recommended)
repo = await Repository.connect("my-app", region="us-east-1")
# Open repository (auto-provisions if needed, recommended)
repo = await Repository.open()
limiter = RateLimiter(repository=repo)

# For infrastructure provisioning, use builder:
repo = await Repository.builder("my-app", "us-east-1").build()
# For explicit infrastructure provisioning, use builder:
repo = await Repository.builder().build()
limiter = RateLimiter(repository=repo)
```

Expand Down
3 changes: 2 additions & 1 deletion docs/contributing/localstack.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ For quick iteration, declare infrastructure in code:
from zae_limiter import Repository, RateLimiter

repo = await (
Repository.builder("limiter", "us-east-1", endpoint_url="http://localhost:4566")
Repository.builder()
.endpoint_url("http://localhost:4566")
.enable_aggregator(False)
.enable_alarms(False)
.build()
Expand Down
20 changes: 10 additions & 10 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ For scripts and quick demos, pass limits inline:
```python
from zae_limiter import Repository, RateLimiter, Limit, RateLimitExceeded

repo = await Repository.builder("my-app", "us-east-1").build()
repo = await Repository.open()
limiter = RateLimiter(repository=repo)

try:
Expand Down Expand Up @@ -78,7 +78,7 @@ For production, configure limits once and keep application code simple.
```python
from zae_limiter import Repository, RateLimiter, Limit

repo = await Repository.builder("my-app", "us-east-1").build()
repo = await Repository.open()
limiter = RateLimiter(repository=repo)

await limiter.set_system_defaults(limits=[
Expand All @@ -92,7 +92,7 @@ For production, configure limits once and keep application code simple.
```python
from zae_limiter import Repository, RateLimiter, RateLimitExceeded

repo = await Repository.connect("my-app", "us-east-1")
repo = await Repository.open()
limiter = RateLimiter(repository=repo)

try:
Expand Down Expand Up @@ -130,10 +130,10 @@ Both programmatic API and CLI are fully supported for managing infrastructure.

=== "Programmatic"

Use builder methods to declare the desired infrastructure state:
Use `open()` which auto-provisions infrastructure if needed:

```python
repo = await Repository.builder("my-app", "us-east-1").build()
repo = await Repository.open()
limiter = RateLimiter(repository=repo)
```

Expand All @@ -149,15 +149,15 @@ Both programmatic API and CLI are fully supported for managing infrastructure.

### Connecting to Existing Infrastructure

Use `Repository.connect()` to connect to existing infrastructure without attempting to create or modify it:
Use `Repository.open()` to connect to infrastructure, auto-provisioning if the table is missing and registering the namespace if not found:

```python
# Connect to existing infrastructure (recommended for application code)
repo = await Repository.connect("my-app", "us-east-1")
# Open repository (auto-provisions if needed)
repo = await Repository.open()
limiter = RateLimiter(repository=repo)
```

This is the recommended entry point for application code when infrastructure is managed separately (e.g., via CLI or Terraform).
Stack defaults to the `ZAEL_STACK` environment variable or `"zae-limiter"`. Namespace defaults to `ZAEL_NAMESPACE` or `"default"`.

!!! warning "Declarative State Management"
Builder methods declare the desired infrastructure state. If multiple applications
Expand Down Expand Up @@ -358,7 +358,7 @@ For multi-tenant applications, namespaces provide logical isolation within a sin
from zae_limiter import Repository, RateLimiter

# Each tenant gets an isolated namespace
repo = await Repository.connect("my-app", "us-east-1", namespace="tenant-alpha")
repo = await Repository.open("tenant-alpha")
limiter = RateLimiter(repository=repo)

# All operations are scoped to tenant-alpha's namespace
Expand Down
6 changes: 3 additions & 3 deletions docs/guide/basic-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,11 +275,11 @@ zae-limiter caches config data (system defaults, resource defaults, entity limit
from zae_limiter import Repository, RateLimiter

# Default: 60-second cache TTL
repo = await Repository.connect("my-app", "us-east-1", config_cache_ttl=60)
repo = await Repository.open(config_cache_ttl=60)
limiter = RateLimiter(repository=repo)

# Disable caching (for testing)
repo = await Repository.connect("my-app", "us-east-1", config_cache_ttl=0)
repo = await Repository.open(config_cache_ttl=0)
limiter = RateLimiter(repository=repo)
```

Expand Down Expand Up @@ -310,7 +310,7 @@ See [Config Cache Tuning](../performance.md#7-config-cache-tuning) for advanced
Speculative writes are enabled by default, skipping the read round trip for pre-warmed buckets. To disable them:

```python
repo = await Repository.connect("my-app", "us-east-1")
repo = await Repository.open()
limiter = RateLimiter(
repository=repo,
speculative_writes=False, # Disable speculative writes
Expand Down
Loading
Loading