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.
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ zae-limiter = zae_limiter.cli:cli
@@ -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.