Skip to content

Commit 562edcb

Browse files
committed
Add QuotaExceededError and fix STORAGE_LIMIT_EXCEEDED / COLLECTION_LIMIT_EXCEEDED mapping.
Introduce QuotaExceededError, route storage and collection quota codes to it, guard details.get() calls against None.
1 parent 20b8071 commit 562edcb

4 files changed

Lines changed: 90 additions & 1 deletion

File tree

aetherfy_vectors/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
SchemaValidationError,
1919
SchemaNotFoundError,
2020
CollectionInUseError,
21+
QuotaExceededError,
2122
)
2223
from .models import (
2324
SearchResult,
@@ -42,6 +43,7 @@
4243
"SchemaValidationError",
4344
"SchemaNotFoundError",
4445
"CollectionInUseError",
46+
"QuotaExceededError",
4547
"SearchResult",
4648
"Point",
4749
"Collection",

aetherfy_vectors/exceptions.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,23 @@ def __init__(self, collection_name: str, agents: list, **kwargs):
137137
self.agents = agents
138138

139139

140+
class QuotaExceededError(AetherfyVectorsException):
141+
"""Tier quota exceeded (e.g. collection count limit)."""
142+
143+
def __init__(
144+
self,
145+
message: str,
146+
quota_type: str,
147+
current: Optional[int] = None,
148+
limit: Optional[int] = None,
149+
**kwargs,
150+
):
151+
super().__init__(message, **kwargs)
152+
self.quota_type = quota_type
153+
self.current = current
154+
self.limit = limit
155+
156+
140157
class SchemaNotFoundError(AetherfyVectorsException):
141158
"""No schema defined for collection."""
142159

aetherfy_vectors/utils.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,21 @@ def parse_error_response(
146146
message, request_id=request_id, status_code=status_code, details=details
147147
)
148148
elif status_code == 429:
149-
retry_after = details.get("retry_after")
149+
if error_code == "STORAGE_LIMIT_EXCEEDED":
150+
from .exceptions import QuotaExceededError
151+
152+
current = details.get("current") if isinstance(details, dict) else None
153+
limit = details.get("limit") if isinstance(details, dict) else None
154+
return QuotaExceededError(
155+
message,
156+
"storage",
157+
current=current,
158+
limit=limit,
159+
request_id=request_id,
160+
status_code=status_code,
161+
details=details,
162+
)
163+
retry_after = details.get("retry_after") if isinstance(details, dict) else None
150164
return RateLimitExceededError(
151165
message,
152166
request_id=request_id,
@@ -178,6 +192,20 @@ def parse_error_response(
178192
details=details,
179193
)
180194
elif status_code == 400:
195+
if error_code == "COLLECTION_LIMIT_EXCEEDED":
196+
from .exceptions import QuotaExceededError
197+
198+
current = details.get("current") if isinstance(details, dict) else None
199+
limit = details.get("limit") if isinstance(details, dict) else None
200+
return QuotaExceededError(
201+
message,
202+
"collections",
203+
current=current,
204+
limit=limit,
205+
request_id=request_id,
206+
status_code=status_code,
207+
details=details,
208+
)
181209
return ValidationError(
182210
message, request_id=request_id, status_code=status_code, details=details
183211
)

tests/test_utils.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
PointNotFoundError,
2424
RequestTimeoutError,
2525
AetherfyVectorsException,
26+
QuotaExceededError,
2627
)
2728

2829

@@ -241,6 +242,47 @@ def test_parse_error_response_400(self):
241242
error = parse_error_response(response_data, 400)
242243
assert isinstance(error, ValidationError)
243244

245+
def test_parse_error_response_400_collection_limit_exceeded(self):
246+
"""Test that COLLECTION_LIMIT_EXCEEDED maps to QuotaExceededError, not ValidationError."""
247+
response_data = {
248+
"error": {
249+
"code": "COLLECTION_LIMIT_EXCEEDED",
250+
"message": "Collection limit reached. Your free plan allows 3 collections.",
251+
"current": 3,
252+
"limit": 3,
253+
}
254+
}
255+
error = parse_error_response(response_data, 400)
256+
assert isinstance(error, QuotaExceededError)
257+
assert error.quota_type == "collections"
258+
assert error.current == 3
259+
assert error.limit == 3
260+
assert "Collection limit reached" in str(error)
261+
262+
def test_parse_error_response_429_storage_limit_exceeded(self):
263+
"""Test that STORAGE_LIMIT_EXCEEDED maps to QuotaExceededError, not RateLimitExceededError."""
264+
response_data = {
265+
"error": {
266+
"code": "STORAGE_LIMIT_EXCEEDED",
267+
"message": "Storage limit exceeded. Your free plan allows 512 MB.",
268+
"current": 536870912,
269+
"limit": 536870912,
270+
}
271+
}
272+
error = parse_error_response(response_data, 429)
273+
assert isinstance(error, QuotaExceededError)
274+
assert error.quota_type == "storage"
275+
assert error.current == 536870912
276+
assert error.limit == 536870912
277+
assert "Storage limit exceeded" in str(error)
278+
279+
def test_parse_error_response_429_rate_limit(self):
280+
"""Test that plain 429 without STORAGE_LIMIT_EXCEEDED still maps to RateLimitExceededError."""
281+
from aetherfy_vectors.exceptions import RateLimitExceededError
282+
response_data = {"message": "Too many requests", "request_id": "req_123"}
283+
error = parse_error_response(response_data, 429)
284+
assert isinstance(error, RateLimitExceededError)
285+
244286
def test_parse_error_response_408(self):
245287
"""Test parsing 408 request timeout error."""
246288
response_data = {"message": "Request Timeout", "request_id": "req_123"}

0 commit comments

Comments
 (0)