airbyte-agent-greenhouse 0.17.48__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.
- airbyte_agent_greenhouse/__init__.py +105 -0
- airbyte_agent_greenhouse/_vendored/__init__.py +1 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/__init__.py +82 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/auth_strategies.py +1120 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/auth_template.py +135 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/cloud_utils/__init__.py +5 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/cloud_utils/client.py +213 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/connector_model_loader.py +965 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/constants.py +78 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/exceptions.py +23 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/executor/__init__.py +31 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/executor/hosted_executor.py +196 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/executor/local_executor.py +1724 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/executor/models.py +190 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/extensions.py +693 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/http/__init__.py +37 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/http/adapters/__init__.py +9 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/http/adapters/httpx_adapter.py +251 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/http/config.py +98 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/http/exceptions.py +119 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/http/protocols.py +114 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/http/response.py +104 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/http_client.py +693 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/introspection.py +262 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/logging/__init__.py +11 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/logging/logger.py +273 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/logging/types.py +93 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/observability/__init__.py +11 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/observability/config.py +179 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/observability/models.py +19 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/observability/redactor.py +81 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/observability/session.py +103 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/performance/__init__.py +6 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/performance/instrumentation.py +57 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/performance/metrics.py +93 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/schema/__init__.py +75 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/schema/base.py +164 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/schema/components.py +239 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/schema/connector.py +120 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/schema/extensions.py +230 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/schema/operations.py +146 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/schema/security.py +223 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/secrets.py +182 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/telemetry/__init__.py +10 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/telemetry/config.py +32 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/telemetry/events.py +59 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/telemetry/tracker.py +155 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/types.py +245 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/utils.py +60 -0
- airbyte_agent_greenhouse/_vendored/connector_sdk/validation.py +828 -0
- airbyte_agent_greenhouse/connector.py +1391 -0
- airbyte_agent_greenhouse/connector_model.py +2356 -0
- airbyte_agent_greenhouse/models.py +281 -0
- airbyte_agent_greenhouse/types.py +136 -0
- airbyte_agent_greenhouse-0.17.48.dist-info/METADATA +116 -0
- airbyte_agent_greenhouse-0.17.48.dist-info/RECORD +57 -0
- airbyte_agent_greenhouse-0.17.48.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""HTTP abstraction layer for the Airbyte SDK.
|
|
2
|
+
|
|
3
|
+
This package provides a client-agnostic HTTP interface that allows the SDK to work
|
|
4
|
+
with different HTTP client implementations (httpx, aiohttp, etc.) while maintaining
|
|
5
|
+
a consistent API.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .config import ClientConfig, ConnectionLimits, TimeoutConfig
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
HTTPClientError,
|
|
12
|
+
HTTPStatusError,
|
|
13
|
+
NetworkError,
|
|
14
|
+
RateLimitError,
|
|
15
|
+
TimeoutError,
|
|
16
|
+
)
|
|
17
|
+
from .protocols import HTTPClientProtocol, HTTPResponseProtocol
|
|
18
|
+
from .response import HTTPResponse
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
# Configuration
|
|
22
|
+
"ClientConfig",
|
|
23
|
+
"ConnectionLimits",
|
|
24
|
+
"TimeoutConfig",
|
|
25
|
+
# Protocols
|
|
26
|
+
"HTTPClientProtocol",
|
|
27
|
+
"HTTPResponseProtocol",
|
|
28
|
+
# Response
|
|
29
|
+
"HTTPResponse",
|
|
30
|
+
# Exceptions
|
|
31
|
+
"HTTPClientError",
|
|
32
|
+
"HTTPStatusError",
|
|
33
|
+
"AuthenticationError",
|
|
34
|
+
"RateLimitError",
|
|
35
|
+
"NetworkError",
|
|
36
|
+
"TimeoutError",
|
|
37
|
+
]
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""HTTPX adapter implementing the HTTP client protocol."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from ..config import ClientConfig, ConnectionLimits, TimeoutConfig
|
|
8
|
+
from ..exceptions import (
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
HTTPStatusError,
|
|
11
|
+
NetworkError,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
TimeoutError,
|
|
14
|
+
)
|
|
15
|
+
from ..protocols import HTTPResponseProtocol
|
|
16
|
+
from ..response import HTTPResponse
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HTTPXClient:
|
|
20
|
+
"""HTTPX-based implementation of the HTTP client protocol.
|
|
21
|
+
|
|
22
|
+
This adapter wraps httpx.AsyncClient and provides the HTTPClientProtocol interface,
|
|
23
|
+
allowing httpx to be swapped out for a different HTTP client in the future.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, config: ClientConfig | None = None) -> None:
|
|
27
|
+
"""Initialize the HTTPX client adapter.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
config: Client configuration. If None, uses default configuration.
|
|
31
|
+
"""
|
|
32
|
+
self.config = config or ClientConfig()
|
|
33
|
+
self._client: httpx.AsyncClient | None = None
|
|
34
|
+
|
|
35
|
+
def _create_client(self) -> httpx.AsyncClient:
|
|
36
|
+
"""Create and configure the httpx AsyncClient.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Configured httpx.AsyncClient instance.
|
|
40
|
+
"""
|
|
41
|
+
# Convert SDK config to httpx config
|
|
42
|
+
limits = self._convert_limits(self.config.limits) # type: ignore
|
|
43
|
+
timeout = self._convert_timeout(self.config.timeout) # type: ignore
|
|
44
|
+
|
|
45
|
+
return httpx.AsyncClient(
|
|
46
|
+
base_url=self.config.base_url or "",
|
|
47
|
+
timeout=timeout,
|
|
48
|
+
limits=limits,
|
|
49
|
+
follow_redirects=self.config.follow_redirects,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def _convert_limits(self, limits: ConnectionLimits) -> httpx.Limits:
|
|
53
|
+
"""Convert SDK ConnectionLimits to httpx.Limits.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
limits: SDK connection limits configuration
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
httpx.Limits instance
|
|
60
|
+
"""
|
|
61
|
+
return httpx.Limits(
|
|
62
|
+
max_connections=limits.max_connections,
|
|
63
|
+
max_keepalive_connections=limits.max_keepalive_connections,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def _convert_timeout(self, timeout: TimeoutConfig) -> httpx.Timeout:
|
|
67
|
+
"""Convert SDK TimeoutConfig to httpx.Timeout.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
timeout: SDK timeout configuration
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
httpx.Timeout instance
|
|
74
|
+
"""
|
|
75
|
+
return httpx.Timeout(
|
|
76
|
+
connect=timeout.connect,
|
|
77
|
+
read=timeout.read,
|
|
78
|
+
write=timeout.write,
|
|
79
|
+
pool=timeout.pool,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def _convert_response(self, httpx_response: httpx.Response, *, stream: bool = False) -> HTTPResponse:
|
|
83
|
+
"""Convert httpx.Response to SDK HTTPResponse.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
httpx_response: The httpx response object
|
|
87
|
+
stream: Whether the response should be treated as streaming (do not eagerly read body)
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
HTTPResponse wrapping the httpx response
|
|
91
|
+
"""
|
|
92
|
+
return HTTPResponse(
|
|
93
|
+
status_code=httpx_response.status_code,
|
|
94
|
+
headers=dict(httpx_response.headers),
|
|
95
|
+
# When streaming, avoid eagerly reading the body
|
|
96
|
+
content=b"" if stream else httpx_response.content,
|
|
97
|
+
_original_response=httpx_response,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
async def request(
|
|
101
|
+
self,
|
|
102
|
+
method: str,
|
|
103
|
+
url: str,
|
|
104
|
+
*,
|
|
105
|
+
params: dict[str, Any] | None = None,
|
|
106
|
+
json: dict[str, Any] | None = None,
|
|
107
|
+
data: dict[str, Any] | str | None = None,
|
|
108
|
+
headers: dict[str, str] | None = None,
|
|
109
|
+
**kwargs: Any,
|
|
110
|
+
) -> HTTPResponseProtocol:
|
|
111
|
+
"""Execute an HTTP request using httpx.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
method: HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
115
|
+
url: The URL to request
|
|
116
|
+
params: Query parameters to append to the URL
|
|
117
|
+
json: JSON data to send in the request body
|
|
118
|
+
data: Form data or raw string to send in the request body
|
|
119
|
+
headers: HTTP headers to include in the request
|
|
120
|
+
**kwargs: Additional httpx-specific parameters
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
HTTPResponse with the response data.
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
HTTPStatusError: For 4xx or 5xx HTTP status codes
|
|
127
|
+
AuthenticationError: For 401 or 403 status codes
|
|
128
|
+
RateLimitError: For 429 status codes
|
|
129
|
+
TimeoutError: For timeout errors
|
|
130
|
+
NetworkError: For network/connection errors
|
|
131
|
+
"""
|
|
132
|
+
if self._client is None:
|
|
133
|
+
self._client = self._create_client()
|
|
134
|
+
|
|
135
|
+
# Extract stream parameter (not supported by httpx.request directly)
|
|
136
|
+
stream = kwargs.pop("stream", False)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
# Execute the request
|
|
140
|
+
httpx_response = await self._client.request(
|
|
141
|
+
method=method,
|
|
142
|
+
url=url,
|
|
143
|
+
params=params,
|
|
144
|
+
json=json,
|
|
145
|
+
data=data,
|
|
146
|
+
headers=headers,
|
|
147
|
+
**kwargs,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Convert to SDK response
|
|
151
|
+
response = self._convert_response(httpx_response, stream=stream)
|
|
152
|
+
|
|
153
|
+
# Check for HTTP errors and wrap them
|
|
154
|
+
if httpx_response.status_code >= 400:
|
|
155
|
+
await self._handle_http_error(httpx_response, response)
|
|
156
|
+
|
|
157
|
+
return response
|
|
158
|
+
|
|
159
|
+
except httpx.TimeoutException as e:
|
|
160
|
+
raise TimeoutError(
|
|
161
|
+
message=f"Request timed out: {e}",
|
|
162
|
+
timeout_type=None, # httpx doesn't provide specific timeout type
|
|
163
|
+
original_error=e,
|
|
164
|
+
) from e
|
|
165
|
+
|
|
166
|
+
except (httpx.ConnectError, httpx.NetworkError) as e:
|
|
167
|
+
raise NetworkError(
|
|
168
|
+
message=f"Network error: {e}",
|
|
169
|
+
original_error=e,
|
|
170
|
+
) from e
|
|
171
|
+
|
|
172
|
+
async def _handle_http_error(self, httpx_response: httpx.Response, sdk_response: HTTPResponse) -> None:
|
|
173
|
+
"""Handle HTTP error responses by raising appropriate SDK exceptions.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
httpx_response: The original httpx response
|
|
177
|
+
sdk_response: The converted SDK response
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
AuthenticationError: For 401 or 403 status codes
|
|
181
|
+
RateLimitError: For 429 status codes
|
|
182
|
+
HTTPStatusError: For other 4xx or 5xx status codes
|
|
183
|
+
"""
|
|
184
|
+
status_code = httpx_response.status_code
|
|
185
|
+
|
|
186
|
+
# Try to get error message from response
|
|
187
|
+
try:
|
|
188
|
+
error_data = httpx_response.json()
|
|
189
|
+
error_message = error_data.get("message") or error_data.get("error") or str(error_data)
|
|
190
|
+
except Exception:
|
|
191
|
+
error_message = httpx_response.text or f"HTTP {status_code} error"
|
|
192
|
+
|
|
193
|
+
# Raise specific exceptions based on status code
|
|
194
|
+
if status_code in (401, 403):
|
|
195
|
+
raise AuthenticationError(
|
|
196
|
+
message=f"Authentication failed: {error_message} (HTTP {status_code})",
|
|
197
|
+
status_code=status_code,
|
|
198
|
+
response=sdk_response,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if status_code == 429:
|
|
202
|
+
# Try to parse Retry-After header
|
|
203
|
+
retry_after = None
|
|
204
|
+
if "retry-after" in httpx_response.headers:
|
|
205
|
+
try:
|
|
206
|
+
retry_after = int(httpx_response.headers["retry-after"])
|
|
207
|
+
except ValueError:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
error_msg = f"Rate limit exceeded: {error_message}"
|
|
211
|
+
if retry_after:
|
|
212
|
+
error_msg += f" (retry after {retry_after}s)"
|
|
213
|
+
|
|
214
|
+
raise RateLimitError(
|
|
215
|
+
message=error_msg,
|
|
216
|
+
retry_after=retry_after,
|
|
217
|
+
response=sdk_response,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Generic HTTP error
|
|
221
|
+
raise HTTPStatusError(
|
|
222
|
+
status_code=status_code,
|
|
223
|
+
message=error_message,
|
|
224
|
+
response=sdk_response,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
async def aclose(self) -> None:
|
|
228
|
+
"""Close the HTTP client and cleanup resources."""
|
|
229
|
+
if self._client is not None:
|
|
230
|
+
await self._client.aclose()
|
|
231
|
+
self._client = None
|
|
232
|
+
|
|
233
|
+
async def __aenter__(self) -> "HTTPXClient":
|
|
234
|
+
"""Enter async context manager.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
The client instance.
|
|
238
|
+
"""
|
|
239
|
+
if self._client is None:
|
|
240
|
+
self._client = self._create_client()
|
|
241
|
+
return self
|
|
242
|
+
|
|
243
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
244
|
+
"""Exit async context manager and cleanup resources.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
exc_type: Exception type if an exception occurred
|
|
248
|
+
exc_val: Exception value if an exception occurred
|
|
249
|
+
exc_tb: Exception traceback if an exception occurred
|
|
250
|
+
"""
|
|
251
|
+
await self.aclose()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Configuration classes for HTTP clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from ..constants import (
|
|
8
|
+
DEFAULT_CONNECT_TIMEOUT,
|
|
9
|
+
DEFAULT_MAX_CONNECTIONS,
|
|
10
|
+
DEFAULT_MAX_KEEPALIVE_CONNECTIONS,
|
|
11
|
+
DEFAULT_POOL_TIMEOUT,
|
|
12
|
+
DEFAULT_READ_TIMEOUT,
|
|
13
|
+
DEFAULT_WRITE_TIMEOUT,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ConnectionLimits:
|
|
19
|
+
"""Configuration for HTTP connection pooling limits.
|
|
20
|
+
|
|
21
|
+
This replaces httpx.Limits and provides a client-agnostic way to configure
|
|
22
|
+
connection pooling behavior.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
max_connections: int = DEFAULT_MAX_CONNECTIONS
|
|
26
|
+
"""Maximum number of concurrent connections to allow."""
|
|
27
|
+
|
|
28
|
+
max_keepalive_connections: int = DEFAULT_MAX_KEEPALIVE_CONNECTIONS
|
|
29
|
+
"""Maximum number of connections to keep alive in the pool."""
|
|
30
|
+
|
|
31
|
+
def __post_init__(self) -> None:
|
|
32
|
+
"""Validate configuration values."""
|
|
33
|
+
if self.max_connections < 1:
|
|
34
|
+
raise ValueError("max_connections must be at least 1")
|
|
35
|
+
if self.max_keepalive_connections < 0:
|
|
36
|
+
raise ValueError("max_keepalive_connections must be non-negative")
|
|
37
|
+
if self.max_keepalive_connections > self.max_connections:
|
|
38
|
+
raise ValueError("max_keepalive_connections cannot exceed max_connections")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class TimeoutConfig:
|
|
43
|
+
"""Configuration for HTTP request timeouts.
|
|
44
|
+
|
|
45
|
+
This replaces httpx.Timeout and provides a client-agnostic way to configure
|
|
46
|
+
timeout behavior for different phases of the HTTP request.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
connect: float | None = DEFAULT_CONNECT_TIMEOUT
|
|
50
|
+
"""Timeout for establishing a connection (seconds). None means no timeout."""
|
|
51
|
+
|
|
52
|
+
read: float | None = DEFAULT_READ_TIMEOUT
|
|
53
|
+
"""Timeout for reading response data (seconds). None means no timeout."""
|
|
54
|
+
|
|
55
|
+
write: float | None = DEFAULT_WRITE_TIMEOUT
|
|
56
|
+
"""Timeout for writing request data (seconds). None means no timeout."""
|
|
57
|
+
|
|
58
|
+
pool: float | None = DEFAULT_POOL_TIMEOUT
|
|
59
|
+
"""Timeout for acquiring a connection from the pool (seconds). None means no timeout."""
|
|
60
|
+
|
|
61
|
+
def __post_init__(self) -> None:
|
|
62
|
+
"""Validate configuration values."""
|
|
63
|
+
for name, value in [
|
|
64
|
+
("connect", self.connect),
|
|
65
|
+
("read", self.read),
|
|
66
|
+
("write", self.write),
|
|
67
|
+
("pool", self.pool),
|
|
68
|
+
]:
|
|
69
|
+
if value is not None and value <= 0:
|
|
70
|
+
raise ValueError(f"{name} timeout must be positive or None")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class ClientConfig:
|
|
75
|
+
"""Overall configuration for an HTTP client.
|
|
76
|
+
|
|
77
|
+
This provides a complete, client-agnostic configuration for HTTP clients
|
|
78
|
+
that can be mapped to any underlying HTTP client implementation.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
timeout: TimeoutConfig | None = None
|
|
82
|
+
"""Timeout configuration. If None, uses default timeouts."""
|
|
83
|
+
|
|
84
|
+
limits: ConnectionLimits | None = None
|
|
85
|
+
"""Connection pooling limits. If None, uses default limits."""
|
|
86
|
+
|
|
87
|
+
base_url: str | None = None
|
|
88
|
+
"""Optional base URL to prepend to all requests."""
|
|
89
|
+
|
|
90
|
+
follow_redirects: bool = True
|
|
91
|
+
"""Whether to automatically follow HTTP redirects."""
|
|
92
|
+
|
|
93
|
+
def __post_init__(self) -> None:
|
|
94
|
+
"""Set default values for None fields."""
|
|
95
|
+
if self.timeout is None:
|
|
96
|
+
self.timeout = TimeoutConfig()
|
|
97
|
+
if self.limits is None:
|
|
98
|
+
self.limits = ConnectionLimits()
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""HTTP-related exceptions for the Airbyte SDK."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .response import HTTPResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HTTPClientError(Exception):
|
|
10
|
+
"""Base exception for HTTP client errors."""
|
|
11
|
+
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HTTPStatusError(HTTPClientError):
|
|
16
|
+
"""Raised when an HTTP response has a 4xx or 5xx status code.
|
|
17
|
+
|
|
18
|
+
This is the base exception for status code errors and is raised by
|
|
19
|
+
HTTPResponse.raise_for_status().
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
status_code: int,
|
|
25
|
+
message: str,
|
|
26
|
+
response: "HTTPResponse | None" = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Initialize HTTP status error.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
status_code: The HTTP status code (e.g., 404, 500)
|
|
32
|
+
message: Error message describing the issue
|
|
33
|
+
response: Optional HTTPResponse object for accessing details
|
|
34
|
+
"""
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
self.status_code = status_code
|
|
37
|
+
self.response = response
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AuthenticationError(HTTPStatusError):
|
|
41
|
+
"""Raised when authentication credentials are missing or invalid (401, 403)."""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
message: str,
|
|
46
|
+
status_code: int = 401,
|
|
47
|
+
response: "HTTPResponse | None" = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Initialize authentication error.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
message: Error message describing the authentication issue
|
|
53
|
+
status_code: HTTP status code (401 or 403)
|
|
54
|
+
response: Optional HTTPResponse object for accessing details
|
|
55
|
+
"""
|
|
56
|
+
super().__init__(status_code=status_code, message=message, response=response)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RateLimitError(HTTPStatusError):
|
|
60
|
+
"""Raised when API rate limit is exceeded (429 response)."""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
message: str,
|
|
65
|
+
retry_after: int | None = None,
|
|
66
|
+
response: "HTTPResponse | None" = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Initialize rate limit error.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
message: Error message describing the rate limit
|
|
72
|
+
retry_after: Seconds to wait before retrying (from Retry-After header)
|
|
73
|
+
response: Optional HTTPResponse object for accessing details
|
|
74
|
+
"""
|
|
75
|
+
super().__init__(status_code=429, message=message, response=response)
|
|
76
|
+
self.retry_after = retry_after
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class NetworkError(HTTPClientError):
|
|
80
|
+
"""Raised when network connection fails.
|
|
81
|
+
|
|
82
|
+
This includes connection errors, DNS resolution failures, and other
|
|
83
|
+
network-level issues.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, message: str, original_error: Exception | None = None) -> None:
|
|
87
|
+
"""Initialize network error.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
message: Error message describing the network issue
|
|
91
|
+
original_error: Optional original exception from the HTTP client
|
|
92
|
+
"""
|
|
93
|
+
super().__init__(message)
|
|
94
|
+
self.original_error = original_error
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TimeoutError(HTTPClientError):
|
|
98
|
+
"""Raised when a request times out.
|
|
99
|
+
|
|
100
|
+
This can occur during connection establishment, reading the response,
|
|
101
|
+
or writing the request.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
message: str,
|
|
107
|
+
timeout_type: str | None = None,
|
|
108
|
+
original_error: Exception | None = None,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Initialize timeout error.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
message: Error message describing the timeout
|
|
114
|
+
timeout_type: Optional type of timeout (connect, read, write, pool)
|
|
115
|
+
original_error: Optional original exception from the HTTP client
|
|
116
|
+
"""
|
|
117
|
+
super().__init__(message)
|
|
118
|
+
self.timeout_type = timeout_type
|
|
119
|
+
self.original_error = original_error
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""HTTP client and response protocols for abstracting HTTP client implementations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@runtime_checkable
|
|
7
|
+
class HTTPResponseProtocol(Protocol):
|
|
8
|
+
"""Protocol defining the interface for HTTP responses.
|
|
9
|
+
|
|
10
|
+
This protocol abstracts the response interface, allowing different HTTP clients
|
|
11
|
+
to provide responses that work with the SDK.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def status_code(self) -> int:
|
|
16
|
+
"""The HTTP status code of the response."""
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def headers(self) -> dict[str, str]:
|
|
21
|
+
"""The response headers as a dictionary."""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
async def json(self) -> Any:
|
|
25
|
+
"""Parse the response body as JSON.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
The parsed JSON data (dict, list, or primitive).
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ValueError: If the response body is not valid JSON.
|
|
32
|
+
"""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
async def text(self) -> str:
|
|
36
|
+
"""Get the response body as text.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The response body decoded as a string.
|
|
40
|
+
"""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
def raise_for_status(self) -> None:
|
|
44
|
+
"""Raise an exception if the response status indicates an error.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
Exception: For 4xx or 5xx status codes.
|
|
48
|
+
"""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@runtime_checkable
|
|
53
|
+
class HTTPClientProtocol(Protocol):
|
|
54
|
+
"""Protocol defining the interface for HTTP clients.
|
|
55
|
+
|
|
56
|
+
This protocol abstracts the HTTP client interface, allowing the SDK to work with
|
|
57
|
+
different HTTP client implementations (httpx, aiohttp, etc.).
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
async def request(
|
|
61
|
+
self,
|
|
62
|
+
method: str,
|
|
63
|
+
url: str,
|
|
64
|
+
*,
|
|
65
|
+
params: dict[str, Any] | None = None,
|
|
66
|
+
json: dict[str, Any] | None = None,
|
|
67
|
+
data: dict[str, Any] | str | None = None,
|
|
68
|
+
headers: dict[str, str] | None = None,
|
|
69
|
+
**kwargs: Any,
|
|
70
|
+
) -> HTTPResponseProtocol:
|
|
71
|
+
"""Execute an HTTP request.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
method: HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
75
|
+
url: The URL to request
|
|
76
|
+
params: Query parameters to append to the URL
|
|
77
|
+
json: JSON data to send in the request body
|
|
78
|
+
data: Form data or raw string to send in the request body
|
|
79
|
+
headers: HTTP headers to include in the request
|
|
80
|
+
**kwargs: Additional client-specific parameters
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
An HTTPResponseProtocol implementation with the response data.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
Exception: For network errors, timeouts, or other request failures.
|
|
87
|
+
"""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
async def aclose(self) -> None:
|
|
91
|
+
"""Close the HTTP client and cleanup resources.
|
|
92
|
+
|
|
93
|
+
This should be called when the client is no longer needed to properly
|
|
94
|
+
cleanup connections and resources.
|
|
95
|
+
"""
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
async def __aenter__(self) -> "HTTPClientProtocol":
|
|
99
|
+
"""Enter async context manager.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
The client instance.
|
|
103
|
+
"""
|
|
104
|
+
...
|
|
105
|
+
|
|
106
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
107
|
+
"""Exit async context manager and cleanup resources.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
exc_type: Exception type if an exception occurred
|
|
111
|
+
exc_val: Exception value if an exception occurred
|
|
112
|
+
exc_tb: Exception traceback if an exception occurred
|
|
113
|
+
"""
|
|
114
|
+
...
|