hammad-python 0.0.30__py3-none-any.whl → 0.0.31__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.
Files changed (137) hide show
  1. ham/__init__.py +10 -0
  2. {hammad_python-0.0.30.dist-info → hammad_python-0.0.31.dist-info}/METADATA +6 -32
  3. hammad_python-0.0.31.dist-info/RECORD +6 -0
  4. hammad/__init__.py +0 -84
  5. hammad/_internal.py +0 -256
  6. hammad/_main.py +0 -226
  7. hammad/cache/__init__.py +0 -40
  8. hammad/cache/base_cache.py +0 -181
  9. hammad/cache/cache.py +0 -169
  10. hammad/cache/decorators.py +0 -261
  11. hammad/cache/file_cache.py +0 -80
  12. hammad/cache/ttl_cache.py +0 -74
  13. hammad/cli/__init__.py +0 -33
  14. hammad/cli/animations.py +0 -573
  15. hammad/cli/plugins.py +0 -867
  16. hammad/cli/styles/__init__.py +0 -55
  17. hammad/cli/styles/settings.py +0 -139
  18. hammad/cli/styles/types.py +0 -358
  19. hammad/cli/styles/utils.py +0 -634
  20. hammad/data/__init__.py +0 -90
  21. hammad/data/collections/__init__.py +0 -49
  22. hammad/data/collections/collection.py +0 -326
  23. hammad/data/collections/indexes/__init__.py +0 -37
  24. hammad/data/collections/indexes/qdrant/__init__.py +0 -1
  25. hammad/data/collections/indexes/qdrant/index.py +0 -723
  26. hammad/data/collections/indexes/qdrant/settings.py +0 -94
  27. hammad/data/collections/indexes/qdrant/utils.py +0 -210
  28. hammad/data/collections/indexes/tantivy/__init__.py +0 -1
  29. hammad/data/collections/indexes/tantivy/index.py +0 -426
  30. hammad/data/collections/indexes/tantivy/settings.py +0 -40
  31. hammad/data/collections/indexes/tantivy/utils.py +0 -176
  32. hammad/data/configurations/__init__.py +0 -35
  33. hammad/data/configurations/configuration.py +0 -564
  34. hammad/data/models/__init__.py +0 -50
  35. hammad/data/models/extensions/__init__.py +0 -4
  36. hammad/data/models/extensions/pydantic/__init__.py +0 -42
  37. hammad/data/models/extensions/pydantic/converters.py +0 -759
  38. hammad/data/models/fields.py +0 -546
  39. hammad/data/models/model.py +0 -1078
  40. hammad/data/models/utils.py +0 -280
  41. hammad/data/sql/__init__.py +0 -24
  42. hammad/data/sql/database.py +0 -576
  43. hammad/data/sql/types.py +0 -127
  44. hammad/data/types/__init__.py +0 -75
  45. hammad/data/types/file.py +0 -431
  46. hammad/data/types/multimodal/__init__.py +0 -36
  47. hammad/data/types/multimodal/audio.py +0 -200
  48. hammad/data/types/multimodal/image.py +0 -182
  49. hammad/data/types/text.py +0 -1308
  50. hammad/formatting/__init__.py +0 -33
  51. hammad/formatting/json/__init__.py +0 -27
  52. hammad/formatting/json/converters.py +0 -158
  53. hammad/formatting/text/__init__.py +0 -63
  54. hammad/formatting/text/converters.py +0 -723
  55. hammad/formatting/text/markdown.py +0 -131
  56. hammad/formatting/yaml/__init__.py +0 -26
  57. hammad/formatting/yaml/converters.py +0 -5
  58. hammad/genai/__init__.py +0 -217
  59. hammad/genai/a2a/__init__.py +0 -32
  60. hammad/genai/a2a/workers.py +0 -552
  61. hammad/genai/agents/__init__.py +0 -59
  62. hammad/genai/agents/agent.py +0 -1973
  63. hammad/genai/agents/run.py +0 -1024
  64. hammad/genai/agents/types/__init__.py +0 -42
  65. hammad/genai/agents/types/agent_context.py +0 -13
  66. hammad/genai/agents/types/agent_event.py +0 -128
  67. hammad/genai/agents/types/agent_hooks.py +0 -220
  68. hammad/genai/agents/types/agent_messages.py +0 -31
  69. hammad/genai/agents/types/agent_response.py +0 -125
  70. hammad/genai/agents/types/agent_stream.py +0 -327
  71. hammad/genai/graphs/__init__.py +0 -125
  72. hammad/genai/graphs/_utils.py +0 -190
  73. hammad/genai/graphs/base.py +0 -1828
  74. hammad/genai/graphs/plugins.py +0 -316
  75. hammad/genai/graphs/types.py +0 -638
  76. hammad/genai/models/__init__.py +0 -1
  77. hammad/genai/models/embeddings/__init__.py +0 -43
  78. hammad/genai/models/embeddings/model.py +0 -226
  79. hammad/genai/models/embeddings/run.py +0 -163
  80. hammad/genai/models/embeddings/types/__init__.py +0 -37
  81. hammad/genai/models/embeddings/types/embedding_model_name.py +0 -75
  82. hammad/genai/models/embeddings/types/embedding_model_response.py +0 -76
  83. hammad/genai/models/embeddings/types/embedding_model_run_params.py +0 -66
  84. hammad/genai/models/embeddings/types/embedding_model_settings.py +0 -47
  85. hammad/genai/models/language/__init__.py +0 -57
  86. hammad/genai/models/language/model.py +0 -1098
  87. hammad/genai/models/language/run.py +0 -878
  88. hammad/genai/models/language/types/__init__.py +0 -40
  89. hammad/genai/models/language/types/language_model_instructor_mode.py +0 -47
  90. hammad/genai/models/language/types/language_model_messages.py +0 -28
  91. hammad/genai/models/language/types/language_model_name.py +0 -239
  92. hammad/genai/models/language/types/language_model_request.py +0 -127
  93. hammad/genai/models/language/types/language_model_response.py +0 -217
  94. hammad/genai/models/language/types/language_model_response_chunk.py +0 -56
  95. hammad/genai/models/language/types/language_model_settings.py +0 -89
  96. hammad/genai/models/language/types/language_model_stream.py +0 -600
  97. hammad/genai/models/language/utils/__init__.py +0 -28
  98. hammad/genai/models/language/utils/requests.py +0 -421
  99. hammad/genai/models/language/utils/structured_outputs.py +0 -135
  100. hammad/genai/models/model_provider.py +0 -4
  101. hammad/genai/models/multimodal.py +0 -47
  102. hammad/genai/models/reranking.py +0 -26
  103. hammad/genai/types/__init__.py +0 -1
  104. hammad/genai/types/base.py +0 -215
  105. hammad/genai/types/history.py +0 -290
  106. hammad/genai/types/tools.py +0 -507
  107. hammad/logging/__init__.py +0 -35
  108. hammad/logging/decorators.py +0 -834
  109. hammad/logging/logger.py +0 -1018
  110. hammad/mcp/__init__.py +0 -53
  111. hammad/mcp/client/__init__.py +0 -35
  112. hammad/mcp/client/client.py +0 -624
  113. hammad/mcp/client/client_service.py +0 -400
  114. hammad/mcp/client/settings.py +0 -178
  115. hammad/mcp/servers/__init__.py +0 -26
  116. hammad/mcp/servers/launcher.py +0 -1161
  117. hammad/runtime/__init__.py +0 -32
  118. hammad/runtime/decorators.py +0 -142
  119. hammad/runtime/run.py +0 -299
  120. hammad/service/__init__.py +0 -49
  121. hammad/service/create.py +0 -527
  122. hammad/service/decorators.py +0 -283
  123. hammad/types.py +0 -288
  124. hammad/typing/__init__.py +0 -435
  125. hammad/web/__init__.py +0 -43
  126. hammad/web/http/__init__.py +0 -1
  127. hammad/web/http/client.py +0 -944
  128. hammad/web/models.py +0 -275
  129. hammad/web/openapi/__init__.py +0 -1
  130. hammad/web/openapi/client.py +0 -740
  131. hammad/web/search/__init__.py +0 -1
  132. hammad/web/search/client.py +0 -1023
  133. hammad/web/utils.py +0 -472
  134. hammad_python-0.0.30.dist-info/RECORD +0 -135
  135. {hammad → ham}/py.typed +0 -0
  136. {hammad_python-0.0.30.dist-info → hammad_python-0.0.31.dist-info}/WHEEL +0 -0
  137. {hammad_python-0.0.30.dist-info → hammad_python-0.0.31.dist-info}/licenses/LICENSE +0 -0
hammad/web/http/client.py DELETED
@@ -1,944 +0,0 @@
1
- """hammad.http.client"""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- import json
7
- from typing import Any, Dict, List, Literal, Optional, Union, overload
8
- from urllib.parse import urljoin, urlparse
9
-
10
- import httpx
11
- from pydantic import BaseModel, Field, field_validator
12
-
13
- __all__ = (
14
- "HttpError",
15
- "HttpRequest",
16
- "HttpResponse",
17
- "HttpClient",
18
- )
19
-
20
-
21
- class HttpError(Exception):
22
- """Custom exception for HTTP toolkit errors with semantic feedback."""
23
-
24
- def __init__(
25
- self,
26
- message: str,
27
- suggestion: str = "",
28
- context: Optional[Dict[str, Any]] = None,
29
- status_code: Optional[int] = None,
30
- response_text: Optional[str] = None,
31
- ):
32
- self.message = message
33
- self.suggestion = suggestion
34
- self.context = context or {}
35
- self.status_code = status_code
36
- self.response_text = response_text
37
- super().__init__(self.message)
38
-
39
- def get_full_error(self) -> str:
40
- """Get the full error message with suggestion and context."""
41
- error_msg = f"HTTP ERROR: {self.message}"
42
- if self.status_code:
43
- error_msg += f" (Status: {self.status_code})"
44
- if self.suggestion:
45
- error_msg += f"\nSUGGESTION: {self.suggestion}"
46
- if self.context:
47
- error_msg += f"\nCONTEXT: {self.context}"
48
- if self.response_text:
49
- error_msg += f"\nRESPONSE: {self.response_text[:500]}..."
50
- return error_msg
51
-
52
- def __str__(self) -> str:
53
- """Return the full error message when converting to string."""
54
- return self.get_full_error()
55
-
56
-
57
- class HttpRequest(BaseModel):
58
- """Model for HTTP request configuration with semantic parameterization."""
59
-
60
- method: Literal["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] = "GET"
61
- url: str
62
- headers: Optional[Dict[str, str]] = None
63
- params: Optional[Dict[str, Any]] = None
64
- json_data: Optional[Dict[str, Any]] = Field(None, alias="json")
65
- form_data: Optional[Dict[str, Any]] = None
66
- content: Optional[Union[str, bytes]] = None
67
- timeout: Optional[float] = 30.0
68
- follow_redirects: bool = True
69
-
70
- # Semantic authentication parameters
71
- api_key: Optional[str] = None
72
- api_key_header: str = "X-API-Key"
73
- bearer_token: Optional[str] = None
74
- basic_auth: Optional[tuple[str, str]] = None
75
- auth_header: Optional[str] = None # Custom auth header value
76
-
77
- # Common convenience parameters
78
- user_agent: Optional[str] = None
79
- content_type: Optional[str] = None
80
- accept: Optional[str] = None
81
-
82
- # Advanced options
83
- retry_attempts: int = 0
84
- retry_delay: float = 1.0
85
-
86
- def get_effective_headers(self) -> Dict[str, str]:
87
- """Get the effective headers with semantic parameters applied."""
88
- effective_headers = (self.headers or {}).copy()
89
-
90
- # Apply semantic authentication
91
- if self.api_key:
92
- effective_headers[self.api_key_header] = self.api_key
93
-
94
- if self.bearer_token:
95
- effective_headers["Authorization"] = f"Bearer {self.bearer_token}"
96
-
97
- if self.basic_auth:
98
- import base64
99
-
100
- credentials = base64.b64encode(
101
- f"{self.basic_auth[0]}:{self.basic_auth[1]}".encode()
102
- ).decode()
103
- effective_headers["Authorization"] = f"Basic {credentials}"
104
-
105
- if self.auth_header:
106
- effective_headers["Authorization"] = self.auth_header
107
-
108
- # Apply convenience headers
109
- if self.user_agent:
110
- effective_headers["User-Agent"] = self.user_agent
111
-
112
- if self.content_type:
113
- effective_headers["Content-Type"] = self.content_type
114
-
115
- if self.accept:
116
- effective_headers["Accept"] = self.accept
117
-
118
- return effective_headers
119
-
120
- @field_validator("url")
121
- @classmethod
122
- def validate_url(cls, v):
123
- """Validate URL format."""
124
- if not v or not v.strip():
125
- raise ValueError("URL cannot be empty")
126
-
127
- parsed = urlparse(v)
128
- if not parsed.scheme:
129
- raise ValueError("URL must include scheme (http:// or https://)")
130
- if not parsed.netloc:
131
- raise ValueError("URL must include domain")
132
-
133
- return v.strip()
134
-
135
- @field_validator("method", mode="before")
136
- @classmethod
137
- def validate_method(cls, v):
138
- """Validate HTTP method."""
139
- return v.upper()
140
-
141
-
142
- class HttpResponse(BaseModel):
143
- """Model for HTTP response data."""
144
-
145
- status_code: int
146
- headers: Dict[str, str]
147
- content: Union[str, bytes]
148
- json_data: Optional[Union[Dict[str, Any], List[Any], str, int, float, bool]] = None
149
- url: str
150
- elapsed_ms: float
151
-
152
- @property
153
- def is_success(self) -> bool:
154
- """Check if response indicates success (2xx status)."""
155
- return 200 <= self.status_code < 300
156
-
157
- @property
158
- def is_redirect(self) -> bool:
159
- """Check if response is a redirect (3xx status)."""
160
- return 300 <= self.status_code < 400
161
-
162
- @property
163
- def is_client_error(self) -> bool:
164
- """Check if response is a client error (4xx status)."""
165
- return 400 <= self.status_code < 500
166
-
167
- @property
168
- def is_server_error(self) -> bool:
169
- """Check if response is a server error (5xx status)."""
170
- return 500 <= self.status_code < 600
171
-
172
-
173
- class AsyncHttpClient:
174
- """
175
- Base HTTP toolkit for making HTTP requests with clean type hints and semantic error handling.
176
-
177
- This class provides a clean, well-typed interface for HTTP operations using httpx.
178
- It includes semantic error handling and validation to provide meaningful feedback.
179
- """
180
-
181
- def __init__(
182
- self,
183
- base_url: Optional[str] = None,
184
- default_headers: Optional[Dict[str, str]] = None,
185
- timeout: float = 30.0,
186
- follow_redirects: bool = True,
187
- verify_ssl: bool = True,
188
- # Semantic authentication parameters
189
- api_key: Optional[str] = None,
190
- api_key_header: str = "X-API-Key",
191
- bearer_token: Optional[str] = None,
192
- basic_auth: Optional[tuple[str, str]] = None,
193
- user_agent: Optional[str] = None,
194
- ):
195
- """
196
- Initialize the HTTP toolkit.
197
-
198
- Args:
199
- base_url: Base URL for all requests (optional)
200
- default_headers: Default headers to include in all requests
201
- timeout: Default timeout in seconds
202
- follow_redirects: Whether to follow redirects by default
203
- verify_ssl: Whether to verify SSL certificates
204
- api_key: API key for authentication
205
- api_key_header: Header name for API key (default: X-API-Key)
206
- bearer_token: Bearer token for Authorization header
207
- basic_auth: Tuple of (username, password) for basic auth
208
- user_agent: User-Agent header value
209
- """
210
- self.base_url = base_url
211
- self.default_headers = default_headers or {}
212
- self.timeout = timeout
213
- self.follow_redirects = follow_redirects
214
- self.verify_ssl = verify_ssl
215
-
216
- # Store semantic authentication parameters
217
- self.api_key = api_key
218
- self.api_key_header = api_key_header
219
- self.bearer_token = bearer_token
220
- self.basic_auth = basic_auth
221
- self.user_agent = user_agent
222
-
223
- # Apply semantic parameters to default headers
224
- self._apply_semantic_headers()
225
-
226
- # Validate base_url if provided
227
- if self.base_url:
228
- parsed = urlparse(self.base_url)
229
- if not parsed.scheme or not parsed.netloc:
230
- raise HttpError(
231
- message=f"Invalid base URL: {self.base_url}",
232
- suggestion="Provide a valid base URL with scheme and domain (e.g., https://api.example.com)",
233
- context={"provided_base_url": self.base_url},
234
- )
235
-
236
- def _apply_semantic_headers(self) -> None:
237
- """Apply semantic authentication parameters to default headers."""
238
- # API Key authentication
239
- if self.api_key:
240
- self.default_headers[self.api_key_header] = self.api_key
241
-
242
- # Bearer token authentication
243
- if self.bearer_token:
244
- self.default_headers["Authorization"] = f"Bearer {self.bearer_token}"
245
-
246
- # Basic authentication
247
- if self.basic_auth:
248
- import base64
249
-
250
- credentials = base64.b64encode(
251
- f"{self.basic_auth[0]}:{self.basic_auth[1]}".encode()
252
- ).decode()
253
- self.default_headers["Authorization"] = f"Basic {credentials}"
254
-
255
- # User-Agent
256
- if self.user_agent:
257
- self.default_headers["User-Agent"] = self.user_agent
258
-
259
- def _build_url(self, url: str) -> str:
260
- """Build the complete URL, combining base_url if provided."""
261
- if self.base_url:
262
- return urljoin(self.base_url.rstrip("/") + "/", url.lstrip("/"))
263
- return url
264
-
265
- def _prepare_headers(
266
- self, request_or_headers: Union[HttpRequest, Dict[str, str], None]
267
- ) -> Dict[str, str]:
268
- """Prepare headers by combining default headers with request-specific ones."""
269
- combined_headers = self.default_headers.copy()
270
-
271
- if request_or_headers is None:
272
- # No additional headers
273
- pass
274
- elif isinstance(request_or_headers, HttpRequest):
275
- # Get effective headers from the request (includes semantic parameters)
276
- request_headers = request_or_headers.get_effective_headers()
277
- combined_headers.update(request_headers)
278
- elif isinstance(request_or_headers, dict):
279
- # Backward compatibility: simple dict of headers
280
- combined_headers.update(request_or_headers)
281
-
282
- return combined_headers
283
-
284
- async def _execute_with_retry(
285
- self,
286
- url: str,
287
- headers: Dict[str, str],
288
- params: Optional[Dict[str, Any]],
289
- json_data: Optional[Dict[str, Any]],
290
- form_data: Optional[Dict[str, Any]],
291
- content: Optional[Union[str, bytes]],
292
- request: HttpRequest,
293
- ) -> HttpResponse:
294
- """Execute HTTP request with retry logic."""
295
- import asyncio
296
- import time
297
-
298
- last_exception = None
299
-
300
- for attempt in range(request.retry_attempts + 1):
301
- try:
302
- async with httpx.AsyncClient(
303
- timeout=request.timeout or self.timeout,
304
- follow_redirects=request.follow_redirects,
305
- verify=self.verify_ssl,
306
- ) as client:
307
- # Record start time
308
- start_time = time.time()
309
-
310
- response = await client.request(
311
- method=request.method,
312
- url=url,
313
- headers=headers,
314
- params=params,
315
- json=json_data,
316
- data=form_data,
317
- content=content,
318
- )
319
-
320
- # Calculate elapsed time
321
- elapsed_ms = (time.time() - start_time) * 1000
322
-
323
- # Handle response errors
324
- self._handle_response_errors(response)
325
-
326
- # Parse JSON if possible
327
- json_response = None
328
- if response.headers.get("content-type", "").startswith(
329
- "application/json"
330
- ):
331
- try:
332
- json_response = response.json()
333
- except json.JSONDecodeError:
334
- # Not valid JSON, leave as None
335
- pass
336
-
337
- return HttpResponse(
338
- status_code=response.status_code,
339
- headers=dict(response.headers),
340
- content=response.text,
341
- json_data=json_response,
342
- url=str(response.url),
343
- elapsed_ms=elapsed_ms,
344
- )
345
-
346
- except (httpx.ConnectError, httpx.TimeoutException, HttpError) as e:
347
- last_exception = e
348
-
349
- # Don't retry on client errors (4xx) or authentication issues
350
- if (
351
- isinstance(e, HttpError)
352
- and e.status_code
353
- and 400 <= e.status_code < 500
354
- ):
355
- raise e
356
-
357
- # If this is the last attempt, raise the exception
358
- if attempt == request.retry_attempts:
359
- raise e
360
-
361
- # Wait before retrying
362
- if request.retry_delay > 0:
363
- await asyncio.sleep(
364
- request.retry_delay * (attempt + 1)
365
- ) # Exponential backoff
366
-
367
- # This should never be reached, but just in case
368
- if last_exception:
369
- raise last_exception
370
- else:
371
- raise HttpError(
372
- message="Request failed after all retry attempts",
373
- suggestion="Check your network connection and the server status",
374
- context={"url": url, "retry_attempts": request.retry_attempts},
375
- )
376
-
377
- def _handle_response_errors(self, response: httpx.Response) -> None:
378
- """Handle HTTP response errors with semantic feedback."""
379
- if response.is_success:
380
- return
381
-
382
- status_code = response.status_code
383
-
384
- # Get response text safely
385
- try:
386
- response_text = response.text
387
- except Exception:
388
- response_text = "Unable to decode response text"
389
-
390
- # Provide semantic error messages based on status code
391
- if status_code == 400:
392
- raise HttpError(
393
- message="Bad Request - The server cannot process the request",
394
- suggestion="Check your request parameters, headers, and data format",
395
- context={"url": str(response.url), "method": response.request.method},
396
- status_code=status_code,
397
- response_text=response_text,
398
- )
399
- elif status_code == 401:
400
- raise HttpError(
401
- message="Unauthorized - Authentication is required",
402
- suggestion="Provide valid authentication credentials (API key, token, etc.)",
403
- context={"url": str(response.url)},
404
- status_code=status_code,
405
- response_text=response_text,
406
- )
407
- elif status_code == 403:
408
- raise HttpError(
409
- message="Forbidden - Access is denied",
410
- suggestion="Check your permissions or API key scope",
411
- context={"url": str(response.url)},
412
- status_code=status_code,
413
- response_text=response_text,
414
- )
415
- elif status_code == 404:
416
- raise HttpError(
417
- message="Not Found - The requested resource does not exist",
418
- suggestion="Verify the URL path and any path parameters",
419
- context={"url": str(response.url)},
420
- status_code=status_code,
421
- response_text=response_text,
422
- )
423
- elif status_code == 429:
424
- raise HttpError(
425
- message="Too Many Requests - Rate limit exceeded",
426
- suggestion="Reduce request frequency or wait before retrying",
427
- context={"url": str(response.url)},
428
- status_code=status_code,
429
- response_text=response_text,
430
- )
431
- elif 400 <= status_code < 500:
432
- raise HttpError(
433
- message=f"Client Error ({status_code}) - Request cannot be fulfilled",
434
- suggestion="Review your request parameters and try again",
435
- context={"url": str(response.url), "method": response.request.method},
436
- status_code=status_code,
437
- response_text=response_text,
438
- )
439
- elif 500 <= status_code < 600:
440
- raise HttpError(
441
- message=f"Server Error ({status_code}) - Server encountered an error",
442
- suggestion="The server is experiencing issues. Try again later or contact support",
443
- context={"url": str(response.url)},
444
- status_code=status_code,
445
- response_text=response_text,
446
- )
447
- else:
448
- raise HttpError(
449
- message=f"HTTP Error ({status_code})",
450
- suggestion="An unexpected HTTP error occurred",
451
- context={"url": str(response.url), "method": response.request.method},
452
- status_code=status_code,
453
- response_text=response_text,
454
- )
455
-
456
- async def request(self, request: HttpRequest) -> HttpResponse:
457
- """
458
- Make an HTTP request with semantic error handling.
459
-
460
- Args:
461
- request: HttpRequest configuration object
462
-
463
- Returns:
464
- HttpResponse object with response data
465
-
466
- Raises:
467
- HttpError: On request failures with semantic feedback
468
- """
469
- try:
470
- # Build the complete URL
471
- url = self._build_url(request.url)
472
-
473
- # Prepare headers
474
- headers = self._prepare_headers(request)
475
-
476
- # Prepare request data
477
- json_data = request.json_data
478
- form_data = request.form_data
479
- content = request.content
480
-
481
- # Validate data payload
482
- data_count = sum(
483
- 1 for x in [json_data, form_data, content] if x is not None
484
- )
485
- if data_count > 1:
486
- raise HttpError(
487
- message="Multiple data payloads provided",
488
- suggestion="Provide only one of: json_data, form_data, or content",
489
- context={
490
- "has_json": json_data is not None,
491
- "has_form": form_data is not None,
492
- "has_content": content is not None,
493
- },
494
- )
495
-
496
- # Execute the request with retry logic
497
- return await self._execute_with_retry(
498
- url, headers, request.params, json_data, form_data, content, request
499
- )
500
-
501
- except httpx.TimeoutException:
502
- raise HttpError(
503
- message="Request timed out",
504
- suggestion=f"The request took longer than {request.timeout or self.timeout} seconds. Try increasing the timeout or check the server status",
505
- context={
506
- "url": request.url,
507
- "timeout": request.timeout or self.timeout,
508
- },
509
- )
510
- except httpx.ConnectError:
511
- raise HttpError(
512
- message="Connection failed",
513
- suggestion="Check the URL and your internet connection. The server might be down",
514
- context={"url": request.url},
515
- )
516
- except HttpError:
517
- # Re-raise HttpError as-is
518
- raise
519
- except httpx.HTTPStatusError as e:
520
- # This shouldn't happen since we handle status errors above, but just in case
521
- raise HttpError(
522
- message=f"HTTP error {e.response.status_code}",
523
- suggestion="The server returned an error status",
524
- context={"url": request.url, "status_code": e.response.status_code},
525
- status_code=e.response.status_code,
526
- )
527
- except Exception as e:
528
- raise HttpError(
529
- message=f"Unexpected error: {str(e)}",
530
- suggestion="An unexpected error occurred. Check your request configuration",
531
- context={"url": request.url, "error_type": type(e).__name__},
532
- )
533
-
534
- # Convenience methods
535
- async def get(
536
- self,
537
- url: str,
538
- params: Optional[Dict[str, Any]] = None,
539
- headers: Optional[Dict[str, str]] = None,
540
- timeout: Optional[float] = None,
541
- api_key: Optional[str] = None,
542
- bearer_token: Optional[str] = None,
543
- retry_attempts: int = 0,
544
- **kwargs,
545
- ) -> HttpResponse:
546
- """Make a GET request with semantic parameters."""
547
- request = HttpRequest(
548
- method="GET",
549
- url=url,
550
- params=params,
551
- headers=headers,
552
- timeout=timeout,
553
- api_key=api_key,
554
- bearer_token=bearer_token,
555
- retry_attempts=retry_attempts,
556
- **kwargs,
557
- )
558
- return await self.request(request)
559
-
560
- async def post(
561
- self,
562
- url: str,
563
- json_data: Optional[Dict[str, Any]] = None,
564
- form_data: Optional[Dict[str, Any]] = None,
565
- headers: Optional[Dict[str, str]] = None,
566
- timeout: Optional[float] = None,
567
- api_key: Optional[str] = None,
568
- bearer_token: Optional[str] = None,
569
- retry_attempts: int = 0,
570
- **kwargs,
571
- ) -> HttpResponse:
572
- """Make a POST request with semantic parameters."""
573
- request = HttpRequest(
574
- method="POST",
575
- url=url,
576
- json_data=json_data,
577
- form_data=form_data,
578
- headers=headers,
579
- timeout=timeout,
580
- api_key=api_key,
581
- bearer_token=bearer_token,
582
- retry_attempts=retry_attempts,
583
- **kwargs,
584
- )
585
- return await self.request(request)
586
-
587
- async def put(
588
- self,
589
- url: str,
590
- json_data: Optional[Dict[str, Any]] = None,
591
- form_data: Optional[Dict[str, Any]] = None,
592
- headers: Optional[Dict[str, str]] = None,
593
- timeout: Optional[float] = None,
594
- api_key: Optional[str] = None,
595
- bearer_token: Optional[str] = None,
596
- retry_attempts: int = 0,
597
- **kwargs,
598
- ) -> HttpResponse:
599
- """Make a PUT request with semantic parameters."""
600
- request = HttpRequest(
601
- method="PUT",
602
- url=url,
603
- json_data=json_data,
604
- form_data=form_data,
605
- headers=headers,
606
- timeout=timeout,
607
- api_key=api_key,
608
- bearer_token=bearer_token,
609
- retry_attempts=retry_attempts,
610
- **kwargs,
611
- )
612
- return await self.request(request)
613
-
614
- async def patch(
615
- self,
616
- url: str,
617
- json_data: Optional[Dict[str, Any]] = None,
618
- form_data: Optional[Dict[str, Any]] = None,
619
- headers: Optional[Dict[str, str]] = None,
620
- timeout: Optional[float] = None,
621
- api_key: Optional[str] = None,
622
- bearer_token: Optional[str] = None,
623
- retry_attempts: int = 0,
624
- **kwargs,
625
- ) -> HttpResponse:
626
- """Make a PATCH request with semantic parameters."""
627
- request = HttpRequest(
628
- method="PATCH",
629
- url=url,
630
- json_data=json_data,
631
- form_data=form_data,
632
- headers=headers,
633
- timeout=timeout,
634
- api_key=api_key,
635
- bearer_token=bearer_token,
636
- retry_attempts=retry_attempts,
637
- **kwargs,
638
- )
639
- return await self.request(request)
640
-
641
- async def delete(
642
- self,
643
- url: str,
644
- headers: Optional[Dict[str, str]] = None,
645
- timeout: Optional[float] = None,
646
- api_key: Optional[str] = None,
647
- bearer_token: Optional[str] = None,
648
- retry_attempts: int = 0,
649
- **kwargs,
650
- ) -> HttpResponse:
651
- """Make a DELETE request with semantic parameters."""
652
- request = HttpRequest(
653
- method="DELETE",
654
- url=url,
655
- headers=headers,
656
- timeout=timeout,
657
- api_key=api_key,
658
- bearer_token=bearer_token,
659
- retry_attempts=retry_attempts,
660
- **kwargs,
661
- )
662
- return await self.request(request)
663
-
664
-
665
- class HttpClient:
666
- """
667
- Base HTTP toolkit for making HTTP requests with clean type hints and semantic error handling.
668
-
669
- This class provides a clean, well-typed interface for HTTP operations using httpx.
670
- It includes semantic error handling and validation to provide meaningful feedback.
671
- """
672
-
673
- def __init__(
674
- self,
675
- base_url: Optional[str] = None,
676
- default_headers: Optional[Dict[str, str]] = None,
677
- timeout: float = 30.0,
678
- follow_redirects: bool = True,
679
- verify_ssl: bool = True,
680
- # Semantic authentication parameters
681
- api_key: Optional[str] = None,
682
- api_key_header: str = "X-API-Key",
683
- bearer_token: Optional[str] = None,
684
- basic_auth: Optional[tuple[str, str]] = None,
685
- user_agent: Optional[str] = None,
686
- ):
687
- """
688
- Initialize the HttpClient.
689
-
690
- Args:
691
- base_url: Base URL for all requests
692
- default_headers: Default headers to include in all requests
693
- timeout: Default timeout for HTTP requests in seconds
694
- follow_redirects: Whether to follow HTTP redirects
695
- verify_ssl: Whether to verify SSL certificates
696
- api_key: API key for authentication
697
- api_key_header: Header name for API key
698
- bearer_token: Bearer token for authentication
699
- basic_auth: Username and password tuple for basic authentication
700
- user_agent: User-Agent header for HTTP requests
701
- """
702
- self._async_client = AsyncHttpClient(
703
- base_url=base_url,
704
- default_headers=default_headers,
705
- timeout=timeout,
706
- follow_redirects=follow_redirects,
707
- verify_ssl=verify_ssl,
708
- api_key=api_key,
709
- api_key_header=api_key_header,
710
- bearer_token=bearer_token,
711
- basic_auth=basic_auth,
712
- user_agent=user_agent,
713
- )
714
-
715
- def _run_async(self, coro):
716
- """Run an async coroutine in a new event loop."""
717
- try:
718
- # Try to get the current event loop
719
- loop = asyncio.get_running_loop()
720
- # If we're already in an event loop, we need to use a thread
721
- import concurrent.futures
722
-
723
- with concurrent.futures.ThreadPoolExecutor() as executor:
724
- future = executor.submit(asyncio.run, coro)
725
- return future.result()
726
- except RuntimeError:
727
- # No event loop running, we can create our own
728
- return asyncio.run(coro)
729
-
730
- def request(self, request: HttpRequest) -> HttpResponse:
731
- """
732
- Make an HTTP request with semantic error handling.
733
-
734
- Args:
735
- request: HttpRequest configuration object
736
-
737
- Returns:
738
- HttpResponse object with response data
739
-
740
- Raises:
741
- HttpError: On request failures with semantic feedback
742
- """
743
- return self._run_async(self._async_client.request(request))
744
-
745
- def get(
746
- self,
747
- url: str,
748
- params: Optional[Dict[str, Any]] = None,
749
- headers: Optional[Dict[str, str]] = None,
750
- timeout: Optional[float] = None,
751
- api_key: Optional[str] = None,
752
- bearer_token: Optional[str] = None,
753
- retry_attempts: int = 0,
754
- **kwargs,
755
- ) -> HttpResponse:
756
- """Make a GET request with semantic parameters."""
757
- return self._run_async(
758
- self._async_client.get(
759
- url,
760
- params=params,
761
- headers=headers,
762
- timeout=timeout,
763
- api_key=api_key,
764
- bearer_token=bearer_token,
765
- retry_attempts=retry_attempts,
766
- **kwargs,
767
- )
768
- )
769
-
770
- def post(
771
- self,
772
- url: str,
773
- json_data: Optional[Dict[str, Any]] = None,
774
- form_data: Optional[Dict[str, Any]] = None,
775
- headers: Optional[Dict[str, str]] = None,
776
- timeout: Optional[float] = None,
777
- api_key: Optional[str] = None,
778
- bearer_token: Optional[str] = None,
779
- retry_attempts: int = 0,
780
- **kwargs,
781
- ) -> HttpResponse:
782
- """Make a POST request with semantic parameters."""
783
- return self._run_async(
784
- self._async_client.post(
785
- url,
786
- json_data=json_data,
787
- form_data=form_data,
788
- headers=headers,
789
- timeout=timeout,
790
- api_key=api_key,
791
- bearer_token=bearer_token,
792
- retry_attempts=retry_attempts,
793
- **kwargs,
794
- )
795
- )
796
-
797
- def put(
798
- self,
799
- url: str,
800
- json_data: Optional[Dict[str, Any]] = None,
801
- form_data: Optional[Dict[str, Any]] = None,
802
- headers: Optional[Dict[str, str]] = None,
803
- timeout: Optional[float] = None,
804
- api_key: Optional[str] = None,
805
- bearer_token: Optional[str] = None,
806
- retry_attempts: int = 0,
807
- **kwargs,
808
- ) -> HttpResponse:
809
- """Make a PUT request with semantic parameters."""
810
- return self._run_async(
811
- self._async_client.put(
812
- url,
813
- json_data=json_data,
814
- form_data=form_data,
815
- headers=headers,
816
- timeout=timeout,
817
- api_key=api_key,
818
- bearer_token=bearer_token,
819
- retry_attempts=retry_attempts,
820
- **kwargs,
821
- )
822
- )
823
-
824
- def patch(
825
- self,
826
- url: str,
827
- json_data: Optional[Dict[str, Any]] = None,
828
- form_data: Optional[Dict[str, Any]] = None,
829
- headers: Optional[Dict[str, str]] = None,
830
- timeout: Optional[float] = None,
831
- api_key: Optional[str] = None,
832
- bearer_token: Optional[str] = None,
833
- retry_attempts: int = 0,
834
- **kwargs,
835
- ) -> HttpResponse:
836
- """Make a PATCH request with semantic parameters."""
837
- return self._run_async(
838
- self._async_client.patch(
839
- url,
840
- json_data=json_data,
841
- form_data=form_data,
842
- headers=headers,
843
- timeout=timeout,
844
- api_key=api_key,
845
- bearer_token=bearer_token,
846
- retry_attempts=retry_attempts,
847
- **kwargs,
848
- )
849
- )
850
-
851
- def delete(
852
- self,
853
- url: str,
854
- headers: Optional[Dict[str, str]] = None,
855
- timeout: Optional[float] = None,
856
- api_key: Optional[str] = None,
857
- bearer_token: Optional[str] = None,
858
- retry_attempts: int = 0,
859
- **kwargs,
860
- ) -> HttpResponse:
861
- """Make a DELETE request with semantic parameters."""
862
- return self._run_async(
863
- self._async_client.delete(
864
- url,
865
- headers=headers,
866
- timeout=timeout,
867
- api_key=api_key,
868
- bearer_token=bearer_token,
869
- retry_attempts=retry_attempts,
870
- **kwargs,
871
- )
872
- )
873
-
874
-
875
- @overload
876
- def create_http_client(
877
- base_url: Optional[str] = None,
878
- default_headers: Optional[Dict[str, str]] = None,
879
- timeout: float = 30.0,
880
- follow_redirects: bool = True,
881
- verify_ssl: bool = True,
882
- # Semantic authentication parameters
883
- api_key: Optional[str] = None,
884
- api_key_header: str = "X-API-Key",
885
- bearer_token: Optional[str] = None,
886
- basic_auth: Optional[tuple[str, str]] = None,
887
- user_agent: Optional[str] = None,
888
- async_client: Literal[True] = ...,
889
- ) -> AsyncHttpClient: ...
890
-
891
-
892
- @overload
893
- def create_http_client(
894
- base_url: Optional[str] = None,
895
- default_headers: Optional[Dict[str, str]] = None,
896
- timeout: float = 30.0,
897
- follow_redirects: bool = True,
898
- verify_ssl: bool = True,
899
- # Semantic authentication parameters
900
- api_key: Optional[str] = None,
901
- api_key_header: str = "X-API-Key",
902
- bearer_token: Optional[str] = None,
903
- basic_auth: Optional[tuple[str, str]] = None,
904
- user_agent: Optional[str] = None,
905
- async_client: Literal[False] = ...,
906
- ) -> HttpClient: ...
907
-
908
-
909
- def create_http_client(
910
- base_url: Optional[str] = None,
911
- default_headers: Optional[Dict[str, str]] = None,
912
- timeout: float = 30.0,
913
- follow_redirects: bool = True,
914
- verify_ssl: bool = True,
915
- # Semantic authentication parameters
916
- api_key: Optional[str] = None,
917
- api_key_header: str = "X-API-Key",
918
- bearer_token: Optional[str] = None,
919
- basic_auth: Optional[tuple[str, str]] = None,
920
- user_agent: Optional[str] = None,
921
- async_client: bool = False,
922
- ) -> Union[HttpClient, AsyncHttpClient]:
923
- """
924
- Create a new HttpClient instance.
925
-
926
- Args:
927
- base_url: Base URL for all requests (optional)
928
- default_headers: Default headers to include in all requests
929
- timeout: Default timeout in seconds
930
- follow_redirects: Whether to follow redirects by default
931
- verify_ssl: Whether to verify SSL certificates
932
- api_key: API key for authentication
933
- api_key_header: Header name for API key (default: X-API-Key)
934
- bearer_token: Bearer token for Authorization header
935
- basic_auth: Tuple of (username, password) for basic auth
936
- user_agent: User-Agent header value
937
- """
938
- params = locals()
939
- del params["async_client"]
940
-
941
- if async_client:
942
- return AsyncHttpClient(**params)
943
- else:
944
- return HttpClient(**params)