airbyte-agent-hubspot 0.15.20__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_hubspot/__init__.py +86 -0
- airbyte_agent_hubspot/_vendored/__init__.py +1 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/__init__.py +82 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/auth_strategies.py +1123 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/auth_template.py +135 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/cloud_utils/__init__.py +5 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/cloud_utils/client.py +213 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/connector_model_loader.py +957 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/constants.py +78 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/exceptions.py +23 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/executor/__init__.py +31 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/executor/hosted_executor.py +197 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/executor/local_executor.py +1504 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/executor/models.py +190 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/extensions.py +655 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/http/__init__.py +37 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/http/adapters/__init__.py +9 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/http/adapters/httpx_adapter.py +251 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/http/config.py +98 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/http/exceptions.py +119 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/http/protocols.py +114 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/http/response.py +102 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/http_client.py +679 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/logging/__init__.py +11 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/logging/logger.py +264 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/logging/types.py +92 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/observability/__init__.py +11 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/observability/models.py +19 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/observability/redactor.py +81 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/observability/session.py +94 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/performance/__init__.py +6 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/performance/instrumentation.py +57 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/performance/metrics.py +93 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/schema/__init__.py +75 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/schema/base.py +161 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/schema/components.py +238 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/schema/connector.py +131 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/schema/extensions.py +109 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/schema/operations.py +146 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/schema/security.py +213 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/secrets.py +182 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/telemetry/__init__.py +10 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/telemetry/config.py +32 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/telemetry/events.py +58 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/telemetry/tracker.py +151 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/types.py +241 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/utils.py +60 -0
- airbyte_agent_hubspot/_vendored/connector_sdk/validation.py +822 -0
- airbyte_agent_hubspot/connector.py +1104 -0
- airbyte_agent_hubspot/connector_model.py +2660 -0
- airbyte_agent_hubspot/models.py +438 -0
- airbyte_agent_hubspot/types.py +217 -0
- airbyte_agent_hubspot-0.15.20.dist-info/METADATA +105 -0
- airbyte_agent_hubspot-0.15.20.dist-info/RECORD +55 -0
- airbyte_agent_hubspot-0.15.20.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Telemetry configuration from environment variables."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
# Hardcoded Segment write key for Airbyte telemetry
|
|
7
|
+
SEGMENT_WRITE_KEY = "sFM7q98HtHTMmCW3d6nsPWYCIdrbs7gq"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TelemetryMode(Enum):
|
|
11
|
+
"""Telemetry tracking modes."""
|
|
12
|
+
|
|
13
|
+
BASIC = "basic"
|
|
14
|
+
DISABLED = "disabled"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TelemetryConfig:
|
|
18
|
+
"""Telemetry configuration from environment variables."""
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def get_mode() -> TelemetryMode:
|
|
22
|
+
"""Get telemetry mode from environment variable."""
|
|
23
|
+
mode_str = os.getenv("AIRBYTE_TELEMETRY_MODE", "basic").lower()
|
|
24
|
+
try:
|
|
25
|
+
return TelemetryMode(mode_str)
|
|
26
|
+
except ValueError:
|
|
27
|
+
return TelemetryMode.BASIC
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def is_enabled() -> bool:
|
|
31
|
+
"""Telemetry is enabled if mode is not disabled."""
|
|
32
|
+
return TelemetryConfig.get_mode() != TelemetryMode.DISABLED
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Telemetry event models."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class BaseEvent:
|
|
10
|
+
"""Base class for all telemetry events."""
|
|
11
|
+
|
|
12
|
+
timestamp: datetime
|
|
13
|
+
session_id: str
|
|
14
|
+
user_id: str
|
|
15
|
+
execution_context: str
|
|
16
|
+
|
|
17
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
18
|
+
"""Convert event to dictionary with ISO formatted timestamp."""
|
|
19
|
+
data = asdict(self)
|
|
20
|
+
data["timestamp"] = self.timestamp.isoformat()
|
|
21
|
+
return data
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ConnectorInitEvent(BaseEvent):
|
|
26
|
+
"""Connector initialization event."""
|
|
27
|
+
|
|
28
|
+
connector_name: str
|
|
29
|
+
python_version: str
|
|
30
|
+
os_name: str
|
|
31
|
+
os_version: str
|
|
32
|
+
public_ip: Optional[str] = None
|
|
33
|
+
connector_version: Optional[str] = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class OperationEvent(BaseEvent):
|
|
38
|
+
"""API operation event."""
|
|
39
|
+
|
|
40
|
+
connector_name: str
|
|
41
|
+
entity: str
|
|
42
|
+
action: str
|
|
43
|
+
timing_ms: float
|
|
44
|
+
public_ip: Optional[str] = None
|
|
45
|
+
status_code: Optional[int] = None
|
|
46
|
+
error_type: Optional[str] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class SessionEndEvent(BaseEvent):
|
|
51
|
+
"""Session end event."""
|
|
52
|
+
|
|
53
|
+
connector_name: str
|
|
54
|
+
duration_seconds: float
|
|
55
|
+
operation_count: int
|
|
56
|
+
success_count: int
|
|
57
|
+
failure_count: int
|
|
58
|
+
public_ip: Optional[str] = None
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Anonymous telemetry tracker using Segment."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import platform
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from ..observability import ObservabilitySession
|
|
10
|
+
|
|
11
|
+
from .config import SEGMENT_WRITE_KEY, TelemetryConfig, TelemetryMode
|
|
12
|
+
from .events import ConnectorInitEvent, OperationEvent, SessionEndEvent
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SegmentTracker:
|
|
18
|
+
"""Anonymous telemetry tracker using Segment."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
session: ObservabilitySession,
|
|
23
|
+
mode: Optional[TelemetryMode] = None,
|
|
24
|
+
):
|
|
25
|
+
self.session = session
|
|
26
|
+
self.mode = mode or TelemetryConfig.get_mode()
|
|
27
|
+
self.success_count = 0
|
|
28
|
+
self.failure_count = 0
|
|
29
|
+
self.enabled = TelemetryConfig.is_enabled()
|
|
30
|
+
self._analytics = None
|
|
31
|
+
|
|
32
|
+
if self.enabled:
|
|
33
|
+
try:
|
|
34
|
+
import segment.analytics as analytics
|
|
35
|
+
|
|
36
|
+
analytics.write_key = SEGMENT_WRITE_KEY
|
|
37
|
+
self._analytics = analytics
|
|
38
|
+
self._log_startup_message()
|
|
39
|
+
except ImportError:
|
|
40
|
+
logger.warning("Telemetry disabled: segment-analytics-python not installed")
|
|
41
|
+
self.enabled = False
|
|
42
|
+
|
|
43
|
+
def _log_startup_message(self):
|
|
44
|
+
"""Log message when telemetry is enabled."""
|
|
45
|
+
logger.info(f"Anonymous telemetry enabled (mode: {self.mode.value})")
|
|
46
|
+
logger.info("To opt-out: export AIRBYTE_TELEMETRY_MODE=disabled")
|
|
47
|
+
|
|
48
|
+
def track_connector_init(
|
|
49
|
+
self,
|
|
50
|
+
connector_version: Optional[str] = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Track connector initialization."""
|
|
53
|
+
if not self.enabled or not self._analytics:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
event = ConnectorInitEvent(
|
|
58
|
+
timestamp=datetime.utcnow(),
|
|
59
|
+
session_id=self.session.session_id,
|
|
60
|
+
user_id=self.session.user_id,
|
|
61
|
+
execution_context=self.session.execution_context,
|
|
62
|
+
public_ip=self.session.public_ip,
|
|
63
|
+
connector_name=self.session.connector_name,
|
|
64
|
+
connector_version=connector_version,
|
|
65
|
+
python_version=f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
|
66
|
+
os_name=platform.system(),
|
|
67
|
+
os_version=platform.release(),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
self._analytics.track(
|
|
71
|
+
user_id=self.session.user_id,
|
|
72
|
+
anonymous_id=event.session_id,
|
|
73
|
+
event="Connector Initialized",
|
|
74
|
+
properties=event.to_dict(),
|
|
75
|
+
)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
# Never fail on tracking errors
|
|
78
|
+
logger.error(f"Telemetry error: {e}")
|
|
79
|
+
|
|
80
|
+
def track_operation(
|
|
81
|
+
self,
|
|
82
|
+
entity: str,
|
|
83
|
+
action: str,
|
|
84
|
+
status_code: Optional[int],
|
|
85
|
+
timing_ms: float,
|
|
86
|
+
error_type: Optional[str] = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Track API operation."""
|
|
89
|
+
# Always track success/failure counts (useful even when tracking is disabled)
|
|
90
|
+
if status_code and 200 <= status_code < 300:
|
|
91
|
+
self.success_count += 1
|
|
92
|
+
else:
|
|
93
|
+
self.failure_count += 1
|
|
94
|
+
|
|
95
|
+
if not self.enabled or not self._analytics:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
event = OperationEvent(
|
|
100
|
+
timestamp=datetime.utcnow(),
|
|
101
|
+
session_id=self.session.session_id,
|
|
102
|
+
user_id=self.session.user_id,
|
|
103
|
+
execution_context=self.session.execution_context,
|
|
104
|
+
public_ip=self.session.public_ip,
|
|
105
|
+
connector_name=self.session.connector_name,
|
|
106
|
+
entity=entity,
|
|
107
|
+
action=action,
|
|
108
|
+
status_code=status_code,
|
|
109
|
+
timing_ms=timing_ms,
|
|
110
|
+
error_type=error_type,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
self._analytics.track(
|
|
114
|
+
user_id=self.session.user_id,
|
|
115
|
+
anonymous_id=event.session_id,
|
|
116
|
+
event="Operation Executed",
|
|
117
|
+
properties=event.to_dict(),
|
|
118
|
+
)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.error(f"Telemetry error: {e}")
|
|
121
|
+
|
|
122
|
+
def track_session_end(self) -> None:
|
|
123
|
+
"""Track session end."""
|
|
124
|
+
if not self.enabled or not self._analytics:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
event = SessionEndEvent(
|
|
129
|
+
timestamp=datetime.utcnow(),
|
|
130
|
+
session_id=self.session.session_id,
|
|
131
|
+
user_id=self.session.user_id,
|
|
132
|
+
execution_context=self.session.execution_context,
|
|
133
|
+
public_ip=self.session.public_ip,
|
|
134
|
+
connector_name=self.session.connector_name,
|
|
135
|
+
duration_seconds=self.session.duration_seconds(),
|
|
136
|
+
operation_count=self.session.operation_count,
|
|
137
|
+
success_count=self.success_count,
|
|
138
|
+
failure_count=self.failure_count,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
self._analytics.track(
|
|
142
|
+
user_id=self.session.user_id,
|
|
143
|
+
anonymous_id=event.session_id,
|
|
144
|
+
event="Session Ended",
|
|
145
|
+
properties=event.to_dict(),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Ensure events are sent before shutdown
|
|
149
|
+
self._analytics.flush()
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error(f"Telemetry error: {e}")
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Type definitions for Airbyte SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
10
|
+
|
|
11
|
+
from .constants import OPENAPI_DEFAULT_VERSION
|
|
12
|
+
from .schema.components import PathOverrideConfig
|
|
13
|
+
from .schema.extensions import RetryConfig
|
|
14
|
+
from .schema.security import AirbyteAuthConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Action(str, Enum):
|
|
18
|
+
"""Supported actions for Entity operations."""
|
|
19
|
+
|
|
20
|
+
GET = "get"
|
|
21
|
+
CREATE = "create"
|
|
22
|
+
UPDATE = "update"
|
|
23
|
+
DELETE = "delete"
|
|
24
|
+
LIST = "list"
|
|
25
|
+
SEARCH = "search"
|
|
26
|
+
DOWNLOAD = "download"
|
|
27
|
+
AUTHORIZE = "authorize"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AuthType(str, Enum):
|
|
31
|
+
"""Supported authentication types."""
|
|
32
|
+
|
|
33
|
+
API_KEY = "api_key"
|
|
34
|
+
BEARER = "bearer"
|
|
35
|
+
HTTP = "http"
|
|
36
|
+
BASIC = "basic"
|
|
37
|
+
OAUTH2 = "oauth2"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ContentType(str, Enum):
|
|
41
|
+
"""Supported content types for request bodies."""
|
|
42
|
+
|
|
43
|
+
JSON = "application/json"
|
|
44
|
+
FORM_URLENCODED = "application/x-www-form-urlencoded"
|
|
45
|
+
FORM_DATA = "multipart/form-data"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ParameterLocation(str, Enum):
|
|
49
|
+
"""Location of operation parameters."""
|
|
50
|
+
|
|
51
|
+
PATH = "path"
|
|
52
|
+
QUERY = "query"
|
|
53
|
+
HEADER = "header"
|
|
54
|
+
COOKIE = "cookie"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# All comprehensive OpenAPI 3.0 models are now in connector_sdk.schema package
|
|
58
|
+
# Import from connector_sdk.schema for: OpenAPIConnector, Components, Schema, Operation, etc.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AuthOption(BaseModel):
|
|
62
|
+
"""A single authentication option in a multi-auth connector.
|
|
63
|
+
|
|
64
|
+
Represents one security scheme from OpenAPI components.securitySchemes.
|
|
65
|
+
Each option defines a complete authentication method with its own type,
|
|
66
|
+
configuration, and user-facing credential specification.
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
For a connector supporting both OAuth2 and API Key auth:
|
|
70
|
+
- AuthOption(scheme_name="oauth", type=OAUTH2, ...)
|
|
71
|
+
- AuthOption(scheme_name="apikey", type=BEARER, ...)
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
scheme_name: str = Field(description="Security scheme name from OpenAPI spec (e.g., 'githubOAuth', 'githubPAT')")
|
|
75
|
+
type: AuthType = Field(description="Authentication type for this option")
|
|
76
|
+
config: dict[str, Any] = Field(
|
|
77
|
+
default_factory=dict,
|
|
78
|
+
description="Auth-specific configuration (e.g., OAuth2 refresh settings)",
|
|
79
|
+
)
|
|
80
|
+
user_config_spec: AirbyteAuthConfig | None = Field(
|
|
81
|
+
None,
|
|
82
|
+
description="User-facing credential specification from x-airbyte-auth-config",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class AuthConfig(BaseModel):
|
|
87
|
+
"""Authentication configuration supporting single or multiple auth methods.
|
|
88
|
+
|
|
89
|
+
Connectors can define either:
|
|
90
|
+
- Single auth: One authentication method (backwards compatible)
|
|
91
|
+
- Multi-auth: Multiple authentication methods (user/agent selects one)
|
|
92
|
+
|
|
93
|
+
For single-auth connectors (most common):
|
|
94
|
+
AuthConfig(type=OAUTH2, config={...}, user_config_spec={...})
|
|
95
|
+
|
|
96
|
+
For multi-auth connectors:
|
|
97
|
+
AuthConfig(options=[
|
|
98
|
+
AuthOption(scheme_name="oauth", type=OAUTH2, ...),
|
|
99
|
+
AuthOption(scheme_name="apikey", type=BEARER, ...)
|
|
100
|
+
])
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
# Single-auth mode (backwards compatible)
|
|
104
|
+
type: AuthType | None = Field(
|
|
105
|
+
None,
|
|
106
|
+
description="Authentication type (single-auth mode only)",
|
|
107
|
+
)
|
|
108
|
+
config: dict[str, Any] = Field(
|
|
109
|
+
default_factory=dict,
|
|
110
|
+
description="Auth configuration (single-auth mode only)",
|
|
111
|
+
)
|
|
112
|
+
user_config_spec: AirbyteAuthConfig | None = Field(
|
|
113
|
+
None,
|
|
114
|
+
description="User-facing config spec from x-airbyte-auth-config (single-auth mode)",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Multi-auth mode
|
|
118
|
+
options: list[AuthOption] | None = Field(
|
|
119
|
+
None,
|
|
120
|
+
description="Multiple authentication options (multi-auth mode only)",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def is_multi_auth(self) -> bool:
|
|
124
|
+
"""Check if this configuration supports multiple authentication methods.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if multiple auth options are available, False for single-auth
|
|
128
|
+
"""
|
|
129
|
+
return self.options is not None and len(self.options) > 0
|
|
130
|
+
|
|
131
|
+
def get_single_option(self) -> AuthOption:
|
|
132
|
+
"""Get single auth option (for backwards compatibility).
|
|
133
|
+
|
|
134
|
+
Converts single-auth config to AuthOption format for uniform handling.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
AuthOption containing the single auth configuration
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
ValueError: If this is a multi-auth config or invalid
|
|
141
|
+
"""
|
|
142
|
+
if self.is_multi_auth():
|
|
143
|
+
raise ValueError("Cannot call get_single_option() on multi-auth config. " "Use options list instead.")
|
|
144
|
+
|
|
145
|
+
if self.type is None:
|
|
146
|
+
raise ValueError("Invalid AuthConfig: neither single-auth nor multi-auth")
|
|
147
|
+
|
|
148
|
+
return AuthOption(
|
|
149
|
+
scheme_name="default",
|
|
150
|
+
type=self.type,
|
|
151
|
+
config=self.config,
|
|
152
|
+
user_config_spec=self.user_config_spec,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# Executor types (used by executor.py)
|
|
157
|
+
class EndpointDefinition(BaseModel):
|
|
158
|
+
"""Definition of an API endpoint."""
|
|
159
|
+
|
|
160
|
+
method: str # GET, POST, PUT, DELETE, etc.
|
|
161
|
+
path: str # e.g., /v1/customers/{id} (OpenAPI path)
|
|
162
|
+
path_override: PathOverrideConfig | None = Field(
|
|
163
|
+
None,
|
|
164
|
+
description=("Path override config from x-airbyte-path-override. " "When set, overrides the path for actual HTTP requests."),
|
|
165
|
+
)
|
|
166
|
+
action: Action | None = None # Semantic action (get, list, create, update, delete)
|
|
167
|
+
description: str | None = None
|
|
168
|
+
body_fields: list[str] = Field(default_factory=list) # For POST/PUT
|
|
169
|
+
query_params: list[str] = Field(default_factory=list) # For GET
|
|
170
|
+
query_params_schema: dict[str, dict[str, Any]] = Field(
|
|
171
|
+
default_factory=dict,
|
|
172
|
+
description="Schema for query params including defaults: {name: {type, default, required}}",
|
|
173
|
+
)
|
|
174
|
+
deep_object_params: list[str] = Field(
|
|
175
|
+
default_factory=list,
|
|
176
|
+
description="Query parameters using deepObject style (e.g., filter[key]=value)",
|
|
177
|
+
) # For GET with deepObject query params
|
|
178
|
+
path_params: list[str] = Field(default_factory=list) # Extracted from path
|
|
179
|
+
path_params_schema: dict[str, dict[str, Any]] = Field(
|
|
180
|
+
default_factory=dict,
|
|
181
|
+
description="Schema for path params including defaults: {name: {type, default, required}}",
|
|
182
|
+
)
|
|
183
|
+
content_type: ContentType = ContentType.JSON
|
|
184
|
+
request_schema: dict[str, Any] | None = None
|
|
185
|
+
response_schema: dict[str, Any] | None = None
|
|
186
|
+
|
|
187
|
+
# GraphQL support (Airbyte extension)
|
|
188
|
+
graphql_body: dict[str, Any] | None = Field(
|
|
189
|
+
None,
|
|
190
|
+
description="GraphQL body configuration from x-airbyte-body-type extension",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Record extractor support (Airbyte extension)
|
|
194
|
+
record_extractor: str | None = Field(
|
|
195
|
+
None,
|
|
196
|
+
description="JSONPath expression to extract records from response envelopes",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Metadata extractor support (Airbyte extension)
|
|
200
|
+
meta_extractor: dict[str, str] | None = Field(
|
|
201
|
+
None,
|
|
202
|
+
description="Dictionary mapping field names to JSONPath expressions for extracting metadata from response envelopes",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Download support (Airbyte extension)
|
|
206
|
+
file_field: str | None = Field(
|
|
207
|
+
None,
|
|
208
|
+
description="Field in metadata response containing download URL (from x-airbyte-file-url extension)",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Test validation support (Airbyte extension)
|
|
212
|
+
untested: bool = Field(
|
|
213
|
+
False,
|
|
214
|
+
description="Mark operation as untested to skip cassette validation (from x-airbyte-untested extension)",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class EntityDefinition(BaseModel):
|
|
219
|
+
"""Definition of an API entity."""
|
|
220
|
+
|
|
221
|
+
model_config = {"populate_by_name": True}
|
|
222
|
+
|
|
223
|
+
name: str
|
|
224
|
+
actions: list[Action]
|
|
225
|
+
endpoints: dict[Action, EndpointDefinition]
|
|
226
|
+
entity_schema: dict[str, Any] | None = Field(default=None, alias="schema")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class ConnectorModel(BaseModel):
|
|
230
|
+
"""Complete connector model loaded from YAML definition."""
|
|
231
|
+
|
|
232
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
233
|
+
|
|
234
|
+
id: UUID
|
|
235
|
+
name: str
|
|
236
|
+
version: str = OPENAPI_DEFAULT_VERSION
|
|
237
|
+
base_url: str
|
|
238
|
+
auth: AuthConfig
|
|
239
|
+
entities: list[EntityDefinition]
|
|
240
|
+
openapi_spec: Any | None = None # Optional reference to OpenAPIConnector
|
|
241
|
+
retry_config: RetryConfig | None = None # Optional retry configuration
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Utility functions for working with connectors."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def save_download(
|
|
8
|
+
download_iterator: AsyncIterator[bytes],
|
|
9
|
+
path: str | Path,
|
|
10
|
+
*,
|
|
11
|
+
overwrite: bool = False,
|
|
12
|
+
) -> Path:
|
|
13
|
+
"""Save a download iterator to a file.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
download_iterator: AsyncIterator[bytes] from a download operation
|
|
17
|
+
path: File path where content should be saved
|
|
18
|
+
overwrite: Whether to overwrite existing file (default: False)
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Absolute Path to the saved file
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
FileExistsError: If file exists and overwrite=False
|
|
25
|
+
OSError: If file cannot be written
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> from .utils import save_download
|
|
29
|
+
>>>
|
|
30
|
+
>>> # Download and save a file
|
|
31
|
+
>>> result = await connector.download_article_attachment(id="123")
|
|
32
|
+
>>> file_path = await save_download(result, "./downloads/attachment.pdf")
|
|
33
|
+
>>> print(f"Downloaded to {file_path}")
|
|
34
|
+
Downloaded to /absolute/path/to/downloads/attachment.pdf
|
|
35
|
+
>>>
|
|
36
|
+
>>> # Overwrite existing file
|
|
37
|
+
>>> file_path = await save_download(result, "./downloads/attachment.pdf", overwrite=True)
|
|
38
|
+
"""
|
|
39
|
+
# Convert to Path object
|
|
40
|
+
file_path = Path(path).expanduser().resolve()
|
|
41
|
+
|
|
42
|
+
# Check if file exists
|
|
43
|
+
if file_path.exists() and not overwrite:
|
|
44
|
+
raise FileExistsError(f"File already exists: {file_path}. Use overwrite=True to replace it.")
|
|
45
|
+
|
|
46
|
+
# Create parent directories if needed
|
|
47
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
|
|
49
|
+
# Stream content to file
|
|
50
|
+
try:
|
|
51
|
+
with open(file_path, "wb") as f:
|
|
52
|
+
async for chunk in download_iterator:
|
|
53
|
+
f.write(chunk)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
# Clean up partial file on error
|
|
56
|
+
if file_path.exists():
|
|
57
|
+
file_path.unlink()
|
|
58
|
+
raise OSError(f"Failed to write file {file_path}: {e}") from e
|
|
59
|
+
|
|
60
|
+
return file_path
|