simpleapps-com-augur-api 0.8.10__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.
- augur_api/__init__.py +43 -0
- augur_api/client.py +453 -0
- augur_api/core/__init__.py +40 -0
- augur_api/core/config.py +75 -0
- augur_api/core/errors.py +173 -0
- augur_api/core/http_client.py +426 -0
- augur_api/core/schemas.py +105 -0
- augur_api/py.typed +0 -0
- augur_api/services/__init__.py +13 -0
- augur_api/services/agr_info/__init__.py +47 -0
- augur_api/services/agr_info/client.py +326 -0
- augur_api/services/agr_info/schemas.py +123 -0
- augur_api/services/agr_site/__init__.py +79 -0
- augur_api/services/agr_site/client.py +384 -0
- augur_api/services/agr_site/schemas.py +268 -0
- augur_api/services/agr_work/__init__.py +7 -0
- augur_api/services/agr_work/client.py +32 -0
- augur_api/services/agr_work/schemas.py +11 -0
- augur_api/services/avalara/__init__.py +17 -0
- augur_api/services/avalara/client.py +64 -0
- augur_api/services/avalara/schemas.py +34 -0
- augur_api/services/base.py +54 -0
- augur_api/services/basecamp2/__init__.py +65 -0
- augur_api/services/basecamp2/client.py +568 -0
- augur_api/services/basecamp2/schemas.py +227 -0
- augur_api/services/brand_folder/__init__.py +31 -0
- augur_api/services/brand_folder/client.py +206 -0
- augur_api/services/brand_folder/schemas.py +133 -0
- augur_api/services/commerce/__init__.py +56 -0
- augur_api/services/commerce/client.py +298 -0
- augur_api/services/commerce/schemas.py +167 -0
- augur_api/services/customers/__init__.py +69 -0
- augur_api/services/customers/client.py +437 -0
- augur_api/services/customers/schemas.py +273 -0
- augur_api/services/gregorovich/__init__.py +31 -0
- augur_api/services/gregorovich/client.py +151 -0
- augur_api/services/gregorovich/schemas.py +42 -0
- augur_api/services/items/__init__.py +302 -0
- augur_api/services/items/client.py +1223 -0
- augur_api/services/items/schemas.py +722 -0
- augur_api/services/joomla/__init__.py +59 -0
- augur_api/services/joomla/client.py +333 -0
- augur_api/services/joomla/schemas.py +286 -0
- augur_api/services/legacy/__init__.py +66 -0
- augur_api/services/legacy/client.py +391 -0
- augur_api/services/legacy/schemas.py +115 -0
- augur_api/services/logistics/__init__.py +34 -0
- augur_api/services/logistics/client.py +116 -0
- augur_api/services/logistics/schemas.py +65 -0
- augur_api/services/nexus/__init__.py +89 -0
- augur_api/services/nexus/client.py +589 -0
- augur_api/services/nexus/schemas.py +171 -0
- augur_api/services/open_search/__init__.py +58 -0
- augur_api/services/open_search/client.py +285 -0
- augur_api/services/open_search/schemas.py +146 -0
- augur_api/services/orders/__init__.py +51 -0
- augur_api/services/orders/client.py +299 -0
- augur_api/services/orders/schemas.py +195 -0
- augur_api/services/p21_apis/__init__.py +83 -0
- augur_api/services/p21_apis/client.py +420 -0
- augur_api/services/p21_apis/schemas.py +130 -0
- augur_api/services/p21_core/__init__.py +29 -0
- augur_api/services/p21_core/client.py +395 -0
- augur_api/services/p21_core/schemas.py +221 -0
- augur_api/services/p21_pim/__init__.py +51 -0
- augur_api/services/p21_pim/client.py +319 -0
- augur_api/services/p21_pim/schemas.py +128 -0
- augur_api/services/p21_sism/__init__.py +60 -0
- augur_api/services/p21_sism/client.py +334 -0
- augur_api/services/p21_sism/schemas.py +92 -0
- augur_api/services/payments/__init__.py +97 -0
- augur_api/services/payments/client.py +508 -0
- augur_api/services/payments/schemas.py +166 -0
- augur_api/services/pricing/__init__.py +43 -0
- augur_api/services/pricing/client.py +175 -0
- augur_api/services/pricing/schemas.py +146 -0
- augur_api/services/resource.py +141 -0
- augur_api/services/shipping/__init__.py +17 -0
- augur_api/services/shipping/client.py +68 -0
- augur_api/services/shipping/schemas.py +38 -0
- augur_api/services/slack/__init__.py +23 -0
- augur_api/services/slack/client.py +74 -0
- augur_api/services/slack/schemas.py +35 -0
- augur_api/services/smarty_streets/__init__.py +19 -0
- augur_api/services/smarty_streets/client.py +82 -0
- augur_api/services/smarty_streets/schemas.py +32 -0
- augur_api/services/ups/__init__.py +17 -0
- augur_api/services/ups/client.py +72 -0
- augur_api/services/ups/schemas.py +41 -0
- augur_api/services/vmi/__init__.py +157 -0
- augur_api/services/vmi/client.py +586 -0
- augur_api/services/vmi/schemas.py +285 -0
- simpleapps_com_augur_api-0.8.10.dist-info/METADATA +177 -0
- simpleapps_com_augur_api-0.8.10.dist-info/RECORD +96 -0
- simpleapps_com_augur_api-0.8.10.dist-info/WHEEL +4 -0
- simpleapps_com_augur_api-0.8.10.dist-info/licenses/LICENSE +21 -0
augur_api/core/errors.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Error classes for the Augur API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AugurError(Exception):
|
|
9
|
+
"""Base error class for all Augur API errors.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
message: Human-readable error message.
|
|
13
|
+
code: Error code string (e.g., 'API_ERROR', 'NETWORK_ERROR').
|
|
14
|
+
status_code: HTTP status code if applicable.
|
|
15
|
+
service: The service that raised the error.
|
|
16
|
+
endpoint: The endpoint that raised the error.
|
|
17
|
+
request_id: Optional request ID for debugging.
|
|
18
|
+
validation_errors: Optional list of validation errors.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
message: str,
|
|
24
|
+
code: str,
|
|
25
|
+
status_code: int,
|
|
26
|
+
service: str,
|
|
27
|
+
endpoint: str,
|
|
28
|
+
request_id: str | None = None,
|
|
29
|
+
validation_errors: list[dict[str, Any]] | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Initialize the error.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
message: Human-readable error message.
|
|
35
|
+
code: Error code string.
|
|
36
|
+
status_code: HTTP status code.
|
|
37
|
+
service: The service that raised the error.
|
|
38
|
+
endpoint: The endpoint that raised the error.
|
|
39
|
+
request_id: Optional request ID for debugging.
|
|
40
|
+
validation_errors: Optional list of validation errors.
|
|
41
|
+
"""
|
|
42
|
+
super().__init__(message)
|
|
43
|
+
self.message = message
|
|
44
|
+
self.code = code
|
|
45
|
+
self.status_code = status_code
|
|
46
|
+
self.service = service
|
|
47
|
+
self.endpoint = endpoint
|
|
48
|
+
self.request_id = request_id
|
|
49
|
+
self.validation_errors = validation_errors
|
|
50
|
+
|
|
51
|
+
def __str__(self) -> str:
|
|
52
|
+
"""Return string representation of the error."""
|
|
53
|
+
parts = [f"{self.code}: {self.message}"]
|
|
54
|
+
parts.append(f"(service={self.service}, endpoint={self.endpoint})")
|
|
55
|
+
if self.request_id:
|
|
56
|
+
parts.append(f"[request_id={self.request_id}]")
|
|
57
|
+
return " ".join(parts)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ValidationError(AugurError):
|
|
61
|
+
"""Error raised when request or response validation fails.
|
|
62
|
+
|
|
63
|
+
Provides detailed information about which fields failed validation.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
message: str,
|
|
69
|
+
service: str,
|
|
70
|
+
endpoint: str,
|
|
71
|
+
validation_errors: list[dict[str, Any]],
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Initialize the validation error.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
message: Human-readable error message.
|
|
77
|
+
service: The service that raised the error.
|
|
78
|
+
endpoint: The endpoint that raised the error.
|
|
79
|
+
validation_errors: List of validation error details.
|
|
80
|
+
"""
|
|
81
|
+
super().__init__(
|
|
82
|
+
message=message,
|
|
83
|
+
code="VALIDATION_ERROR",
|
|
84
|
+
status_code=400,
|
|
85
|
+
service=service,
|
|
86
|
+
endpoint=endpoint,
|
|
87
|
+
validation_errors=validation_errors,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def get_formatted_errors(self) -> list[str]:
|
|
91
|
+
"""Format validation errors into human-readable messages.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
List of formatted error messages.
|
|
95
|
+
"""
|
|
96
|
+
if not self.validation_errors:
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
formatted = []
|
|
100
|
+
for error in self.validation_errors:
|
|
101
|
+
path = ".".join(str(p) for p in error.get("loc", [])) or "root"
|
|
102
|
+
msg = error.get("msg", "Unknown error")
|
|
103
|
+
formatted.append(f"{path}: {msg}")
|
|
104
|
+
return formatted
|
|
105
|
+
|
|
106
|
+
def __str__(self) -> str:
|
|
107
|
+
"""Return string representation with validation details."""
|
|
108
|
+
base = super().__str__()
|
|
109
|
+
formatted = self.get_formatted_errors()
|
|
110
|
+
if formatted:
|
|
111
|
+
errors_str = "\n - ".join(formatted)
|
|
112
|
+
return f"{base}\nValidation errors:\n - {errors_str}"
|
|
113
|
+
return base
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class AuthenticationError(AugurError):
|
|
117
|
+
"""Error raised when authentication fails (HTTP 401)."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, message: str, service: str, endpoint: str) -> None:
|
|
120
|
+
"""Initialize the authentication error.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
message: Human-readable error message.
|
|
124
|
+
service: The service that raised the error.
|
|
125
|
+
endpoint: The endpoint that raised the error.
|
|
126
|
+
"""
|
|
127
|
+
super().__init__(
|
|
128
|
+
message=message,
|
|
129
|
+
code="AUTHENTICATION_ERROR",
|
|
130
|
+
status_code=401,
|
|
131
|
+
service=service,
|
|
132
|
+
endpoint=endpoint,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class NotFoundError(AugurError):
|
|
137
|
+
"""Error raised when a resource is not found (HTTP 404)."""
|
|
138
|
+
|
|
139
|
+
def __init__(self, message: str, service: str, endpoint: str) -> None:
|
|
140
|
+
"""Initialize the not found error.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
message: Human-readable error message.
|
|
144
|
+
service: The service that raised the error.
|
|
145
|
+
endpoint: The endpoint that raised the error.
|
|
146
|
+
"""
|
|
147
|
+
super().__init__(
|
|
148
|
+
message=message,
|
|
149
|
+
code="NOT_FOUND",
|
|
150
|
+
status_code=404,
|
|
151
|
+
service=service,
|
|
152
|
+
endpoint=endpoint,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class RateLimitError(AugurError):
|
|
157
|
+
"""Error raised when rate limit is exceeded (HTTP 429)."""
|
|
158
|
+
|
|
159
|
+
def __init__(self, message: str, service: str, endpoint: str) -> None:
|
|
160
|
+
"""Initialize the rate limit error.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
message: Human-readable error message.
|
|
164
|
+
service: The service that raised the error.
|
|
165
|
+
endpoint: The endpoint that raised the error.
|
|
166
|
+
"""
|
|
167
|
+
super().__init__(
|
|
168
|
+
message=message,
|
|
169
|
+
code="RATE_LIMIT_EXCEEDED",
|
|
170
|
+
status_code=429,
|
|
171
|
+
service=service,
|
|
172
|
+
endpoint=endpoint,
|
|
173
|
+
)
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"""HTTP client for making requests to Augur API services."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import random
|
|
8
|
+
import time
|
|
9
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from augur_api.core.errors import (
|
|
14
|
+
AugurError,
|
|
15
|
+
AuthenticationError,
|
|
16
|
+
NotFoundError,
|
|
17
|
+
RateLimitError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from augur_api.core.config import AugurAPIConfig
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
|
|
25
|
+
# Service base URLs
|
|
26
|
+
SERVICE_BASE_URLS: dict[str, str] = {
|
|
27
|
+
"agr-info": "https://agr-info.augur-api.com",
|
|
28
|
+
"agr-site": "https://agr-site.augur-api.com",
|
|
29
|
+
"agr-work": "https://agr-work.augur-api.com",
|
|
30
|
+
"avalara": "https://avalara.augur-api.com",
|
|
31
|
+
"basecamp2": "https://basecamp2.augur-api.com",
|
|
32
|
+
"brand-folder": "https://brand-folder.augur-api.com",
|
|
33
|
+
"commerce": "https://commerce.augur-api.com",
|
|
34
|
+
"customers": "https://customers.augur-api.com",
|
|
35
|
+
"gregorovich": "https://gregorovich.augur-api.com",
|
|
36
|
+
"items": "https://items.augur-api.com",
|
|
37
|
+
"joomla": "https://joomla.augur-api.com",
|
|
38
|
+
"legacy": "https://legacy.augur-api.com",
|
|
39
|
+
"logistics": "https://logistics.augur-api.com",
|
|
40
|
+
"nexus": "https://nexus.augur-api.com",
|
|
41
|
+
"open-search": "https://open-search.augur-api.com",
|
|
42
|
+
"orders": "https://orders.augur-api.com",
|
|
43
|
+
"p21-apis": "https://p21-apis.augur-api.com",
|
|
44
|
+
"p21-core": "https://p21-core.augur-api.com",
|
|
45
|
+
"p21-pim": "https://p21-pim.augur-api.com",
|
|
46
|
+
"p21-sism": "https://p21-sism.augur-api.com",
|
|
47
|
+
"payments": "https://payments.augur-api.com",
|
|
48
|
+
"pricing": "https://pricing.augur-api.com",
|
|
49
|
+
"shipping": "https://shipping.augur-api.com",
|
|
50
|
+
"slack": "https://slack.augur-api.com",
|
|
51
|
+
"smarty-streets": "https://smarty-streets.augur-api.com",
|
|
52
|
+
"ups": "https://ups.augur-api.com",
|
|
53
|
+
"vmi": "https://vmi.augur-api.com",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _generate_request_key(method: str, url: str, params: Any | None = None) -> str:
|
|
58
|
+
"""Generate a unique key for request deduplication.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
method: HTTP method.
|
|
62
|
+
url: Request URL.
|
|
63
|
+
params: Query parameters or request body.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Unique string key for this request.
|
|
67
|
+
"""
|
|
68
|
+
params_str = ""
|
|
69
|
+
if params:
|
|
70
|
+
# Sort keys for consistent hashing
|
|
71
|
+
params_str = json.dumps(params, sort_keys=True)
|
|
72
|
+
combined = f"{method}:{url}:{params_str}"
|
|
73
|
+
return hashlib.md5(combined.encode()).hexdigest() # noqa: S324
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _calculate_backoff_delay(attempt: int, base_delay: float) -> float:
|
|
77
|
+
"""Calculate delay for exponential backoff with jitter.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
attempt: Current attempt number (0-indexed).
|
|
81
|
+
base_delay: Base delay in seconds.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Delay in seconds with jitter.
|
|
85
|
+
"""
|
|
86
|
+
exponential_delay = base_delay * (2**attempt)
|
|
87
|
+
jitter = random.random() * 0.3 * exponential_delay # noqa: S311
|
|
88
|
+
delay = exponential_delay + jitter
|
|
89
|
+
return delay if delay < 30.0 else 30.0 # Cap at 30 seconds
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _is_retryable_error(status_code: int | None) -> bool:
|
|
93
|
+
"""Determine if an error is retryable.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
status_code: HTTP status code, or None for network errors.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
True if the request should be retried.
|
|
100
|
+
"""
|
|
101
|
+
# Network errors (no status) are retryable
|
|
102
|
+
if status_code is None:
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
# Rate limit (429) and server errors (5xx) are retryable
|
|
106
|
+
return status_code == 429 or (status_code >= 500 and status_code < 600)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class HTTPClient:
|
|
110
|
+
"""HTTP client for making requests to a specific Augur service.
|
|
111
|
+
|
|
112
|
+
Handles authentication, retries, request deduplication, and error mapping.
|
|
113
|
+
|
|
114
|
+
Attributes:
|
|
115
|
+
service_name: Name of the service (e.g., 'items', 'customers').
|
|
116
|
+
config: Client configuration.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(self, service_name: str, config: AugurAPIConfig) -> None:
|
|
120
|
+
"""Initialize the HTTP client.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
service_name: Name of the service.
|
|
124
|
+
config: Client configuration.
|
|
125
|
+
"""
|
|
126
|
+
self.service_name = service_name
|
|
127
|
+
self.config = config
|
|
128
|
+
self._base_url = SERVICE_BASE_URLS.get(
|
|
129
|
+
service_name, f"https://{service_name}.augur-api.com"
|
|
130
|
+
)
|
|
131
|
+
self._inflight_requests: dict[str, dict[str, Any]] = {}
|
|
132
|
+
self._client = httpx.Client(timeout=config.timeout)
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def base_url(self) -> str:
|
|
136
|
+
"""Get the base URL for this service."""
|
|
137
|
+
return self._base_url
|
|
138
|
+
|
|
139
|
+
def _get_headers(self, endpoint: str) -> dict[str, str]:
|
|
140
|
+
"""Build request headers.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
endpoint: The endpoint path being called.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Dictionary of headers.
|
|
147
|
+
"""
|
|
148
|
+
headers: dict[str, str] = {
|
|
149
|
+
"x-site-id": self.config.site_id,
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
"Accept": "application/json",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Add bearer token for non-public endpoints
|
|
155
|
+
is_public = endpoint.endswith(("/health-check", "/ping"))
|
|
156
|
+
if not is_public and self.config.token:
|
|
157
|
+
headers["Authorization"] = f"Bearer {self.config.token}"
|
|
158
|
+
|
|
159
|
+
return headers
|
|
160
|
+
|
|
161
|
+
def _handle_http_error(self, response: httpx.Response, endpoint: str) -> None:
|
|
162
|
+
"""Handle HTTP error responses by raising appropriate exceptions.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
response: The HTTP response.
|
|
166
|
+
endpoint: The endpoint that was called.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
AuthenticationError: For 401 responses.
|
|
170
|
+
NotFoundError: For 404 responses.
|
|
171
|
+
RateLimitError: For 429 responses.
|
|
172
|
+
AugurError: For other error responses.
|
|
173
|
+
"""
|
|
174
|
+
status = response.status_code
|
|
175
|
+
|
|
176
|
+
# Try to extract error details from response
|
|
177
|
+
try:
|
|
178
|
+
data = response.json()
|
|
179
|
+
message = data.get("message", f"Request failed with status {status}")
|
|
180
|
+
code = data.get("code", "API_ERROR")
|
|
181
|
+
request_id = data.get("requestId")
|
|
182
|
+
except Exception:
|
|
183
|
+
message = f"Request failed with status {status}"
|
|
184
|
+
code = "API_ERROR"
|
|
185
|
+
request_id = None
|
|
186
|
+
|
|
187
|
+
if status == 401:
|
|
188
|
+
raise AuthenticationError(
|
|
189
|
+
message=message or "Authentication failed",
|
|
190
|
+
service=self.service_name,
|
|
191
|
+
endpoint=endpoint,
|
|
192
|
+
)
|
|
193
|
+
if status == 404:
|
|
194
|
+
raise NotFoundError(
|
|
195
|
+
message=message or "Resource not found",
|
|
196
|
+
service=self.service_name,
|
|
197
|
+
endpoint=endpoint,
|
|
198
|
+
)
|
|
199
|
+
if status == 429:
|
|
200
|
+
raise RateLimitError(
|
|
201
|
+
message=message or "Rate limit exceeded",
|
|
202
|
+
service=self.service_name,
|
|
203
|
+
endpoint=endpoint,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
raise AugurError(
|
|
207
|
+
message=message,
|
|
208
|
+
code=code,
|
|
209
|
+
status_code=status,
|
|
210
|
+
service=self.service_name,
|
|
211
|
+
endpoint=endpoint,
|
|
212
|
+
request_id=request_id,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def _execute_with_retry(
|
|
216
|
+
self,
|
|
217
|
+
method: str,
|
|
218
|
+
url: str,
|
|
219
|
+
params: dict[str, Any] | None = None,
|
|
220
|
+
json_data: Any | None = None,
|
|
221
|
+
) -> httpx.Response:
|
|
222
|
+
"""Execute a request with retry logic.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
method: HTTP method.
|
|
226
|
+
url: Full URL to request.
|
|
227
|
+
params: Query parameters.
|
|
228
|
+
json_data: JSON body data.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
HTTP response.
|
|
232
|
+
|
|
233
|
+
Raises:
|
|
234
|
+
AugurError: For network or API errors.
|
|
235
|
+
"""
|
|
236
|
+
max_retries = self.config.retries
|
|
237
|
+
base_delay = self.config.retry_delay
|
|
238
|
+
endpoint = url.replace(self._base_url, "")
|
|
239
|
+
|
|
240
|
+
for attempt in range(max_retries + 1):
|
|
241
|
+
try:
|
|
242
|
+
headers = self._get_headers(endpoint)
|
|
243
|
+
response = self._client.request(
|
|
244
|
+
method=method,
|
|
245
|
+
url=url,
|
|
246
|
+
params=params,
|
|
247
|
+
json=json_data,
|
|
248
|
+
headers=headers,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Check for HTTP errors
|
|
252
|
+
if response.status_code >= 400:
|
|
253
|
+
# Only retry on retryable errors
|
|
254
|
+
if attempt < max_retries and _is_retryable_error(response.status_code):
|
|
255
|
+
delay = _calculate_backoff_delay(attempt, base_delay)
|
|
256
|
+
time.sleep(delay)
|
|
257
|
+
continue
|
|
258
|
+
self._handle_http_error(response, endpoint)
|
|
259
|
+
|
|
260
|
+
return response
|
|
261
|
+
|
|
262
|
+
except httpx.RequestError as e:
|
|
263
|
+
# Network errors are retryable
|
|
264
|
+
if attempt < max_retries:
|
|
265
|
+
delay = _calculate_backoff_delay(attempt, base_delay)
|
|
266
|
+
time.sleep(delay)
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
raise AugurError(
|
|
270
|
+
message=str(e),
|
|
271
|
+
code="NETWORK_ERROR",
|
|
272
|
+
status_code=0,
|
|
273
|
+
service=self.service_name,
|
|
274
|
+
endpoint=endpoint,
|
|
275
|
+
) from e
|
|
276
|
+
|
|
277
|
+
# Should not reach here, but satisfy type checker
|
|
278
|
+
raise AugurError( # pragma: no cover
|
|
279
|
+
message="Max retries exceeded",
|
|
280
|
+
code="MAX_RETRIES",
|
|
281
|
+
status_code=0,
|
|
282
|
+
service=self.service_name,
|
|
283
|
+
endpoint=endpoint,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def _transform_edge_cache_params(self, params: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
287
|
+
"""Transform edge_cache parameter to Cloudflare's cacheSiteId format.
|
|
288
|
+
|
|
289
|
+
Cloudflare expects cacheSiteId{suffix}=<site_id> where suffix indicates duration:
|
|
290
|
+
- '30s', '1m', '5m' for sub-hour caches
|
|
291
|
+
- 1, 2, 3, 4, 5, 8 for hourly caches
|
|
292
|
+
|
|
293
|
+
Examples:
|
|
294
|
+
- edge_cache: '30s' → cacheSiteId30s: <site_id>
|
|
295
|
+
- edge_cache: '1m' → cacheSiteId1m: <site_id>
|
|
296
|
+
- edge_cache: '5m' → cacheSiteId5m: <site_id>
|
|
297
|
+
- edge_cache: 1 → cacheSiteId1: <site_id>
|
|
298
|
+
- edge_cache: 8 → cacheSiteId8: <site_id>
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
params: Original request parameters.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Parameters with edge_cache transformed to cacheSiteId{suffix}.
|
|
305
|
+
"""
|
|
306
|
+
if not params:
|
|
307
|
+
return params
|
|
308
|
+
|
|
309
|
+
edge_cache = params.get("edge_cache")
|
|
310
|
+
|
|
311
|
+
# If no edge_cache param, return original params
|
|
312
|
+
if edge_cache is None:
|
|
313
|
+
return params
|
|
314
|
+
|
|
315
|
+
# Valid sub-hour values (strings only)
|
|
316
|
+
valid_sub_hour = {"30s", "1m", "5m"}
|
|
317
|
+
# Valid hourly values
|
|
318
|
+
valid_hourly = {1, 2, 3, 4, 5, 8}
|
|
319
|
+
|
|
320
|
+
edge_cache_str = str(edge_cache)
|
|
321
|
+
|
|
322
|
+
if edge_cache_str in valid_sub_hour:
|
|
323
|
+
# Sub-hour cache: '30s', '1m', '5m'
|
|
324
|
+
cache_suffix = edge_cache_str
|
|
325
|
+
else:
|
|
326
|
+
# Try to parse as hourly value
|
|
327
|
+
try:
|
|
328
|
+
cache_hours = int(edge_cache_str)
|
|
329
|
+
except ValueError:
|
|
330
|
+
# Invalid value - return params without edge_cache
|
|
331
|
+
return {k: v for k, v in params.items() if k != "edge_cache"}
|
|
332
|
+
|
|
333
|
+
if cache_hours not in valid_hourly:
|
|
334
|
+
# Invalid value - return params without edge_cache
|
|
335
|
+
return {k: v for k, v in params.items() if k != "edge_cache"}
|
|
336
|
+
|
|
337
|
+
cache_suffix = str(cache_hours)
|
|
338
|
+
|
|
339
|
+
# Transform to Cloudflare format: cacheSiteId{suffix}=<site_id>
|
|
340
|
+
cache_key = f"cacheSiteId{cache_suffix}"
|
|
341
|
+
result = {k: v for k, v in params.items() if k != "edge_cache"}
|
|
342
|
+
result[cache_key] = self.config.site_id
|
|
343
|
+
|
|
344
|
+
return result
|
|
345
|
+
|
|
346
|
+
def get(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
347
|
+
"""Make a GET request.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
path: Endpoint path (e.g., '/health-check').
|
|
351
|
+
params: Query parameters.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Response JSON as a dictionary.
|
|
355
|
+
"""
|
|
356
|
+
# Transform edge_cache to cacheSiteId{N} format for Cloudflare
|
|
357
|
+
transformed_params = self._transform_edge_cache_params(params)
|
|
358
|
+
url = f"{self._base_url}{path}"
|
|
359
|
+
|
|
360
|
+
# Request deduplication for GET requests
|
|
361
|
+
request_key = _generate_request_key("GET", url, transformed_params)
|
|
362
|
+
if request_key in self._inflight_requests:
|
|
363
|
+
return self._inflight_requests[request_key]
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
response = self._execute_with_retry("GET", url, params=transformed_params)
|
|
367
|
+
result: dict[str, Any] = response.json()
|
|
368
|
+
return result
|
|
369
|
+
finally:
|
|
370
|
+
# Clean up inflight tracking
|
|
371
|
+
self._inflight_requests.pop(request_key, None)
|
|
372
|
+
|
|
373
|
+
def post(
|
|
374
|
+
self, path: str, data: Any | None = None, params: dict[str, Any] | None = None
|
|
375
|
+
) -> dict[str, Any]:
|
|
376
|
+
"""Make a POST request.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
path: Endpoint path.
|
|
380
|
+
data: Request body data.
|
|
381
|
+
params: Query parameters.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Response JSON as a dictionary.
|
|
385
|
+
"""
|
|
386
|
+
url = f"{self._base_url}{path}"
|
|
387
|
+
response = self._execute_with_retry("POST", url, params=params, json_data=data)
|
|
388
|
+
result: dict[str, Any] = response.json()
|
|
389
|
+
return result
|
|
390
|
+
|
|
391
|
+
def put(
|
|
392
|
+
self, path: str, data: Any | None = None, params: dict[str, Any] | None = None
|
|
393
|
+
) -> dict[str, Any]:
|
|
394
|
+
"""Make a PUT request.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
path: Endpoint path.
|
|
398
|
+
data: Request body data.
|
|
399
|
+
params: Query parameters.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Response JSON as a dictionary.
|
|
403
|
+
"""
|
|
404
|
+
url = f"{self._base_url}{path}"
|
|
405
|
+
response = self._execute_with_retry("PUT", url, params=params, json_data=data)
|
|
406
|
+
result: dict[str, Any] = response.json()
|
|
407
|
+
return result
|
|
408
|
+
|
|
409
|
+
def delete(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
410
|
+
"""Make a DELETE request.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
path: Endpoint path.
|
|
414
|
+
params: Query parameters.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Response JSON as a dictionary.
|
|
418
|
+
"""
|
|
419
|
+
url = f"{self._base_url}{path}"
|
|
420
|
+
response = self._execute_with_retry("DELETE", url, params=params)
|
|
421
|
+
result: dict[str, Any] = response.json()
|
|
422
|
+
return result
|
|
423
|
+
|
|
424
|
+
def close(self) -> None:
|
|
425
|
+
"""Close the HTTP client and release resources."""
|
|
426
|
+
self._client.close()
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Base schemas for Augur API responses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
from pydantic.alias_generators import to_camel
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CamelCaseModel(BaseModel):
|
|
14
|
+
"""Base model that handles camelCase to snake_case conversion.
|
|
15
|
+
|
|
16
|
+
The Augur API returns camelCase field names, but Python convention
|
|
17
|
+
uses snake_case. This model automatically handles the conversion.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
model_config = ConfigDict(
|
|
21
|
+
extra="allow",
|
|
22
|
+
populate_by_name=True,
|
|
23
|
+
alias_generator=to_camel,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BaseResponse(CamelCaseModel, Generic[T]):
|
|
28
|
+
"""Base response schema for all Augur API responses.
|
|
29
|
+
|
|
30
|
+
All API responses follow this structure, wrapping the actual data
|
|
31
|
+
with metadata about the request and response.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
count: Number of items in the current response.
|
|
35
|
+
data: The actual response data (type varies by endpoint).
|
|
36
|
+
message: Response message from the API.
|
|
37
|
+
options: Additional options (supports both array and object formats).
|
|
38
|
+
params: Parameters used in the request.
|
|
39
|
+
status: HTTP status code.
|
|
40
|
+
total: Total number of results available.
|
|
41
|
+
total_results: Total results (alias handled by CamelCaseModel).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
count: int
|
|
45
|
+
data: T
|
|
46
|
+
message: str
|
|
47
|
+
options: list[Any] | dict[str, Any]
|
|
48
|
+
params: list[Any] | dict[str, Any]
|
|
49
|
+
status: int
|
|
50
|
+
total: int
|
|
51
|
+
total_results: int
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class HealthCheckData(CamelCaseModel):
|
|
55
|
+
"""Health check response data.
|
|
56
|
+
|
|
57
|
+
Returned by all service /health-check endpoints.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
site_hash: Hash of the site configuration.
|
|
61
|
+
site_id: Site identifier.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
site_hash: str
|
|
65
|
+
site_id: str
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class PingData(BaseModel):
|
|
69
|
+
"""Ping response data.
|
|
70
|
+
|
|
71
|
+
Simple connectivity check - returns 'pong'.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
model_config = ConfigDict(extra="allow")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class PaginationParams(BaseModel):
|
|
78
|
+
"""Common pagination parameters for list endpoints.
|
|
79
|
+
|
|
80
|
+
Attributes:
|
|
81
|
+
limit: Maximum number of items to return.
|
|
82
|
+
offset: Number of items to skip.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
limit: int | None = None
|
|
86
|
+
offset: int | None = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class EdgeCacheParams(BaseModel):
|
|
90
|
+
"""Edge cache duration parameters.
|
|
91
|
+
|
|
92
|
+
Supported values for Cloudflare cache rules.
|
|
93
|
+
|
|
94
|
+
Attributes:
|
|
95
|
+
edge_cache: Cache duration - sub-hour ('30s', '1m', '5m') or hourly (1-5, 8).
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
edge_cache: str | int | None = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class BaseGetParams(PaginationParams, EdgeCacheParams):
|
|
102
|
+
"""Combined base parameters for GET requests.
|
|
103
|
+
|
|
104
|
+
Includes both pagination and edge caching options.
|
|
105
|
+
"""
|
augur_api/py.typed
ADDED
|
File without changes
|