airbyte-agent-mailchimp 0.1.4__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 (57) hide show
  1. airbyte_agent_mailchimp/__init__.py +217 -0
  2. airbyte_agent_mailchimp/_vendored/__init__.py +1 -0
  3. airbyte_agent_mailchimp/_vendored/connector_sdk/__init__.py +82 -0
  4. airbyte_agent_mailchimp/_vendored/connector_sdk/auth_strategies.py +1120 -0
  5. airbyte_agent_mailchimp/_vendored/connector_sdk/auth_template.py +135 -0
  6. airbyte_agent_mailchimp/_vendored/connector_sdk/cloud_utils/__init__.py +5 -0
  7. airbyte_agent_mailchimp/_vendored/connector_sdk/cloud_utils/client.py +213 -0
  8. airbyte_agent_mailchimp/_vendored/connector_sdk/connector_model_loader.py +965 -0
  9. airbyte_agent_mailchimp/_vendored/connector_sdk/constants.py +78 -0
  10. airbyte_agent_mailchimp/_vendored/connector_sdk/exceptions.py +23 -0
  11. airbyte_agent_mailchimp/_vendored/connector_sdk/executor/__init__.py +31 -0
  12. airbyte_agent_mailchimp/_vendored/connector_sdk/executor/hosted_executor.py +196 -0
  13. airbyte_agent_mailchimp/_vendored/connector_sdk/executor/local_executor.py +1641 -0
  14. airbyte_agent_mailchimp/_vendored/connector_sdk/executor/models.py +190 -0
  15. airbyte_agent_mailchimp/_vendored/connector_sdk/extensions.py +693 -0
  16. airbyte_agent_mailchimp/_vendored/connector_sdk/http/__init__.py +37 -0
  17. airbyte_agent_mailchimp/_vendored/connector_sdk/http/adapters/__init__.py +9 -0
  18. airbyte_agent_mailchimp/_vendored/connector_sdk/http/adapters/httpx_adapter.py +251 -0
  19. airbyte_agent_mailchimp/_vendored/connector_sdk/http/config.py +98 -0
  20. airbyte_agent_mailchimp/_vendored/connector_sdk/http/exceptions.py +119 -0
  21. airbyte_agent_mailchimp/_vendored/connector_sdk/http/protocols.py +114 -0
  22. airbyte_agent_mailchimp/_vendored/connector_sdk/http/response.py +104 -0
  23. airbyte_agent_mailchimp/_vendored/connector_sdk/http_client.py +686 -0
  24. airbyte_agent_mailchimp/_vendored/connector_sdk/introspection.py +262 -0
  25. airbyte_agent_mailchimp/_vendored/connector_sdk/logging/__init__.py +11 -0
  26. airbyte_agent_mailchimp/_vendored/connector_sdk/logging/logger.py +264 -0
  27. airbyte_agent_mailchimp/_vendored/connector_sdk/logging/types.py +92 -0
  28. airbyte_agent_mailchimp/_vendored/connector_sdk/observability/__init__.py +11 -0
  29. airbyte_agent_mailchimp/_vendored/connector_sdk/observability/config.py +179 -0
  30. airbyte_agent_mailchimp/_vendored/connector_sdk/observability/models.py +19 -0
  31. airbyte_agent_mailchimp/_vendored/connector_sdk/observability/redactor.py +81 -0
  32. airbyte_agent_mailchimp/_vendored/connector_sdk/observability/session.py +103 -0
  33. airbyte_agent_mailchimp/_vendored/connector_sdk/performance/__init__.py +6 -0
  34. airbyte_agent_mailchimp/_vendored/connector_sdk/performance/instrumentation.py +57 -0
  35. airbyte_agent_mailchimp/_vendored/connector_sdk/performance/metrics.py +93 -0
  36. airbyte_agent_mailchimp/_vendored/connector_sdk/schema/__init__.py +75 -0
  37. airbyte_agent_mailchimp/_vendored/connector_sdk/schema/base.py +164 -0
  38. airbyte_agent_mailchimp/_vendored/connector_sdk/schema/components.py +239 -0
  39. airbyte_agent_mailchimp/_vendored/connector_sdk/schema/connector.py +120 -0
  40. airbyte_agent_mailchimp/_vendored/connector_sdk/schema/extensions.py +230 -0
  41. airbyte_agent_mailchimp/_vendored/connector_sdk/schema/operations.py +146 -0
  42. airbyte_agent_mailchimp/_vendored/connector_sdk/schema/security.py +223 -0
  43. airbyte_agent_mailchimp/_vendored/connector_sdk/secrets.py +182 -0
  44. airbyte_agent_mailchimp/_vendored/connector_sdk/telemetry/__init__.py +10 -0
  45. airbyte_agent_mailchimp/_vendored/connector_sdk/telemetry/config.py +32 -0
  46. airbyte_agent_mailchimp/_vendored/connector_sdk/telemetry/events.py +59 -0
  47. airbyte_agent_mailchimp/_vendored/connector_sdk/telemetry/tracker.py +155 -0
  48. airbyte_agent_mailchimp/_vendored/connector_sdk/types.py +245 -0
  49. airbyte_agent_mailchimp/_vendored/connector_sdk/utils.py +60 -0
  50. airbyte_agent_mailchimp/_vendored/connector_sdk/validation.py +822 -0
  51. airbyte_agent_mailchimp/connector.py +1378 -0
  52. airbyte_agent_mailchimp/connector_model.py +4749 -0
  53. airbyte_agent_mailchimp/models.py +956 -0
  54. airbyte_agent_mailchimp/types.py +164 -0
  55. airbyte_agent_mailchimp-0.1.4.dist-info/METADATA +119 -0
  56. airbyte_agent_mailchimp-0.1.4.dist-info/RECORD +57 -0
  57. airbyte_agent_mailchimp-0.1.4.dist-info/WHEEL +4 -0
@@ -0,0 +1,182 @@
1
+ """Secret handling utilities for the Airbyte SDK.
2
+
3
+ This module provides utilities for handling sensitive data like API keys, tokens,
4
+ and passwords. It uses Pydantic's SecretStr to ensure secrets are obfuscated in
5
+ logs, error messages, and string representations.
6
+
7
+ Example:
8
+ >>> from .secrets import SecretStr
9
+ >>> api_key = SecretStr("my-secret-key")
10
+ >>> print(api_key) # Outputs: **********
11
+ >>> print(repr(api_key)) # Outputs: SecretStr('**********')
12
+ >>> api_key.get_secret_value() # Returns: 'my-secret-key'
13
+ """
14
+
15
+ import os
16
+ import re
17
+ from typing import Any, Dict
18
+
19
+ from pydantic import SecretStr
20
+
21
+ __all__ = [
22
+ "SecretStr",
23
+ "convert_to_secret_dict",
24
+ "get_secret_values",
25
+ "resolve_env_var_references",
26
+ ]
27
+
28
+
29
+ def convert_to_secret_dict(secrets: Dict[str, str]) -> Dict[str, SecretStr]:
30
+ """Convert a dictionary of plain string secrets to SecretStr values.
31
+
32
+ Args:
33
+ secrets: Dictionary with string keys and plain string secret values
34
+
35
+ Returns:
36
+ Dictionary with string keys and SecretStr values
37
+
38
+ Example:
39
+ >>> plain_secrets = {"api_key": "secret123", "token": "token456"}
40
+ >>> secret_dict = convert_to_secret_dict(plain_secrets)
41
+ >>> print(secret_dict["api_key"]) # Outputs: **********
42
+ """
43
+ return {key: SecretStr(value) for key, value in secrets.items()}
44
+
45
+
46
+ def get_secret_values(secrets: Dict[str, SecretStr]) -> Dict[str, str]:
47
+ """Extract plain string values from a dictionary of SecretStr values.
48
+
49
+ Args:
50
+ secrets: Dictionary with string keys and SecretStr values
51
+
52
+ Returns:
53
+ Dictionary with string keys and plain string values
54
+
55
+ Warning:
56
+ Use with caution. This exposes the actual secret values.
57
+
58
+ Example:
59
+ >>> secret_dict = {"api_key": SecretStr("secret123")}
60
+ >>> plain_dict = get_secret_values(secret_dict)
61
+ >>> print(plain_dict["api_key"]) # Outputs: secret123
62
+ """
63
+ return {key: value.get_secret_value() for key, value in secrets.items()}
64
+
65
+
66
+ class SecretResolutionError(Exception):
67
+ """Raised when environment variable resolution fails."""
68
+
69
+ pass
70
+
71
+
72
+ def resolve_env_var_references(
73
+ secret_mappings: Dict[str, Any],
74
+ strict: bool = True,
75
+ env_vars: Dict[str, str] | None = None,
76
+ ) -> Dict[str, str]:
77
+ """Resolve environment variable references in secret values.
78
+
79
+ This function processes a dictionary of secret mappings and resolves any
80
+ environment variable references using the ${ENV_VAR_NAME} syntax. All
81
+ environment variable references in the values will be replaced with their
82
+ actual values from the provided environment variable map or os.environ.
83
+
84
+ Args:
85
+ secret_mappings: Dictionary mapping secret keys to values that may contain
86
+ environment variable references (e.g., {"token": "${API_KEY}"})
87
+ strict: If True, raises SecretResolutionError when a referenced environment
88
+ variable is not found. If False, leaves unresolved references as-is.
89
+ env_vars: Optional dictionary of environment variables to use for resolution.
90
+ If None, uses os.environ.
91
+
92
+ Returns:
93
+ Dictionary with the same keys but environment variable references resolved
94
+ to their actual values as plain strings
95
+
96
+ Raises:
97
+ SecretResolutionError: When strict=True and a referenced environment variable
98
+ is not found or is empty
99
+ ValueError: When an environment variable name doesn't match valid naming
100
+ conventions (must start with letter or underscore, followed by
101
+ alphanumeric characters or underscores)
102
+
103
+ Example:
104
+ >>> import os
105
+ >>> os.environ["MY_TOKEN"] = "secret_value_123"
106
+ >>> mappings = {"token": "${MY_TOKEN}", "literal": "plain_value"}
107
+ >>> resolved = resolve_env_var_references(mappings)
108
+ >>> print(resolved)
109
+ {'token': 'secret_value_123', 'literal': 'plain_value'}
110
+
111
+ >>> # Using custom env_vars dict
112
+ >>> custom_env = {"CUSTOM_TOKEN": "my_secret"}
113
+ >>> mappings = {"token": "${CUSTOM_TOKEN}"}
114
+ >>> resolved = resolve_env_var_references(mappings, env_vars=custom_env)
115
+ >>> print(resolved)
116
+ {'token': 'my_secret'}
117
+
118
+ >>> # Multiple references in one value
119
+ >>> mappings = {"combined": "${PREFIX}_${SUFFIX}"}
120
+ >>> os.environ["PREFIX"] = "api"
121
+ >>> os.environ["SUFFIX"] = "key"
122
+ >>> resolved = resolve_env_var_references(mappings)
123
+ >>> print(resolved["combined"])
124
+ 'api_key'
125
+
126
+ >>> # Missing variable with strict=True (raises error)
127
+ >>> try:
128
+ ... resolve_env_var_references({"token": "${MISSING_VAR}"})
129
+ ... except SecretResolutionError as e:
130
+ ... print(f"Error: {e}")
131
+ Error: Environment variable 'MISSING_VAR' not found or empty
132
+
133
+ >>> # Missing variable with strict=False (keeps reference)
134
+ >>> resolved = resolve_env_var_references(
135
+ ... {"token": "${MISSING_VAR}"},
136
+ ... strict=False
137
+ ... )
138
+ >>> print(resolved["token"])
139
+ ${MISSING_VAR}
140
+ """
141
+ resolved = {}
142
+
143
+ # Use provided env_vars or default to os.environ
144
+ env_source = env_vars if env_vars is not None else os.environ
145
+
146
+ # Environment variable name pattern: starts with letter or underscore,
147
+ # followed by alphanumeric or underscores
148
+ env_var_pattern = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
149
+
150
+ for key, value in secret_mappings.items():
151
+ if isinstance(value, str):
152
+
153
+ def replacer(match: re.Match) -> str:
154
+ """Replace a single ${ENV_VAR} reference with its value."""
155
+ env_var_name = match.group(1)
156
+
157
+ # Validate environment variable name format
158
+ if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", env_var_name):
159
+ raise ValueError(
160
+ f"Invalid environment variable name: '{env_var_name}'. "
161
+ "Must start with a letter or underscore, followed by "
162
+ "alphanumeric characters or underscores."
163
+ )
164
+
165
+ env_value = env_source.get(env_var_name)
166
+
167
+ if env_value is None or env_value == "":
168
+ if strict:
169
+ raise SecretResolutionError(f"Environment variable '{env_var_name}' not found or empty")
170
+ # In non-strict mode, keep the original reference
171
+ return match.group(0)
172
+
173
+ return env_value
174
+
175
+ # Replace all ${ENV_VAR} references in the value
176
+ resolved_value = env_var_pattern.sub(replacer, value)
177
+ resolved[key] = resolved_value
178
+ else:
179
+ # Non-string values are converted to strings
180
+ resolved[key] = str(value)
181
+
182
+ return resolved
@@ -0,0 +1,10 @@
1
+ """Telemetry tracking for Airbyte SDK."""
2
+
3
+ from .config import TelemetryConfig, TelemetryMode
4
+ from .tracker import SegmentTracker
5
+
6
+ __all__ = [
7
+ "TelemetryConfig",
8
+ "TelemetryMode",
9
+ "SegmentTracker",
10
+ ]
@@ -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,59 @@
1
+ """Telemetry event models."""
2
+
3
+ from dataclasses import asdict, dataclass, field
4
+ from datetime import datetime
5
+ from typing import Any, Dict
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
+ is_internal_user: bool = field(default=False, kw_only=True)
17
+
18
+ def to_dict(self) -> Dict[str, Any]:
19
+ """Convert event to dictionary with ISO formatted timestamp."""
20
+ data = asdict(self)
21
+ data["timestamp"] = self.timestamp.isoformat()
22
+ return data
23
+
24
+
25
+ @dataclass
26
+ class ConnectorInitEvent(BaseEvent):
27
+ """Connector initialization event."""
28
+
29
+ connector_name: str
30
+ python_version: str
31
+ os_name: str
32
+ os_version: str
33
+ public_ip: str | None = None
34
+ connector_version: str | None = None
35
+
36
+
37
+ @dataclass
38
+ class OperationEvent(BaseEvent):
39
+ """API operation event."""
40
+
41
+ connector_name: str
42
+ entity: str
43
+ action: str
44
+ timing_ms: float
45
+ public_ip: str | None = None
46
+ status_code: int | None = None
47
+ error_type: str | None = None
48
+
49
+
50
+ @dataclass
51
+ class SessionEndEvent(BaseEvent):
52
+ """Session end event."""
53
+
54
+ connector_name: str
55
+ duration_seconds: float
56
+ operation_count: int
57
+ success_count: int
58
+ failure_count: int
59
+ public_ip: str | None = None
@@ -0,0 +1,155 @@
1
+ """Anonymous telemetry tracker using Segment."""
2
+
3
+ import logging
4
+ import platform
5
+ import sys
6
+ from datetime import datetime
7
+
8
+ from ..observability import ObservabilitySession
9
+
10
+ from .config import SEGMENT_WRITE_KEY, TelemetryConfig, TelemetryMode
11
+ from .events import ConnectorInitEvent, OperationEvent, SessionEndEvent
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class SegmentTracker:
17
+ """Anonymous telemetry tracker using Segment."""
18
+
19
+ def __init__(
20
+ self,
21
+ session: ObservabilitySession,
22
+ mode: TelemetryMode | None = None,
23
+ ):
24
+ self.session = session
25
+ self.mode = mode or TelemetryConfig.get_mode()
26
+ self.success_count = 0
27
+ self.failure_count = 0
28
+ self.enabled = TelemetryConfig.is_enabled()
29
+ self._analytics = None
30
+
31
+ if self.enabled:
32
+ try:
33
+ # NOTE: Import here intentionally - segment is an optional dependency.
34
+ # This allows the SDK to work without telemetry if segment is not installed.
35
+ import segment.analytics as analytics
36
+
37
+ analytics.write_key = SEGMENT_WRITE_KEY
38
+ self._analytics = analytics
39
+ self._log_startup_message()
40
+ except ImportError:
41
+ logger.warning("Telemetry disabled: segment-analytics-python not installed")
42
+ self.enabled = False
43
+
44
+ def _log_startup_message(self):
45
+ """Log message when telemetry is enabled."""
46
+ logger.info(f"Anonymous telemetry enabled (mode: {self.mode.value})")
47
+ logger.info("To opt-out: export AIRBYTE_TELEMETRY_MODE=disabled")
48
+
49
+ def track_connector_init(
50
+ self,
51
+ connector_version: str | None = None,
52
+ ) -> None:
53
+ """Track connector initialization."""
54
+ if not self.enabled or not self._analytics:
55
+ return
56
+
57
+ try:
58
+ event = ConnectorInitEvent(
59
+ timestamp=datetime.utcnow(),
60
+ session_id=self.session.session_id,
61
+ user_id=self.session.user_id,
62
+ execution_context=self.session.execution_context,
63
+ is_internal_user=self.session.is_internal_user,
64
+ public_ip=self.session.public_ip,
65
+ connector_name=self.session.connector_name,
66
+ connector_version=connector_version,
67
+ python_version=f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
68
+ os_name=platform.system(),
69
+ os_version=platform.release(),
70
+ )
71
+
72
+ self._analytics.track(
73
+ user_id=self.session.user_id,
74
+ anonymous_id=event.session_id,
75
+ event="Connector Initialized",
76
+ properties=event.to_dict(),
77
+ )
78
+ except Exception as e:
79
+ # Never fail on tracking errors
80
+ logger.error(f"Telemetry error: {e}")
81
+
82
+ def track_operation(
83
+ self,
84
+ entity: str,
85
+ action: str,
86
+ status_code: int | None,
87
+ timing_ms: float,
88
+ error_type: str | None = None,
89
+ ) -> None:
90
+ """Track API operation."""
91
+ # Always track success/failure counts (useful even when tracking is disabled)
92
+ if status_code and 200 <= status_code < 300:
93
+ self.success_count += 1
94
+ else:
95
+ self.failure_count += 1
96
+
97
+ if not self.enabled or not self._analytics:
98
+ return
99
+
100
+ try:
101
+ event = OperationEvent(
102
+ timestamp=datetime.utcnow(),
103
+ session_id=self.session.session_id,
104
+ user_id=self.session.user_id,
105
+ execution_context=self.session.execution_context,
106
+ is_internal_user=self.session.is_internal_user,
107
+ public_ip=self.session.public_ip,
108
+ connector_name=self.session.connector_name,
109
+ entity=entity,
110
+ action=action,
111
+ status_code=status_code,
112
+ timing_ms=timing_ms,
113
+ error_type=error_type,
114
+ )
115
+
116
+ self._analytics.track(
117
+ user_id=self.session.user_id,
118
+ anonymous_id=event.session_id,
119
+ event="Operation Executed",
120
+ properties=event.to_dict(),
121
+ )
122
+ except Exception as e:
123
+ logger.error(f"Telemetry error: {e}")
124
+
125
+ def track_session_end(self) -> None:
126
+ """Track session end."""
127
+ if not self.enabled or not self._analytics:
128
+ return
129
+
130
+ try:
131
+ event = SessionEndEvent(
132
+ timestamp=datetime.utcnow(),
133
+ session_id=self.session.session_id,
134
+ user_id=self.session.user_id,
135
+ execution_context=self.session.execution_context,
136
+ is_internal_user=self.session.is_internal_user,
137
+ public_ip=self.session.public_ip,
138
+ connector_name=self.session.connector_name,
139
+ duration_seconds=self.session.duration_seconds(),
140
+ operation_count=self.session.operation_count,
141
+ success_count=self.success_count,
142
+ failure_count=self.failure_count,
143
+ )
144
+
145
+ self._analytics.track(
146
+ user_id=self.session.user_id,
147
+ anonymous_id=event.session_id,
148
+ event="Session Ended",
149
+ properties=event.to_dict(),
150
+ )
151
+
152
+ # Ensure events are sent before shutdown
153
+ self._analytics.flush()
154
+ except Exception as e:
155
+ logger.error(f"Telemetry error: {e}")
@@ -0,0 +1,245 @@
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
+ API_SEARCH = "api_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
+ stream_name: str | None = Field(
225
+ default=None,
226
+ description="Airbyte stream name for cache lookup (from x-airbyte-stream-name schema extension)",
227
+ )
228
+ actions: list[Action]
229
+ endpoints: dict[Action, EndpointDefinition]
230
+ entity_schema: dict[str, Any] | None = Field(default=None, alias="schema")
231
+
232
+
233
+ class ConnectorModel(BaseModel):
234
+ """Complete connector model loaded from YAML definition."""
235
+
236
+ model_config = ConfigDict(use_enum_values=True)
237
+
238
+ id: UUID
239
+ name: str
240
+ version: str = OPENAPI_DEFAULT_VERSION
241
+ base_url: str
242
+ auth: AuthConfig
243
+ entities: list[EntityDefinition]
244
+ openapi_spec: Any | None = None # Optional reference to OpenAPIConnector
245
+ retry_config: RetryConfig | None = None # Optional retry configuration