zae-limiter 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zae_limiter/__init__.py +130 -0
- zae_limiter/aggregator/__init__.py +11 -0
- zae_limiter/aggregator/handler.py +54 -0
- zae_limiter/aggregator/processor.py +270 -0
- zae_limiter/bucket.py +291 -0
- zae_limiter/cli.py +608 -0
- zae_limiter/exceptions.py +214 -0
- zae_limiter/infra/__init__.py +10 -0
- zae_limiter/infra/cfn_template.yaml +255 -0
- zae_limiter/infra/lambda_builder.py +85 -0
- zae_limiter/infra/stack_manager.py +536 -0
- zae_limiter/lease.py +196 -0
- zae_limiter/limiter.py +925 -0
- zae_limiter/migrations/__init__.py +114 -0
- zae_limiter/migrations/v1_0_0.py +55 -0
- zae_limiter/models.py +302 -0
- zae_limiter/repository.py +656 -0
- zae_limiter/schema.py +163 -0
- zae_limiter/version.py +214 -0
- zae_limiter-0.1.0.dist-info/METADATA +470 -0
- zae_limiter-0.1.0.dist-info/RECORD +24 -0
- zae_limiter-0.1.0.dist-info/WHEEL +4 -0
- zae_limiter-0.1.0.dist-info/entry_points.txt +2 -0
- zae_limiter-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zae-limiter
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Rate limiting library backed by DynamoDB with token bucket algorithm
|
|
5
|
+
Project-URL: Homepage, https://github.com/zeroae/zae-limiter
|
|
6
|
+
Project-URL: Repository, https://github.com/zeroae/zae-limiter
|
|
7
|
+
Author-email: ZeroAE <dev@zeroae.com>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2025-2026 Zero A.E., LLC
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Keywords: api-rate-limit,aws,dynamodb,llm,rate-limiting,token-bucket
|
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
37
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
38
|
+
Requires-Python: >=3.11
|
|
39
|
+
Requires-Dist: aioboto3>=12.0.0
|
|
40
|
+
Requires-Dist: boto3>=1.34.0
|
|
41
|
+
Requires-Dist: click>=8.0.0
|
|
42
|
+
Requires-Dist: types-aiobotocore[dynamodb]>=2.0.0
|
|
43
|
+
Provides-Extra: cdk
|
|
44
|
+
Requires-Dist: aws-cdk-lib>=2.0.0; extra == 'cdk'
|
|
45
|
+
Requires-Dist: constructs>=10.0.0; extra == 'cdk'
|
|
46
|
+
Provides-Extra: dev
|
|
47
|
+
Requires-Dist: moto[dynamodb]>=5.0.0; extra == 'dev'
|
|
48
|
+
Requires-Dist: mypy>=1.8.0; extra == 'dev'
|
|
49
|
+
Requires-Dist: openapi-spec-validator>=0.7.1; extra == 'dev'
|
|
50
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
51
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
52
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
53
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
54
|
+
Provides-Extra: lambda
|
|
55
|
+
Requires-Dist: aws-lambda-powertools>=2.0.0; extra == 'lambda'
|
|
56
|
+
Description-Content-Type: text/markdown
|
|
57
|
+
|
|
58
|
+
# zae-limiter
|
|
59
|
+
|
|
60
|
+
A rate limiting library backed by DynamoDB using the token bucket algorithm.
|
|
61
|
+
|
|
62
|
+
## Features
|
|
63
|
+
|
|
64
|
+
- **Token Bucket Algorithm**: Precise rate limiting with configurable burst capacity
|
|
65
|
+
- **Multiple Limits**: Track requests per minute, tokens per minute, etc. in a single call
|
|
66
|
+
- **Hierarchical Entities**: Two-level hierarchy (project → API keys) with cascade mode
|
|
67
|
+
- **Atomic Transactions**: Multi-key updates via DynamoDB TransactWriteItems
|
|
68
|
+
- **Rollback on Exception**: Automatic rollback if your code throws
|
|
69
|
+
- **Stored Limits**: Configure per-entity limits in DynamoDB
|
|
70
|
+
- **Usage Analytics**: Lambda aggregator for hourly/daily usage snapshots
|
|
71
|
+
- **Async + Sync APIs**: First-class async support with sync wrapper
|
|
72
|
+
|
|
73
|
+
## Installation
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install zae-limiter
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Or using uv:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
uv pip install zae-limiter
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Quick Start
|
|
86
|
+
|
|
87
|
+
### 1. Deploy Infrastructure
|
|
88
|
+
|
|
89
|
+
Using CLI (recommended):
|
|
90
|
+
```bash
|
|
91
|
+
zae-limiter deploy --table-name rate_limits --region us-east-1
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Or get template for manual deployment:
|
|
95
|
+
```bash
|
|
96
|
+
zae-limiter cfn-template > template.yaml
|
|
97
|
+
aws cloudformation deploy --template-file template.yaml --stack-name zae-limiter
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Or auto-create in code (development):
|
|
101
|
+
```python
|
|
102
|
+
from zae_limiter import RateLimiter
|
|
103
|
+
|
|
104
|
+
limiter = RateLimiter(
|
|
105
|
+
table_name="rate_limits",
|
|
106
|
+
region="us-east-1",
|
|
107
|
+
create_stack=True, # auto-create CloudFormation stack
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 2. Use in Code
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from zae_limiter import RateLimiter, Limit
|
|
115
|
+
|
|
116
|
+
# Initialize the limiter (stack must already exist)
|
|
117
|
+
limiter = RateLimiter(
|
|
118
|
+
table_name="rate_limits",
|
|
119
|
+
region="us-east-1",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Acquire rate limit capacity
|
|
123
|
+
async with limiter.acquire(
|
|
124
|
+
entity_id="api-key-123",
|
|
125
|
+
resource="gpt-4",
|
|
126
|
+
limits=[
|
|
127
|
+
Limit.per_minute("rpm", 100), # 100 requests/minute
|
|
128
|
+
Limit.per_minute("tpm", 10_000), # 10k tokens/minute
|
|
129
|
+
],
|
|
130
|
+
consume={"rpm": 1, "tpm": 500}, # estimate 500 tokens
|
|
131
|
+
) as lease:
|
|
132
|
+
response = await call_llm()
|
|
133
|
+
|
|
134
|
+
# Reconcile actual token usage (can go negative)
|
|
135
|
+
actual_tokens = response.usage.total_tokens
|
|
136
|
+
await lease.adjust(tpm=actual_tokens - 500)
|
|
137
|
+
|
|
138
|
+
# On success: consumption is committed
|
|
139
|
+
# On exception: consumption is rolled back
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Local Development
|
|
143
|
+
|
|
144
|
+
For DynamoDB Local, auto-creation uses direct table creation (not CloudFormation):
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
limiter = RateLimiter(
|
|
148
|
+
table_name="rate_limits",
|
|
149
|
+
endpoint_url="http://localhost:8000",
|
|
150
|
+
create_table=True, # Creates table directly (CloudFormation skipped)
|
|
151
|
+
)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Usage
|
|
155
|
+
|
|
156
|
+
### Basic Rate Limiting
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from zae_limiter import RateLimiter, Limit, RateLimitExceeded
|
|
160
|
+
|
|
161
|
+
limiter = RateLimiter(table_name="rate_limits")
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
async with limiter.acquire(
|
|
165
|
+
entity_id="user-123",
|
|
166
|
+
resource="api",
|
|
167
|
+
limits=[Limit.per_minute("requests", 100)],
|
|
168
|
+
consume={"requests": 1},
|
|
169
|
+
) as lease:
|
|
170
|
+
await do_work()
|
|
171
|
+
except RateLimitExceeded as e:
|
|
172
|
+
# Exception includes ALL limit statuses (passed and failed)
|
|
173
|
+
print(f"Retry after {e.retry_after_seconds:.1f}s")
|
|
174
|
+
|
|
175
|
+
# For API responses
|
|
176
|
+
return JSONResponse(
|
|
177
|
+
status_code=429,
|
|
178
|
+
content=e.as_dict(),
|
|
179
|
+
headers={"Retry-After": e.retry_after_header},
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Hierarchical Rate Limits (Cascade)
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
# Create parent (project) and child (API key)
|
|
187
|
+
await limiter.create_entity(entity_id="proj-1", name="Production")
|
|
188
|
+
await limiter.create_entity(entity_id="key-abc", parent_id="proj-1")
|
|
189
|
+
|
|
190
|
+
# Cascade mode: consume from both key AND project
|
|
191
|
+
async with limiter.acquire(
|
|
192
|
+
entity_id="key-abc",
|
|
193
|
+
resource="gpt-4",
|
|
194
|
+
limits=[
|
|
195
|
+
Limit.per_minute("tpm", 10_000), # per-key limit
|
|
196
|
+
],
|
|
197
|
+
consume={"tpm": 500},
|
|
198
|
+
cascade=True, # also applies to parent
|
|
199
|
+
) as lease:
|
|
200
|
+
await call_api()
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Burst Capacity
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
# Allow burst of 15k tokens, but sustain only 10k/minute
|
|
207
|
+
limits = [
|
|
208
|
+
Limit.per_minute("tpm", 10_000, burst=15_000),
|
|
209
|
+
]
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Stored Limits
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
# Store custom limits for premium users
|
|
216
|
+
await limiter.set_limits(
|
|
217
|
+
entity_id="user-premium",
|
|
218
|
+
limits=[
|
|
219
|
+
Limit.per_minute("rpm", 500),
|
|
220
|
+
Limit.per_minute("tpm", 50_000, burst=75_000),
|
|
221
|
+
],
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Use stored limits (falls back to defaults if not stored)
|
|
225
|
+
async with limiter.acquire(
|
|
226
|
+
entity_id="user-premium",
|
|
227
|
+
resource="gpt-4",
|
|
228
|
+
limits=[Limit.per_minute("rpm", 100)], # default
|
|
229
|
+
consume={"rpm": 1},
|
|
230
|
+
use_stored_limits=True,
|
|
231
|
+
) as lease:
|
|
232
|
+
...
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### LLM Token Estimation + Reconciliation
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
async with limiter.acquire(
|
|
239
|
+
entity_id="key-abc",
|
|
240
|
+
resource="gpt-4",
|
|
241
|
+
limits=[
|
|
242
|
+
Limit.per_minute("rpm", 100),
|
|
243
|
+
Limit.per_minute("tpm", 10_000),
|
|
244
|
+
],
|
|
245
|
+
consume={"rpm": 1, "tpm": 500}, # estimate
|
|
246
|
+
) as lease:
|
|
247
|
+
response = await llm.complete(prompt)
|
|
248
|
+
actual = response.usage.total_tokens
|
|
249
|
+
|
|
250
|
+
# Adjust without throwing (can go negative)
|
|
251
|
+
await lease.adjust(tpm=actual - 500)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Check Capacity Before Expensive Operations
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
# Check available capacity
|
|
258
|
+
available = await limiter.available(
|
|
259
|
+
entity_id="key-abc",
|
|
260
|
+
resource="gpt-4",
|
|
261
|
+
limits=[Limit.per_minute("tpm", 10_000)],
|
|
262
|
+
)
|
|
263
|
+
print(f"Available tokens: {available['tpm']}")
|
|
264
|
+
|
|
265
|
+
# Check when capacity will be available
|
|
266
|
+
if available["tpm"] < needed_tokens:
|
|
267
|
+
wait = await limiter.time_until_available(
|
|
268
|
+
entity_id="key-abc",
|
|
269
|
+
resource="gpt-4",
|
|
270
|
+
limits=[Limit.per_minute("tpm", 10_000)],
|
|
271
|
+
needed={"tpm": needed_tokens},
|
|
272
|
+
)
|
|
273
|
+
raise RetryAfter(seconds=wait)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Synchronous API
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
from zae_limiter import SyncRateLimiter, Limit
|
|
280
|
+
|
|
281
|
+
limiter = SyncRateLimiter(table_name="rate_limits")
|
|
282
|
+
|
|
283
|
+
with limiter.acquire(
|
|
284
|
+
entity_id="key-abc",
|
|
285
|
+
resource="api",
|
|
286
|
+
limits=[Limit.per_minute("rpm", 100)],
|
|
287
|
+
consume={"rpm": 1},
|
|
288
|
+
) as lease:
|
|
289
|
+
response = call_api()
|
|
290
|
+
lease.adjust(tokens=response.token_count)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Failure Modes
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
from zae_limiter import RateLimiter, FailureMode
|
|
297
|
+
|
|
298
|
+
# Fail closed (default): reject requests if DynamoDB unavailable
|
|
299
|
+
limiter = RateLimiter(
|
|
300
|
+
table_name="rate_limits",
|
|
301
|
+
failure_mode=FailureMode.FAIL_CLOSED,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Fail open: allow requests if DynamoDB unavailable
|
|
305
|
+
limiter = RateLimiter(
|
|
306
|
+
table_name="rate_limits",
|
|
307
|
+
failure_mode=FailureMode.FAIL_OPEN,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Override per-call
|
|
311
|
+
async with limiter.acquire(
|
|
312
|
+
...,
|
|
313
|
+
failure_mode=FailureMode.FAIL_OPEN,
|
|
314
|
+
):
|
|
315
|
+
...
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Exception Details
|
|
319
|
+
|
|
320
|
+
When a rate limit is exceeded, `RateLimitExceeded` includes full details:
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
try:
|
|
324
|
+
async with limiter.acquire(...):
|
|
325
|
+
...
|
|
326
|
+
except RateLimitExceeded as e:
|
|
327
|
+
# All limits that were checked
|
|
328
|
+
for status in e.statuses:
|
|
329
|
+
print(f"{status.limit_name}: {status.available}/{status.limit.capacity}")
|
|
330
|
+
print(f" exceeded: {status.exceeded}")
|
|
331
|
+
print(f" retry_after: {status.retry_after_seconds}s")
|
|
332
|
+
|
|
333
|
+
# Just the violations
|
|
334
|
+
for v in e.violations:
|
|
335
|
+
print(f"Exceeded: {v.limit_name}")
|
|
336
|
+
|
|
337
|
+
# Just the passed limits
|
|
338
|
+
for p in e.passed:
|
|
339
|
+
print(f"Passed: {p.limit_name}")
|
|
340
|
+
|
|
341
|
+
# Primary bottleneck
|
|
342
|
+
print(f"Bottleneck: {e.primary_violation.limit_name}")
|
|
343
|
+
print(f"Retry after: {e.retry_after_seconds}s")
|
|
344
|
+
|
|
345
|
+
# For HTTP responses
|
|
346
|
+
response_body = e.as_dict()
|
|
347
|
+
retry_header = e.retry_after_header
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Infrastructure
|
|
351
|
+
|
|
352
|
+
### Deploy with CloudFormation
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
# Export the template from the installed package
|
|
356
|
+
zae-limiter cfn-template > template.yaml
|
|
357
|
+
|
|
358
|
+
# Deploy the DynamoDB table and Lambda aggregator
|
|
359
|
+
aws cloudformation deploy \
|
|
360
|
+
--template-file template.yaml \
|
|
361
|
+
--stack-name zae-limiter \
|
|
362
|
+
--parameter-overrides \
|
|
363
|
+
TableName=rate_limits \
|
|
364
|
+
SnapshotRetentionDays=90 \
|
|
365
|
+
--capabilities CAPABILITY_NAMED_IAM
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Automatic Lambda Deployment
|
|
369
|
+
|
|
370
|
+
The `zae-limiter deploy` CLI command automatically handles Lambda deployment:
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
# Deploy stack with Lambda aggregator (automatic)
|
|
374
|
+
zae-limiter deploy --table-name rate_limits --region us-east-1
|
|
375
|
+
|
|
376
|
+
# The CLI automatically:
|
|
377
|
+
# 1. Creates CloudFormation stack with DynamoDB table and Lambda function
|
|
378
|
+
# 2. Builds Lambda deployment package from installed library
|
|
379
|
+
# 3. Deploys Lambda code via AWS Lambda API (~30KB, no S3 required)
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
To deploy without the Lambda aggregator:
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
zae-limiter deploy --table-name rate_limits --no-aggregator
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Local Development with DynamoDB Local
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
# Start DynamoDB Local
|
|
392
|
+
docker run -p 8000:8000 amazon/dynamodb-local
|
|
393
|
+
|
|
394
|
+
# Use endpoint_url
|
|
395
|
+
limiter = RateLimiter(
|
|
396
|
+
table_name="rate_limits",
|
|
397
|
+
endpoint_url="http://localhost:8000",
|
|
398
|
+
create_table=True,
|
|
399
|
+
)
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## Development
|
|
403
|
+
|
|
404
|
+
### Setup
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
# Clone repository
|
|
408
|
+
git clone https://github.com/zeroae/zae-limiter.git
|
|
409
|
+
cd zae-limiter
|
|
410
|
+
|
|
411
|
+
# Using uv
|
|
412
|
+
uv venv
|
|
413
|
+
source .venv/bin/activate
|
|
414
|
+
uv pip install -e ".[dev]"
|
|
415
|
+
|
|
416
|
+
# Using conda
|
|
417
|
+
conda create -n zae-limiter python=3.12
|
|
418
|
+
conda activate zae-limiter
|
|
419
|
+
pip install -e ".[dev]"
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Run Tests
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
# Run all tests
|
|
426
|
+
pytest
|
|
427
|
+
|
|
428
|
+
# Run with coverage
|
|
429
|
+
pytest --cov=zae_limiter --cov-report=html
|
|
430
|
+
|
|
431
|
+
# Run specific test file
|
|
432
|
+
pytest tests/test_limiter.py -v
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Code Quality
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
# Format and lint
|
|
439
|
+
ruff check --fix .
|
|
440
|
+
ruff format .
|
|
441
|
+
|
|
442
|
+
# Type checking
|
|
443
|
+
mypy src/zae_limiter
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
## Architecture
|
|
447
|
+
|
|
448
|
+
### DynamoDB Schema (Single Table)
|
|
449
|
+
|
|
450
|
+
| Record Type | PK | SK |
|
|
451
|
+
|-------------|----|----|
|
|
452
|
+
| Entity metadata | `ENTITY#{id}` | `#META` |
|
|
453
|
+
| Bucket | `ENTITY#{id}` | `#BUCKET#{resource}#{limit_name}` |
|
|
454
|
+
| Limit config | `ENTITY#{id}` | `#LIMIT#{resource}#{limit_name}` |
|
|
455
|
+
| Usage snapshot | `ENTITY#{id}` | `#USAGE#{resource}#{window_key}` |
|
|
456
|
+
|
|
457
|
+
**Indexes:**
|
|
458
|
+
- **GSI1**: Parent → Children lookup (`PARENT#{id}` → `CHILD#{id}`)
|
|
459
|
+
- **GSI2**: Resource aggregation (`RESOURCE#{name}` → buckets/usage)
|
|
460
|
+
|
|
461
|
+
### Token Bucket Implementation
|
|
462
|
+
|
|
463
|
+
- All values stored as **millitokens** (×1000) for precision
|
|
464
|
+
- Refill rate stored as **fraction** (amount/period) to avoid floating point
|
|
465
|
+
- Supports **negative buckets** for post-hoc reconciliation
|
|
466
|
+
- Uses DynamoDB **transactions** for multi-key atomicity
|
|
467
|
+
|
|
468
|
+
## License
|
|
469
|
+
|
|
470
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
zae_limiter/__init__.py,sha256=PDk9pr5p0SrhzboaywO2YOYzjO5XVk06mfF4apZL39U,3378
|
|
2
|
+
zae_limiter/bucket.py,sha256=Nw3YYkj1TZIXR5tjpR15-LJb2yaNu_uqLUkoXmrtBy0,8381
|
|
3
|
+
zae_limiter/cli.py,sha256=3j-H33hwZXvC_9TS0QeKFomkFZqem9ormnqMwfASejE,19634
|
|
4
|
+
zae_limiter/exceptions.py,sha256=e_Jp1oQLiZAMXsY4OK8TEneGbJEeh2wowWkw1Z-1DNo,6930
|
|
5
|
+
zae_limiter/lease.py,sha256=Vok1lOusSr_ZuUne7-axvGrz81enTLY1KmGEV3ul4Yw,6442
|
|
6
|
+
zae_limiter/limiter.py,sha256=L9Vsmje1OA3DLYusEwB8Lsnw7njrxPdpZSsHArfWIIU,31022
|
|
7
|
+
zae_limiter/models.py,sha256=augqHoJo_PHyuWBqdRyst_xX713FhDi74d1U9Cao7U0,8496
|
|
8
|
+
zae_limiter/repository.py,sha256=Y-S7AzyRq2EoMiVVJKtuOyGEbaCeEDRpDqMtI7Yy_hM,23831
|
|
9
|
+
zae_limiter/schema.py,sha256=xaQtzbxhf89_VgoENblSrnmdo2L0EF9SznRx4vVynB8,4856
|
|
10
|
+
zae_limiter/version.py,sha256=vP8nq2UK_PeaFwaJzbmKvN4wEmu9BUGfjnLsRZvix7w,6843
|
|
11
|
+
zae_limiter/aggregator/__init__.py,sha256=EqzD7AvRjYtXsCd1fPRnpdtiWy2hVpaIzWH5twPPhNs,259
|
|
12
|
+
zae_limiter/aggregator/handler.py,sha256=8pPudwmJA_506YgLwot_g5jqgJT6R2JKSVQ2eqkRmXo,1506
|
|
13
|
+
zae_limiter/aggregator/processor.py,sha256=l66BfU5sxq4OVnToaqv0AUBRH0S61gLuPKdqQQSI3XM,7903
|
|
14
|
+
zae_limiter/infra/__init__.py,sha256=tPsPGP3DCVv8r-nPVAJ_16thG9QFIiagtH2TU4z3DFU,266
|
|
15
|
+
zae_limiter/infra/cfn_template.yaml,sha256=wMjA6UYQ0fGBGdE4c95KeSh43mhsvGWClsb6MF5Asxo,7124
|
|
16
|
+
zae_limiter/infra/lambda_builder.py,sha256=5eW5ayAqU153MjMAIbnB-fpGsi8Z9bigICP488AN9fY,2461
|
|
17
|
+
zae_limiter/infra/stack_manager.py,sha256=ej-7ISW94_QhXe5zyC9PgqDqYP-wlYPhAHI7uFDRurA,18287
|
|
18
|
+
zae_limiter/migrations/__init__.py,sha256=QQ4GvADecRzP36yhc1vr2b1MIUkZ8iUJePcso3Pz3nQ,2986
|
|
19
|
+
zae_limiter/migrations/v1_0_0.py,sha256=FielAlgXd4qKgQ5dRcQssJR6wD5fiwXfslQOk9e7918,1472
|
|
20
|
+
zae_limiter-0.1.0.dist-info/METADATA,sha256=m28Q9tQyx8n1BKy6G29xRO3MqQDwHJdsDB4HiBWHZ80,12632
|
|
21
|
+
zae_limiter-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
22
|
+
zae_limiter-0.1.0.dist-info/entry_points.txt,sha256=CCa1Dwe99X_3fqSLabD6aE2S9uYG4NVdp2_k3G6-FPI,52
|
|
23
|
+
zae_limiter-0.1.0.dist-info/licenses/LICENSE,sha256=e9yyrQClplsHjduiLZfOtCeuwJ1xFKmAZQxJ-Gc55Rs,1076
|
|
24
|
+
zae_limiter-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Zero A.E., LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|