tango-python 0.4.2__py3-none-any.whl → 0.4.3__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.
- tango/__init__.py +3 -1
- tango/client.py +34 -1
- tango/exceptions.py +28 -1
- tango/models.py +15 -0
- {tango_python-0.4.2.dist-info → tango_python-0.4.3.dist-info}/METADATA +1 -1
- {tango_python-0.4.2.dist-info → tango_python-0.4.3.dist-info}/RECORD +8 -8
- {tango_python-0.4.2.dist-info → tango_python-0.4.3.dist-info}/WHEEL +0 -0
- {tango_python-0.4.2.dist-info → tango_python-0.4.3.dist-info}/licenses/LICENSE +0 -0
tango/__init__.py
CHANGED
|
@@ -11,6 +11,7 @@ from .exceptions import (
|
|
|
11
11
|
from .models import (
|
|
12
12
|
GsaElibraryContract,
|
|
13
13
|
PaginatedResponse,
|
|
14
|
+
RateLimitInfo,
|
|
14
15
|
SearchFilters,
|
|
15
16
|
ShapeConfig,
|
|
16
17
|
WebhookEndpoint,
|
|
@@ -27,7 +28,7 @@ from .shapes import (
|
|
|
27
28
|
TypeGenerator,
|
|
28
29
|
)
|
|
29
30
|
|
|
30
|
-
__version__ = "0.4.
|
|
31
|
+
__version__ = "0.4.3"
|
|
31
32
|
__all__ = [
|
|
32
33
|
"TangoClient",
|
|
33
34
|
"TangoAPIError",
|
|
@@ -35,6 +36,7 @@ __all__ = [
|
|
|
35
36
|
"TangoNotFoundError",
|
|
36
37
|
"TangoValidationError",
|
|
37
38
|
"TangoRateLimitError",
|
|
39
|
+
"RateLimitInfo",
|
|
38
40
|
"GsaElibraryContract",
|
|
39
41
|
"PaginatedResponse",
|
|
40
42
|
"SearchFilters",
|
tango/client.py
CHANGED
|
@@ -32,6 +32,7 @@ from tango.models import (
|
|
|
32
32
|
Organization,
|
|
33
33
|
PaginatedResponse,
|
|
34
34
|
Protest,
|
|
35
|
+
RateLimitInfo,
|
|
35
36
|
SearchFilters,
|
|
36
37
|
ShapeConfig,
|
|
37
38
|
Subaward,
|
|
@@ -77,6 +78,7 @@ class TangoClient:
|
|
|
77
78
|
headers["X-API-KEY"] = self.api_key
|
|
78
79
|
|
|
79
80
|
self.client = httpx.Client(headers=headers, timeout=30.0)
|
|
81
|
+
self._last_rate_limit_info: RateLimitInfo | None = None
|
|
80
82
|
|
|
81
83
|
# Use hardcoded sensible defaults
|
|
82
84
|
cache_size = 100
|
|
@@ -98,6 +100,34 @@ class TangoClient:
|
|
|
98
100
|
# Core HTTP Request Utilities
|
|
99
101
|
# ============================================================================
|
|
100
102
|
|
|
103
|
+
@property
|
|
104
|
+
def rate_limit_info(self) -> RateLimitInfo | None:
|
|
105
|
+
"""Rate limit info from the most recent API response."""
|
|
106
|
+
return self._last_rate_limit_info
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _parse_rate_limit_headers(headers: httpx.Headers) -> RateLimitInfo:
|
|
110
|
+
"""Extract rate limit info from response headers."""
|
|
111
|
+
def _int_or_none(val: str | None) -> int | None:
|
|
112
|
+
if val is None:
|
|
113
|
+
return None
|
|
114
|
+
try:
|
|
115
|
+
return int(val)
|
|
116
|
+
except (ValueError, TypeError):
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
return RateLimitInfo(
|
|
120
|
+
limit=_int_or_none(headers.get("X-RateLimit-Limit")),
|
|
121
|
+
remaining=_int_or_none(headers.get("X-RateLimit-Remaining")),
|
|
122
|
+
reset=_int_or_none(headers.get("X-RateLimit-Reset")),
|
|
123
|
+
daily_limit=_int_or_none(headers.get("X-RateLimit-Daily-Limit")),
|
|
124
|
+
daily_remaining=_int_or_none(headers.get("X-RateLimit-Daily-Remaining")),
|
|
125
|
+
daily_reset=_int_or_none(headers.get("X-RateLimit-Daily-Reset")),
|
|
126
|
+
burst_limit=_int_or_none(headers.get("X-RateLimit-Burst-Limit")),
|
|
127
|
+
burst_remaining=_int_or_none(headers.get("X-RateLimit-Burst-Remaining")),
|
|
128
|
+
burst_reset=_int_or_none(headers.get("X-RateLimit-Burst-Reset")),
|
|
129
|
+
)
|
|
130
|
+
|
|
101
131
|
def _request(
|
|
102
132
|
self,
|
|
103
133
|
method: str,
|
|
@@ -110,6 +140,7 @@ class TangoClient:
|
|
|
110
140
|
|
|
111
141
|
try:
|
|
112
142
|
response = self.client.request(method=method, url=url, params=params, json=json_data)
|
|
143
|
+
self._last_rate_limit_info = self._parse_rate_limit_headers(response.headers)
|
|
113
144
|
|
|
114
145
|
if response.status_code == 401:
|
|
115
146
|
raise TangoAuthError(
|
|
@@ -136,7 +167,9 @@ class TangoClient:
|
|
|
136
167
|
error_data,
|
|
137
168
|
)
|
|
138
169
|
elif response.status_code == 429:
|
|
139
|
-
|
|
170
|
+
error_data = response.json() if response.content else {}
|
|
171
|
+
detail = error_data.get("detail", "Rate limit exceeded")
|
|
172
|
+
raise TangoRateLimitError(detail, response.status_code, error_data)
|
|
140
173
|
elif not response.is_success:
|
|
141
174
|
raise TangoAPIError(
|
|
142
175
|
f"API request failed with status {response.status_code}", response.status_code
|
tango/exceptions.py
CHANGED
|
@@ -39,7 +39,34 @@ class TangoValidationError(TangoAPIError):
|
|
|
39
39
|
class TangoRateLimitError(TangoAPIError):
|
|
40
40
|
"""Rate limit exceeded error"""
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
@property
|
|
43
|
+
def wait_in_seconds(self) -> int | None:
|
|
44
|
+
"""Seconds to wait before retrying, from API response."""
|
|
45
|
+
val = self.response_data.get("wait_in_seconds")
|
|
46
|
+
if val is not None:
|
|
47
|
+
try:
|
|
48
|
+
return int(val)
|
|
49
|
+
except (ValueError, TypeError):
|
|
50
|
+
return None
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def detail(self) -> str | None:
|
|
55
|
+
"""Human-readable detail from API response."""
|
|
56
|
+
return self.response_data.get("detail")
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def limit_type(self) -> str | None:
|
|
60
|
+
"""Which limit was hit: 'burst' or 'daily', parsed from detail."""
|
|
61
|
+
d = self.detail
|
|
62
|
+
if not d:
|
|
63
|
+
return None
|
|
64
|
+
lower = d.lower()
|
|
65
|
+
if "burst" in lower or "minute" in lower:
|
|
66
|
+
return "burst"
|
|
67
|
+
if "daily" in lower or "day" in lower:
|
|
68
|
+
return "daily"
|
|
69
|
+
return None
|
|
43
70
|
|
|
44
71
|
|
|
45
72
|
class ShapeError(TangoAPIError):
|
tango/models.py
CHANGED
|
@@ -23,6 +23,21 @@ T = TypeVar("T")
|
|
|
23
23
|
# ============================================================================
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
@dataclass
|
|
27
|
+
class RateLimitInfo:
|
|
28
|
+
"""Rate limit information from API response headers."""
|
|
29
|
+
|
|
30
|
+
limit: int | None = None
|
|
31
|
+
remaining: int | None = None
|
|
32
|
+
reset: int | None = None
|
|
33
|
+
daily_limit: int | None = None
|
|
34
|
+
daily_remaining: int | None = None
|
|
35
|
+
daily_reset: int | None = None
|
|
36
|
+
burst_limit: int | None = None
|
|
37
|
+
burst_remaining: int | None = None
|
|
38
|
+
burst_reset: int | None = None
|
|
39
|
+
|
|
40
|
+
|
|
26
41
|
@dataclass
|
|
27
42
|
class SearchFilters:
|
|
28
43
|
"""Search filter parameters for contract search
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
tango/__init__.py,sha256=
|
|
2
|
-
tango/client.py,sha256=
|
|
3
|
-
tango/exceptions.py,sha256=
|
|
4
|
-
tango/models.py,sha256=
|
|
1
|
+
tango/__init__.py,sha256=MkZ4VcgIB5WFfCaxK6XXrK25icagQ2o9ZNta64xsqgw,1142
|
|
2
|
+
tango/client.py,sha256=kzUVPPOur45QobKBrRzPXGOOYZHXg-cBEWlSIB8vvAo,87725
|
|
3
|
+
tango/exceptions.py,sha256=aRvDm0dUCEtNDfRVYCX7SEDdd1WlIVVY6sN78Tzo-a0,3114
|
|
4
|
+
tango/models.py,sha256=EDKsZ4fsxkAbDhX5roOfiKYyPjZaTV-5QKrPaQCsb0Y,20959
|
|
5
5
|
tango/shapes/__init__.py,sha256=7ea1WU74jp4znhNw-gXruag6m6eyPZtbVgbDFmFUWro,1072
|
|
6
6
|
tango/shapes/explicit_schemas.py,sha256=H4pYs0LCTSV5msRCxftmgiM_-3sc4LsqpDPgj36DkPY,55202
|
|
7
7
|
tango/shapes/factory.py,sha256=ytpMi5Uw72XZ8MimhuSsLDVXF3zO_Zt3_tAL6NF7LnU,34318
|
|
@@ -10,7 +10,7 @@ tango/shapes/models.py,sha256=h3pIhOqrrdlN953Y6r0oney5HFbKPOD-frRndRWimJ0,3018
|
|
|
10
10
|
tango/shapes/parser.py,sha256=k6OsI2w3GH6-IBbc-XTLgL1mWH7bMf7A_dA6pr1xKfw,24619
|
|
11
11
|
tango/shapes/schema.py,sha256=VRPOB1sBdjFyimNchrZKIpTHn83CyX4RfU9077aQtIU,14136
|
|
12
12
|
tango/shapes/types.py,sha256=27jrAE0VIdrKaLjR_FK71hfIIGX2Tg3ex7REEBV1TFE,1301
|
|
13
|
-
tango_python-0.4.
|
|
14
|
-
tango_python-0.4.
|
|
15
|
-
tango_python-0.4.
|
|
16
|
-
tango_python-0.4.
|
|
13
|
+
tango_python-0.4.3.dist-info/METADATA,sha256=OhV1f_Hifu1hUd2c0zfGc91rIIe07ddJ3Nw3MDnqFl8,17595
|
|
14
|
+
tango_python-0.4.3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
15
|
+
tango_python-0.4.3.dist-info/licenses/LICENSE,sha256=j2kYVHMwTkoGn3ZNScnrdIueG0k2XzB_LCPFoyBc2wk,1064
|
|
16
|
+
tango_python-0.4.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|