agentic-fabriq-sdk 0.1.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.
Potentially problematic release.
This version of agentic-fabriq-sdk might be problematic. Click here for more details.
- af_sdk/__init__.py +55 -0
- af_sdk/auth/__init__.py +31 -0
- af_sdk/auth/dpop.py +43 -0
- af_sdk/auth/oauth.py +247 -0
- af_sdk/auth/token_cache.py +318 -0
- af_sdk/connectors/__init__.py +23 -0
- af_sdk/connectors/base.py +231 -0
- af_sdk/connectors/registry.py +262 -0
- af_sdk/dx/__init__.py +12 -0
- af_sdk/dx/decorators.py +40 -0
- af_sdk/dx/runtime.py +170 -0
- af_sdk/events.py +699 -0
- af_sdk/exceptions.py +140 -0
- af_sdk/fabriq_client.py +198 -0
- af_sdk/models/__init__.py +47 -0
- af_sdk/models/audit.py +44 -0
- af_sdk/models/types.py +242 -0
- af_sdk/py.typed +0 -0
- af_sdk/transport/__init__.py +7 -0
- af_sdk/transport/http.py +366 -0
- af_sdk/vault.py +500 -0
- agentic_fabriq_sdk-0.1.3.dist-info/METADATA +81 -0
- agentic_fabriq_sdk-0.1.3.dist-info/RECORD +24 -0
- agentic_fabriq_sdk-0.1.3.dist-info/WHEEL +4 -0
af_sdk/transport/http.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP transport layer for Agentic Fabric SDK.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
from urllib.parse import urljoin
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
from opentelemetry import trace
|
|
13
|
+
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
|
|
14
|
+
|
|
15
|
+
from ..exceptions import (
|
|
16
|
+
AuthenticationError,
|
|
17
|
+
AuthorizationError,
|
|
18
|
+
NotFoundError,
|
|
19
|
+
RateLimitError,
|
|
20
|
+
ServiceUnavailableError,
|
|
21
|
+
UpstreamError,
|
|
22
|
+
ValidationError,
|
|
23
|
+
create_exception_from_response,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HTTPClient:
|
|
28
|
+
"""HTTP client with retries, tracing, and error handling."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
base_url: str,
|
|
33
|
+
timeout: float = 30.0,
|
|
34
|
+
retries: int = 3,
|
|
35
|
+
backoff_factor: float = 0.5,
|
|
36
|
+
logger: Optional[logging.Logger] = None,
|
|
37
|
+
auth_token: Optional[str] = None,
|
|
38
|
+
user_agent: str = "agentic-fabriq-sdk/1.0.0",
|
|
39
|
+
trace_enabled: bool = True,
|
|
40
|
+
):
|
|
41
|
+
self.base_url = base_url.rstrip("/")
|
|
42
|
+
self.timeout = timeout
|
|
43
|
+
self.retries = retries
|
|
44
|
+
self.backoff_factor = backoff_factor
|
|
45
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
46
|
+
self.auth_token = auth_token
|
|
47
|
+
self.user_agent = user_agent
|
|
48
|
+
self.trace_enabled = trace_enabled
|
|
49
|
+
|
|
50
|
+
# Configure HTTP client
|
|
51
|
+
self.client = httpx.AsyncClient(
|
|
52
|
+
timeout=httpx.Timeout(timeout),
|
|
53
|
+
headers={
|
|
54
|
+
"User-Agent": user_agent,
|
|
55
|
+
"Accept": "application/json",
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Enable OpenTelemetry tracing
|
|
61
|
+
if trace_enabled:
|
|
62
|
+
HTTPXClientInstrumentor().instrument_client(self.client)
|
|
63
|
+
|
|
64
|
+
self.tracer = trace.get_tracer(__name__)
|
|
65
|
+
|
|
66
|
+
async def __aenter__(self):
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
70
|
+
await self.close()
|
|
71
|
+
|
|
72
|
+
async def close(self):
|
|
73
|
+
"""Close the HTTP client."""
|
|
74
|
+
await self.client.aclose()
|
|
75
|
+
|
|
76
|
+
def _build_url(self, path: str) -> str:
|
|
77
|
+
"""Build full URL from base URL and path."""
|
|
78
|
+
return urljoin(self.base_url + "/", path.lstrip("/"))
|
|
79
|
+
|
|
80
|
+
def _prepare_headers(self, headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
|
|
81
|
+
"""Prepare headers for request."""
|
|
82
|
+
request_headers = headers.copy() if headers else {}
|
|
83
|
+
|
|
84
|
+
if self.auth_token:
|
|
85
|
+
request_headers.setdefault("Authorization", f"Bearer {self.auth_token}")
|
|
86
|
+
|
|
87
|
+
return request_headers
|
|
88
|
+
|
|
89
|
+
async def _make_request(
|
|
90
|
+
self,
|
|
91
|
+
method: str,
|
|
92
|
+
path: str,
|
|
93
|
+
headers: Optional[Dict[str, str]] = None,
|
|
94
|
+
params: Optional[Dict[str, Any]] = None,
|
|
95
|
+
json: Optional[Dict[str, Any]] = None,
|
|
96
|
+
data: Optional[Any] = None,
|
|
97
|
+
files: Optional[Dict[str, Any]] = None,
|
|
98
|
+
**kwargs,
|
|
99
|
+
) -> httpx.Response:
|
|
100
|
+
"""Make HTTP request with retries and error handling."""
|
|
101
|
+
url = self._build_url(path)
|
|
102
|
+
request_headers = self._prepare_headers(headers)
|
|
103
|
+
|
|
104
|
+
# Generate request ID for tracing
|
|
105
|
+
request_id = f"req_{int(time.time() * 1000)}"
|
|
106
|
+
request_headers["X-Request-ID"] = request_id
|
|
107
|
+
|
|
108
|
+
with self.tracer.start_as_current_span(
|
|
109
|
+
f"http_{method.lower()}",
|
|
110
|
+
attributes={
|
|
111
|
+
"http.method": method,
|
|
112
|
+
"http.url": url,
|
|
113
|
+
"http.request_id": request_id,
|
|
114
|
+
},
|
|
115
|
+
) as span:
|
|
116
|
+
last_exception = None
|
|
117
|
+
|
|
118
|
+
for attempt in range(self.retries + 1):
|
|
119
|
+
try:
|
|
120
|
+
self.logger.debug(
|
|
121
|
+
f"Making {method} request to {url} (attempt {attempt + 1})",
|
|
122
|
+
extra={
|
|
123
|
+
"request_id": request_id,
|
|
124
|
+
"method": method,
|
|
125
|
+
"url": url,
|
|
126
|
+
"attempt": attempt + 1,
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
response = await self.client.request(
|
|
131
|
+
method=method,
|
|
132
|
+
url=url,
|
|
133
|
+
headers=request_headers,
|
|
134
|
+
params=params,
|
|
135
|
+
json=json,
|
|
136
|
+
data=data,
|
|
137
|
+
files=files,
|
|
138
|
+
**kwargs,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Add response attributes to span
|
|
142
|
+
span.set_attributes({
|
|
143
|
+
"http.status_code": response.status_code,
|
|
144
|
+
"http.response_size": len(response.content),
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
self.logger.debug(
|
|
148
|
+
f"Response: {response.status_code}",
|
|
149
|
+
extra={
|
|
150
|
+
"request_id": request_id,
|
|
151
|
+
"status_code": response.status_code,
|
|
152
|
+
"response_time": response.elapsed.total_seconds(),
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Handle different status codes
|
|
157
|
+
if response.status_code < 400:
|
|
158
|
+
return response
|
|
159
|
+
elif response.status_code in [429, 502, 503, 504]:
|
|
160
|
+
# Retry on rate limit and server errors
|
|
161
|
+
if attempt < self.retries:
|
|
162
|
+
await self._handle_retry_response(response, attempt)
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
# Handle client and server errors
|
|
166
|
+
await self._handle_error_response(response, request_id)
|
|
167
|
+
|
|
168
|
+
except httpx.TimeoutException as e:
|
|
169
|
+
last_exception = e
|
|
170
|
+
self.logger.warning(
|
|
171
|
+
f"Request timeout (attempt {attempt + 1})",
|
|
172
|
+
extra={"request_id": request_id, "error": str(e)},
|
|
173
|
+
)
|
|
174
|
+
if attempt < self.retries:
|
|
175
|
+
await asyncio.sleep(self.backoff_factor * (2 ** attempt))
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
except httpx.NetworkError as e:
|
|
179
|
+
last_exception = e
|
|
180
|
+
self.logger.warning(
|
|
181
|
+
f"Network error (attempt {attempt + 1})",
|
|
182
|
+
extra={"request_id": request_id, "error": str(e)},
|
|
183
|
+
)
|
|
184
|
+
if attempt < self.retries:
|
|
185
|
+
await asyncio.sleep(self.backoff_factor * (2 ** attempt))
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
except Exception as e:
|
|
189
|
+
last_exception = e
|
|
190
|
+
self.logger.error(
|
|
191
|
+
f"Unexpected error (attempt {attempt + 1})",
|
|
192
|
+
extra={"request_id": request_id, "error": str(e)},
|
|
193
|
+
)
|
|
194
|
+
if attempt < self.retries:
|
|
195
|
+
await asyncio.sleep(self.backoff_factor * (2 ** attempt))
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
# All retries exhausted
|
|
199
|
+
span.set_status(trace.Status(trace.StatusCode.ERROR))
|
|
200
|
+
if last_exception:
|
|
201
|
+
raise ServiceUnavailableError(
|
|
202
|
+
f"Request failed after {self.retries + 1} attempts: {last_exception}",
|
|
203
|
+
request_id=request_id,
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
raise ServiceUnavailableError(
|
|
207
|
+
f"Request failed after {self.retries + 1} attempts",
|
|
208
|
+
request_id=request_id,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
async def _handle_retry_response(self, response: httpx.Response, attempt: int):
|
|
212
|
+
"""Handle response that should be retried."""
|
|
213
|
+
if response.status_code == 429:
|
|
214
|
+
# Rate limited - check for Retry-After header
|
|
215
|
+
retry_after = response.headers.get("Retry-After")
|
|
216
|
+
if retry_after:
|
|
217
|
+
try:
|
|
218
|
+
delay = float(retry_after)
|
|
219
|
+
self.logger.info(f"Rate limited, retrying after {delay}s")
|
|
220
|
+
await asyncio.sleep(delay)
|
|
221
|
+
return
|
|
222
|
+
except ValueError:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
# Default exponential backoff
|
|
226
|
+
delay = self.backoff_factor * (2 ** attempt)
|
|
227
|
+
self.logger.info(f"Retrying after {delay}s")
|
|
228
|
+
await asyncio.sleep(delay)
|
|
229
|
+
|
|
230
|
+
async def _handle_error_response(self, response: httpx.Response, request_id: str):
|
|
231
|
+
"""Handle error responses by raising appropriate exceptions."""
|
|
232
|
+
try:
|
|
233
|
+
error_data = response.json()
|
|
234
|
+
except Exception:
|
|
235
|
+
error_data = {
|
|
236
|
+
"error": "SERVER_ERROR",
|
|
237
|
+
"message": f"HTTP {response.status_code}",
|
|
238
|
+
"request_id": request_id,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Map HTTP status codes to exceptions
|
|
242
|
+
status_code = response.status_code
|
|
243
|
+
if status_code == 401:
|
|
244
|
+
raise AuthenticationError(
|
|
245
|
+
error_data.get("message", "Authentication failed"),
|
|
246
|
+
request_id=request_id,
|
|
247
|
+
details=error_data.get("details", {}),
|
|
248
|
+
)
|
|
249
|
+
elif status_code == 403:
|
|
250
|
+
raise AuthorizationError(
|
|
251
|
+
error_data.get("message", "Access denied"),
|
|
252
|
+
request_id=request_id,
|
|
253
|
+
details=error_data.get("details", {}),
|
|
254
|
+
)
|
|
255
|
+
elif status_code == 404:
|
|
256
|
+
raise NotFoundError(
|
|
257
|
+
error_data.get("message", "Resource not found"),
|
|
258
|
+
request_id=request_id,
|
|
259
|
+
details=error_data.get("details", {}),
|
|
260
|
+
)
|
|
261
|
+
elif status_code == 400:
|
|
262
|
+
raise ValidationError(
|
|
263
|
+
error_data.get("message", "Validation failed"),
|
|
264
|
+
request_id=request_id,
|
|
265
|
+
details=error_data.get("details", {}),
|
|
266
|
+
)
|
|
267
|
+
elif status_code == 429:
|
|
268
|
+
raise RateLimitError(
|
|
269
|
+
error_data.get("message", "Rate limit exceeded"),
|
|
270
|
+
request_id=request_id,
|
|
271
|
+
details=error_data.get("details", {}),
|
|
272
|
+
)
|
|
273
|
+
elif status_code >= 500:
|
|
274
|
+
raise UpstreamError(
|
|
275
|
+
error_data.get("message", "Server error"),
|
|
276
|
+
request_id=request_id,
|
|
277
|
+
details=error_data.get("details", {}),
|
|
278
|
+
)
|
|
279
|
+
else:
|
|
280
|
+
# Use error response data if available
|
|
281
|
+
if "error" in error_data:
|
|
282
|
+
raise create_exception_from_response(error_data)
|
|
283
|
+
else:
|
|
284
|
+
raise UpstreamError(
|
|
285
|
+
f"HTTP {status_code}: {response.text}",
|
|
286
|
+
request_id=request_id,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
async def get(
|
|
290
|
+
self,
|
|
291
|
+
path: str,
|
|
292
|
+
params: Optional[Dict[str, Any]] = None,
|
|
293
|
+
headers: Optional[Dict[str, str]] = None,
|
|
294
|
+
**kwargs,
|
|
295
|
+
) -> httpx.Response:
|
|
296
|
+
"""Make GET request."""
|
|
297
|
+
return await self._make_request("GET", path, params=params, headers=headers, **kwargs)
|
|
298
|
+
|
|
299
|
+
async def post(
|
|
300
|
+
self,
|
|
301
|
+
path: str,
|
|
302
|
+
json: Optional[Dict[str, Any]] = None,
|
|
303
|
+
data: Optional[Any] = None,
|
|
304
|
+
headers: Optional[Dict[str, str]] = None,
|
|
305
|
+
**kwargs,
|
|
306
|
+
) -> httpx.Response:
|
|
307
|
+
"""Make POST request."""
|
|
308
|
+
return await self._make_request("POST", path, json=json, data=data, headers=headers, **kwargs)
|
|
309
|
+
|
|
310
|
+
async def put(
|
|
311
|
+
self,
|
|
312
|
+
path: str,
|
|
313
|
+
json: Optional[Dict[str, Any]] = None,
|
|
314
|
+
data: Optional[Any] = None,
|
|
315
|
+
headers: Optional[Dict[str, str]] = None,
|
|
316
|
+
**kwargs,
|
|
317
|
+
) -> httpx.Response:
|
|
318
|
+
"""Make PUT request."""
|
|
319
|
+
return await self._make_request("PUT", path, json=json, data=data, headers=headers, **kwargs)
|
|
320
|
+
|
|
321
|
+
async def patch(
|
|
322
|
+
self,
|
|
323
|
+
path: str,
|
|
324
|
+
json: Optional[Dict[str, Any]] = None,
|
|
325
|
+
data: Optional[Any] = None,
|
|
326
|
+
headers: Optional[Dict[str, str]] = None,
|
|
327
|
+
**kwargs,
|
|
328
|
+
) -> httpx.Response:
|
|
329
|
+
"""Make PATCH request."""
|
|
330
|
+
return await self._make_request("PATCH", path, json=json, data=data, headers=headers, **kwargs)
|
|
331
|
+
|
|
332
|
+
async def delete(
|
|
333
|
+
self,
|
|
334
|
+
path: str,
|
|
335
|
+
headers: Optional[Dict[str, str]] = None,
|
|
336
|
+
**kwargs,
|
|
337
|
+
) -> httpx.Response:
|
|
338
|
+
"""Make DELETE request."""
|
|
339
|
+
return await self._make_request("DELETE", path, headers=headers, **kwargs)
|
|
340
|
+
|
|
341
|
+
def set_auth_token(self, token: str):
|
|
342
|
+
"""Set authentication token."""
|
|
343
|
+
self.auth_token = token
|
|
344
|
+
|
|
345
|
+
def clear_auth_token(self):
|
|
346
|
+
"""Clear authentication token."""
|
|
347
|
+
self.auth_token = None
|
|
348
|
+
|
|
349
|
+
async def stream(
|
|
350
|
+
self,
|
|
351
|
+
method: str,
|
|
352
|
+
path: str,
|
|
353
|
+
*,
|
|
354
|
+
headers: Optional[Dict[str, str]] = None,
|
|
355
|
+
params: Optional[Dict[str, Any]] = None,
|
|
356
|
+
json: Optional[Dict[str, Any]] = None,
|
|
357
|
+
):
|
|
358
|
+
"""Stream a response using httpx's streaming interface.
|
|
359
|
+
|
|
360
|
+
Returns an async context manager yielding the httpx.Response stream.
|
|
361
|
+
"""
|
|
362
|
+
url = self._build_url(path)
|
|
363
|
+
request_headers = self._prepare_headers(headers)
|
|
364
|
+
# Add request id for traceability
|
|
365
|
+
request_headers.setdefault("X-Request-ID", f"req_{int(time.time() * 1000)}")
|
|
366
|
+
return self.client.stream(method, url, headers=request_headers, params=params, json=json)
|