Skip to content

Commit de8ff6d

Browse files
authored
📝 docs: migrate to RepositoryBuilder API and add namespace documentation (#392)
## Summary - Migrate all code examples from deprecated `Repository()`/`StackOptions`/`RateLimiter(name=)` constructors to the v0.10.0 `RepositoryBuilder` pattern - Add namespace documentation throughout: `{ns}/` key prefixes, GSI3+GSI4 schema, namespace registration, `--namespace` CLI flag, namespace-scoped IAM policies (TBAC), multi-tenant isolation - Update README, landing page, architecture, operator guides, user guides, CLI reference, performance, and monitoring docs ## Test plan - [x] `mkdocs build --strict` passes with no errors - [x] Grep verification: no remaining `Repository(name=`, `RateLimiter(name=`, or `stack_options=StackOptions` in updated files - [x] Visual review of rendered docs site Closes #389 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 0858e59 + 457afe4 commit de8ff6d

18 files changed

Lines changed: 431 additions & 237 deletions

‎README.md‎

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,15 @@ conda install -c conda-forge zae-limiter
2828
## Usage
2929

3030
```python
31-
from zae_limiter import RateLimiter, SyncRateLimiter, Limit, StackOptions
31+
from zae_limiter import Repository, RateLimiter, SyncRepository, SyncRateLimiter, Limit
3232

33-
# async-aws-backed-production-ready-rate-limiter
34-
limiter = RateLimiter(
35-
name="my-app",
36-
region="us-east-1",
37-
# Declare desired infrastructure state - CloudFormation ensures it matches
38-
stack_options=StackOptions(),
39-
)
33+
# Async — builder handles infrastructure + namespace resolution
34+
repo = await Repository.builder("my-app", "us-east-1").build()
35+
limiter = RateLimiter(repository=repo)
4036

41-
# Sync wrapper shares the same infrastructure and API.
42-
sync_limiter = SyncRateLimiter(name="my-app", region="us-east-1")
37+
# Sync wrapper shares the same infrastructure and API
38+
sync_repo = SyncRepository.builder("my-app", "us-east-1").build()
39+
sync_limiter = SyncRateLimiter(repository=sync_repo)
4340

4441
# Define default limits (can be overridden per-entity)
4542
default_limits = [
@@ -70,12 +67,15 @@ with sync_limiter.acquire(
7067
resource="gpt-4",
7168
limits=default_limits,
7269
consume={"rpm": 1, "tpm": 500},
73-
use_stored_limits=True, # Uses proj-1's 100k tpm limit
7470
):
7571
call_api()
7672

73+
# Multi-tenant: each tenant gets an isolated namespace
74+
tenant_repo = await Repository.builder("my-app", "us-east-1").namespace("tenant-alpha").build()
75+
tenant_limiter = RateLimiter(repository=tenant_repo)
76+
7777
# Cleanup (removes all data)
78-
await limiter.delete_stack()
78+
await repo.delete_stack()
7979
```
8080

8181
## Documentation
@@ -89,6 +89,7 @@ await limiter.delete_stack()
8989
| [Hierarchical Limits](https://zeroae.github.io/zae-limiter/guide/hierarchical/) | Parent/child entities, cascade mode |
9090
| [LLM Integration](https://zeroae.github.io/zae-limiter/guide/llm-integration/) | Token estimation and reconciliation |
9191
| [CLI Reference](https://zeroae.github.io/zae-limiter/cli/) | Deploy, status, delete commands |
92+
| [Multi-Tenant Guide](https://zeroae.github.io/zae-limiter/infra/production/#multi-tenant-deployments) | Namespace isolation, per-tenant IAM |
9293
| [Production Guide](https://zeroae.github.io/zae-limiter/infra/production/) | Security, monitoring, cost |
9394

9495
## Production Deployment

‎docs/cli.md‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,46 @@ The CLI respects standard AWS environment variables:
3232
| `2` | Invalid arguments |
3333
| `3` | AWS API error |
3434
| `4` | Stack not found |
35+
36+
## Namespace Flag
37+
38+
Most data-access commands accept `--namespace` / `-N` to scope operations to a specific namespace. When omitted, operations default to the `"default"` namespace.
39+
40+
```bash
41+
# Entity operations in a specific namespace
42+
zae-limiter entity set-limits user-123 --namespace tenant-alpha -l rpm:1000
43+
44+
# System defaults for a namespace
45+
zae-limiter system set-defaults --namespace tenant-alpha -l rpm:5000
46+
47+
# Usage and audit scoped to a namespace
48+
zae-limiter usage list --namespace tenant-alpha
49+
zae-limiter audit list --namespace tenant-alpha
50+
```
51+
52+
## Namespace Lifecycle
53+
54+
The `namespace` command group manages the namespace registry:
55+
56+
```bash
57+
# Register namespaces
58+
zae-limiter namespace register tenant-alpha tenant-beta
59+
60+
# List active namespaces
61+
zae-limiter namespace list
62+
63+
# Show namespace details (including opaque ID)
64+
zae-limiter namespace show tenant-alpha
65+
66+
# Soft delete (data preserved, forward lookup removed)
67+
zae-limiter namespace delete tenant-alpha
68+
69+
# Recover a soft-deleted namespace
70+
zae-limiter namespace recover <namespace-id>
71+
72+
# List deleted namespaces (candidates for purge)
73+
zae-limiter namespace orphans
74+
75+
# Hard delete all data in a namespace (irreversible)
76+
zae-limiter namespace purge <namespace-id> --yes
77+
```

‎docs/contributing/architecture.md‎

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,53 @@ All data is stored in a single DynamoDB table using a composite key pattern:
88

99
| Record Type | PK | SK |
1010
|-------------|----|----|
11-
| Entity metadata | `ENTITY#{id}` | `#META` |
12-
| Bucket | `ENTITY#{id}` | `#BUCKET#{resource}#{limit_name}` |
13-
| Entity config | `ENTITY#{id}` | `#CONFIG#{resource}` |
14-
| Resource config | `RESOURCE#{resource}` | `#CONFIG` |
15-
| System config | `SYSTEM#` | `#CONFIG` |
16-
| Usage snapshot | `ENTITY#{id}` | `#USAGE#{resource}#{window_key}` |
17-
| System version | `SYSTEM#` | `#VERSION` |
18-
| Audit events | `AUDIT#{entity_id}` | `#AUDIT#{timestamp}` |
11+
| Entity metadata | `{ns}/ENTITY#{id}` | `#META` |
12+
| Bucket | `{ns}/ENTITY#{id}` | `#BUCKET#{resource}#{limit_name}` |
13+
| Entity config | `{ns}/ENTITY#{id}` | `#CONFIG#{resource}` |
14+
| Resource config | `{ns}/RESOURCE#{resource}` | `#CONFIG` |
15+
| System config | `{ns}/SYSTEM#` | `#CONFIG` |
16+
| Usage snapshot | `{ns}/ENTITY#{id}` | `#USAGE#{resource}#{window_key}` |
17+
| System version | `{ns}/SYSTEM#` | `#VERSION` |
18+
| Audit events | `{ns}/AUDIT#{entity_id}` | `#AUDIT#{timestamp}` |
19+
| Namespace forward | `_/SYSTEM#` | `#NAMESPACE#{name}` |
20+
| Namespace reverse | `_/SYSTEM#` | `#NSID#{id}` |
1921

2022
### Global Secondary Indexes
2123

2224
| Index | Purpose | Key Pattern |
2325
|-------|---------|-------------|
24-
| **GSI1** | Parent → Children lookup | `GSI1PK=PARENT#{id}` → `GSI1SK=CHILD#{id}` |
25-
| **GSI2** | Resource aggregation | `GSI2PK=RESOURCE#{name}` → buckets/usage |
26-
| **GSI3** | Entity config queries (sparse) | `GSI3PK=ENTITY_CONFIG#{resource}` → `GSI3SK=entity_id` |
26+
| **GSI1** | Parent → Children lookup | `GSI1PK={ns}/PARENT#{id}` → `GSI1SK=CHILD#{id}` |
27+
| **GSI2** | Resource aggregation | `GSI2PK={ns}/RESOURCE#{name}` → buckets/usage |
28+
| **GSI3** | Entity config queries (sparse) | `GSI3PK={ns}/ENTITY_CONFIG#{resource}` → `GSI3SK=entity_id` |
29+
| **GSI4** | Namespace item discovery (KEYS_ONLY) | `GSI4PK={ns}` → `GSI4SK=PK` |
2730

2831
### Access Patterns
2932

3033
| Pattern | Query |
3134
|---------|-------|
32-
| Get entity | `PK=ENTITY#{id}, SK=#META` |
33-
| Get buckets | `PK=ENTITY#{id}, SK begins_with #BUCKET#` |
35+
| Get entity | `PK={ns}/ENTITY#{id}, SK=#META` |
36+
| Get buckets | `PK={ns}/ENTITY#{id}, SK begins_with #BUCKET#` |
3437
| Batch get buckets | `BatchGetItem` with multiple PK/SK pairs |
35-
| Get children | GSI1: `GSI1PK=PARENT#{id}` |
36-
| Resource capacity | GSI2: `GSI2PK=RESOURCE#{name}, SK begins_with BUCKET#` |
37-
| Get version | `PK=SYSTEM#, SK=#VERSION` |
38-
| Get audit events | `PK=AUDIT#{entity_id}, SK begins_with #AUDIT#` |
39-
| Get usage snapshots | `PK=ENTITY#{id}, SK begins_with #USAGE#` |
40-
| Get system config | `PK=SYSTEM#, SK=#CONFIG` |
41-
| Get resource config | `PK=RESOURCE#{resource}, SK=#CONFIG` |
42-
| Get entity config | `PK=ENTITY#{id}, SK=#CONFIG#{resource}` |
43-
| List entities with custom limits | GSI3: `GSI3PK=ENTITY_CONFIG#{resource}` |
38+
| Get children | GSI1: `GSI1PK={ns}/PARENT#{id}` |
39+
| Resource capacity | GSI2: `GSI2PK={ns}/RESOURCE#{name}, SK begins_with BUCKET#` |
40+
| Get version | `PK={ns}/SYSTEM#, SK=#VERSION` |
41+
| Get audit events | `PK={ns}/AUDIT#{entity_id}, SK begins_with #AUDIT#` |
42+
| Get usage snapshots | `PK={ns}/ENTITY#{id}, SK begins_with #USAGE#` |
43+
| Get system config | `PK={ns}/SYSTEM#, SK=#CONFIG` |
44+
| Get resource config | `PK={ns}/RESOURCE#{resource}, SK=#CONFIG` |
45+
| Get entity config | `PK={ns}/ENTITY#{id}, SK=#CONFIG#{resource}` |
46+
| List entities with custom limits | GSI3: `GSI3PK={ns}/ENTITY_CONFIG#{resource}` |
47+
| Namespace forward lookup | `PK=_/SYSTEM#, SK=#NAMESPACE#{name}` |
48+
| Namespace reverse lookup | `PK=_/SYSTEM#, SK=#NSID#{id}` |
49+
| List all items in namespace | GSI4: `GSI4PK={ns}` |
50+
51+
### Namespace Isolation
52+
53+
All partition key values are prefixed with an opaque namespace ID (`{ns}/`), providing logical isolation between tenants within a single DynamoDB table. The reserved namespace `_` is used for the namespace registry itself (forward and reverse lookup records).
54+
55+
- **Namespace ID format**: 11-character opaque string generated via `secrets.token_urlsafe(8)`
56+
- **Default namespace**: Automatically registered on first deploy or `RepositoryBuilder.build()`
57+
- **GSI4**: A KEYS_ONLY index on `GSI4PK=namespace_id` enables `purge_namespace()` to discover and delete all items belonging to a namespace
4458

4559
### Optimized Read Patterns
4660

@@ -80,7 +94,7 @@ See [ADR-111](../adr/111-flatten-all-records.md).
8094
```{.python .lint-only}
8195
# Entity record (FLAT structure):
8296
{
83-
"PK": "ENTITY#user-1",
97+
"PK": "{ns}/ENTITY#user-1",
8498
"SK": "#META",
8599
"entity_id": "user-1",
86100
"name": "User One",
@@ -94,7 +108,7 @@ See [ADR-111](../adr/111-flatten-all-records.md).
94108
```python
95109
# Bucket record (FLAT structure, ADR-114/115):
96110
{
97-
"PK": "ENTITY#user-1",
111+
"PK": "{ns}/ENTITY#user-1",
98112
"SK": "#BUCKET#gpt-4",
99113
"entity_id": "user-1",
100114
"resource": "gpt-4",
@@ -105,7 +119,7 @@ See [ADR-111](../adr/111-flatten-all-records.md).
105119
"b_rpm_cp": 100000, # capacity_milli for rpm limit
106120
"b_rpm_tc": 5000, # total_consumed_milli for rpm
107121
"rf": 1704067200000, # last_refill_ms (shared across limits)
108-
"GSI2PK": "RESOURCE#gpt-4",
122+
"GSI2PK": "{ns}/RESOURCE#gpt-4",
109123
"ttl": 1234567890
110124
}
111125
```
@@ -121,15 +135,15 @@ exceeds consumption rate. See [Issue #179](https://github.com/zeroae/zae-limiter
121135
```python
122136
# Usage snapshot (FLAT structure):
123137
{
124-
"PK": "ENTITY#user-1",
138+
"PK": "{ns}/ENTITY#user-1",
125139
"SK": "#USAGE#gpt-4#2024-01-01T14:00:00Z",
126140
"entity_id": "user-1",
127141
"resource": "gpt-4", # Top-level attribute
128142
"window": "hourly", # Top-level attribute
129143
"window_start": "...", # Top-level attribute
130144
"tpm": 5000, # Counter at top-level
131145
"total_events": 10, # Counter at top-level
132-
"GSI2PK": "RESOURCE#gpt-4",
146+
"GSI2PK": "{ns}/RESOURCE#gpt-4",
133147
"ttl": 1234567890
134148
}
135149
```
@@ -147,7 +161,7 @@ See: [Issue #168](https://github.com/zeroae/zae-limiter/issues/168)
147161
```python
148162
# Resource config (composite, FLAT structure):
149163
{
150-
"PK": "RESOURCE#gpt-4", # or SYSTEM# or ENTITY#{id}
164+
"PK": "{ns}/RESOURCE#gpt-4", # or {ns}/SYSTEM# or {ns}/ENTITY#{id}
151165
"SK": "#CONFIG", # or #CONFIG#{resource} for entity level
152166
"resource": "gpt-4",
153167
"l_tpm_cp": 100000, # capacity for tpm limit
@@ -162,11 +176,13 @@ Config records use four-level precedence: **Entity (resource-specific) > Entity
162176

163177
**Key builders:**
164178

165-
- `pk_system()` - Returns `SYSTEM#`
166-
- `pk_resource(resource)` - Returns `RESOURCE#{resource}`
167-
- `pk_entity(entity_id)` - Returns `ENTITY#{entity_id}`
179+
- `pk_system(namespace_id)` - Returns `{ns}/SYSTEM#`
180+
- `pk_resource(namespace_id, resource)` - Returns `{ns}/RESOURCE#{resource}`
181+
- `pk_entity(namespace_id, entity_id)` - Returns `{ns}/ENTITY#{entity_id}`
168182
- `sk_config()` - Returns `#CONFIG` (system/resource level)
169183
- `sk_config(resource)` - Returns `#CONFIG#{resource}` (entity level)
184+
- `sk_namespace(name)` - Returns `#NAMESPACE#{name}` (forward lookup)
185+
- `sk_nsid(id)` - Returns `#NSID#{id}` (reverse lookup)
170186

171187
**Audit entity IDs for config levels** (see [ADR-106](../adr/106-audit-entity-ids-for-config.md)):
172188

@@ -484,7 +500,7 @@ src/zae_limiter/
484500
├── exceptions.py # RateLimitExceeded, RateLimiterUnavailable, etc.
485501
├── naming.py # Resource name validation
486502
├── bucket.py # Token bucket math (integer arithmetic)
487-
├── schema.py # DynamoDB key builders
503+
├── schema.py # DynamoDB key builders (namespace-prefixed)
488504
├── repository_protocol.py # RepositoryProtocol for backend abstraction
489505
├── repository.py # DynamoDB operations
490506
├── config_cache.py # Client-side config caching with TTL

‎docs/contributing/localstack.md‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,16 @@ async with limiter.acquire(
9393

9494
For quick iteration, declare infrastructure in code:
9595

96-
```python
97-
from zae_limiter import RateLimiter, StackOptions
96+
```{.python .requires-localstack}
97+
from zae_limiter import Repository, RateLimiter
9898
99-
limiter = RateLimiter(
100-
name="limiter",
101-
endpoint_url="http://localhost:4566",
102-
region="us-east-1",
103-
stack_options=StackOptions(), # Declare desired state
99+
repo = await (
100+
Repository.builder("limiter", "us-east-1", endpoint_url="http://localhost:4566")
101+
.enable_aggregator(False)
102+
.enable_alarms(False)
103+
.build()
104104
)
105+
limiter = RateLimiter(repository=repo)
105106
```
106107

107108
## Environment Variables

‎docs/contributing/testing.md‎

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def localstack_endpoint():
6969
```python
7070
import uuid
7171
import pytest
72-
from zae_limiter import Repository, RateLimiter, StackOptions
72+
from zae_limiter import Repository, RateLimiter
7373

7474
@pytest.fixture(scope="function")
7575
async def limiter(localstack_endpoint):
@@ -84,16 +84,14 @@ async def limiter(localstack_endpoint):
8484
# Unique name prevents test interference
8585
name = f"test-{uuid.uuid4().hex[:8]}"
8686

87-
repo = Repository(
88-
name=name,
89-
endpoint_url=localstack_endpoint,
90-
region="us-east-1",
91-
stack_options=StackOptions(enable_aggregator=False),
87+
repo = await (
88+
Repository.builder(name, "us-east-1", endpoint_url=localstack_endpoint)
89+
.enable_aggregator(False)
90+
.build()
9291
)
9392
limiter = RateLimiter(repository=repo)
9493

95-
async with limiter:
96-
yield limiter
94+
yield limiter
9795

9896
# Cleanup: delete the CloudFormation stack
9997
await repo.delete_stack()
@@ -122,16 +120,14 @@ async def shared_limiter(localstack_endpoint):
122120
123121
Trade-off: Tests share state, less isolation.
124122
"""
125-
repo = Repository(
126-
name="integration-test-shared",
127-
endpoint_url=localstack_endpoint,
128-
region="us-east-1",
129-
stack_options=StackOptions(enable_aggregator=False),
123+
repo = await (
124+
Repository.builder("integration-test-shared", "us-east-1", endpoint_url=localstack_endpoint)
125+
.enable_aggregator(False)
126+
.build()
130127
)
131128
limiter = RateLimiter(repository=repo)
132129
133-
async with limiter:
134-
yield limiter
130+
yield limiter
135131
136132
await repo.delete_stack()
137133
```
@@ -142,21 +138,19 @@ async def shared_limiter(localstack_endpoint):
142138
@pytest.fixture(scope="function")
143139
def sync_limiter(localstack_endpoint):
144140
"""Synchronous rate limiter with cleanup."""
145-
from zae_limiter import SyncRepository, SyncRateLimiter, StackOptions
141+
from zae_limiter import SyncRepository, SyncRateLimiter
146142
import uuid
147143
148144
name = f"test-sync-{uuid.uuid4().hex[:8]}"
149145
150-
repo = SyncRepository(
151-
name=name,
152-
endpoint_url=localstack_endpoint,
153-
region="us-east-1",
154-
stack_options=StackOptions(enable_aggregator=False),
146+
repo = (
147+
SyncRepository.builder(name, "us-east-1", endpoint_url=localstack_endpoint)
148+
.enable_aggregator(False)
149+
.build()
155150
)
156151
limiter = SyncRateLimiter(repository=repo)
157152
158-
with limiter:
159-
yield limiter
153+
yield limiter
160154
161155
repo.delete_stack()
162156
```

0 commit comments

Comments
 (0)