airbyte-agent-klaviyo 0.1.0__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_klaviyo/__init__.py +225 -0
  2. airbyte_agent_klaviyo/_vendored/__init__.py +1 -0
  3. airbyte_agent_klaviyo/_vendored/connector_sdk/__init__.py +82 -0
  4. airbyte_agent_klaviyo/_vendored/connector_sdk/auth_strategies.py +1171 -0
  5. airbyte_agent_klaviyo/_vendored/connector_sdk/auth_template.py +135 -0
  6. airbyte_agent_klaviyo/_vendored/connector_sdk/cloud_utils/__init__.py +5 -0
  7. airbyte_agent_klaviyo/_vendored/connector_sdk/cloud_utils/client.py +213 -0
  8. airbyte_agent_klaviyo/_vendored/connector_sdk/connector_model_loader.py +1120 -0
  9. airbyte_agent_klaviyo/_vendored/connector_sdk/constants.py +78 -0
  10. airbyte_agent_klaviyo/_vendored/connector_sdk/exceptions.py +23 -0
  11. airbyte_agent_klaviyo/_vendored/connector_sdk/executor/__init__.py +31 -0
  12. airbyte_agent_klaviyo/_vendored/connector_sdk/executor/hosted_executor.py +201 -0
  13. airbyte_agent_klaviyo/_vendored/connector_sdk/executor/local_executor.py +1854 -0
  14. airbyte_agent_klaviyo/_vendored/connector_sdk/executor/models.py +202 -0
  15. airbyte_agent_klaviyo/_vendored/connector_sdk/extensions.py +693 -0
  16. airbyte_agent_klaviyo/_vendored/connector_sdk/http/__init__.py +37 -0
  17. airbyte_agent_klaviyo/_vendored/connector_sdk/http/adapters/__init__.py +9 -0
  18. airbyte_agent_klaviyo/_vendored/connector_sdk/http/adapters/httpx_adapter.py +251 -0
  19. airbyte_agent_klaviyo/_vendored/connector_sdk/http/config.py +98 -0
  20. airbyte_agent_klaviyo/_vendored/connector_sdk/http/exceptions.py +119 -0
  21. airbyte_agent_klaviyo/_vendored/connector_sdk/http/protocols.py +114 -0
  22. airbyte_agent_klaviyo/_vendored/connector_sdk/http/response.py +104 -0
  23. airbyte_agent_klaviyo/_vendored/connector_sdk/http_client.py +693 -0
  24. airbyte_agent_klaviyo/_vendored/connector_sdk/introspection.py +481 -0
  25. airbyte_agent_klaviyo/_vendored/connector_sdk/logging/__init__.py +11 -0
  26. airbyte_agent_klaviyo/_vendored/connector_sdk/logging/logger.py +273 -0
  27. airbyte_agent_klaviyo/_vendored/connector_sdk/logging/types.py +93 -0
  28. airbyte_agent_klaviyo/_vendored/connector_sdk/observability/__init__.py +11 -0
  29. airbyte_agent_klaviyo/_vendored/connector_sdk/observability/config.py +179 -0
  30. airbyte_agent_klaviyo/_vendored/connector_sdk/observability/models.py +19 -0
  31. airbyte_agent_klaviyo/_vendored/connector_sdk/observability/redactor.py +81 -0
  32. airbyte_agent_klaviyo/_vendored/connector_sdk/observability/session.py +103 -0
  33. airbyte_agent_klaviyo/_vendored/connector_sdk/performance/__init__.py +6 -0
  34. airbyte_agent_klaviyo/_vendored/connector_sdk/performance/instrumentation.py +57 -0
  35. airbyte_agent_klaviyo/_vendored/connector_sdk/performance/metrics.py +93 -0
  36. airbyte_agent_klaviyo/_vendored/connector_sdk/schema/__init__.py +75 -0
  37. airbyte_agent_klaviyo/_vendored/connector_sdk/schema/base.py +201 -0
  38. airbyte_agent_klaviyo/_vendored/connector_sdk/schema/components.py +244 -0
  39. airbyte_agent_klaviyo/_vendored/connector_sdk/schema/connector.py +120 -0
  40. airbyte_agent_klaviyo/_vendored/connector_sdk/schema/extensions.py +301 -0
  41. airbyte_agent_klaviyo/_vendored/connector_sdk/schema/operations.py +156 -0
  42. airbyte_agent_klaviyo/_vendored/connector_sdk/schema/security.py +236 -0
  43. airbyte_agent_klaviyo/_vendored/connector_sdk/secrets.py +182 -0
  44. airbyte_agent_klaviyo/_vendored/connector_sdk/telemetry/__init__.py +10 -0
  45. airbyte_agent_klaviyo/_vendored/connector_sdk/telemetry/config.py +32 -0
  46. airbyte_agent_klaviyo/_vendored/connector_sdk/telemetry/events.py +59 -0
  47. airbyte_agent_klaviyo/_vendored/connector_sdk/telemetry/tracker.py +155 -0
  48. airbyte_agent_klaviyo/_vendored/connector_sdk/types.py +270 -0
  49. airbyte_agent_klaviyo/_vendored/connector_sdk/utils.py +60 -0
  50. airbyte_agent_klaviyo/_vendored/connector_sdk/validation.py +848 -0
  51. airbyte_agent_klaviyo/connector.py +1431 -0
  52. airbyte_agent_klaviyo/connector_model.py +2230 -0
  53. airbyte_agent_klaviyo/models.py +676 -0
  54. airbyte_agent_klaviyo/types.py +1319 -0
  55. airbyte_agent_klaviyo-0.1.0.dist-info/METADATA +151 -0
  56. airbyte_agent_klaviyo-0.1.0.dist-info/RECORD +57 -0
  57. airbyte_agent_klaviyo-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,156 @@
1
+ """
2
+ Operation and PathItem models for OpenAPI 3.1.
3
+
4
+ References:
5
+ - https://spec.openapis.org/oas/v3.1.0#operation-object
6
+ - https://spec.openapis.org/oas/v3.1.0#path-item-object
7
+ """
8
+
9
+ from typing import Any, Dict, List
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
12
+
13
+ from ..extensions import ActionTypeLiteral
14
+ from .components import Parameter, PathOverrideConfig, RequestBody, Response
15
+ from .security import SecurityRequirement
16
+
17
+
18
+ class Operation(BaseModel):
19
+ """
20
+ Single API operation (GET, POST, PUT, PATCH, DELETE, etc.).
21
+
22
+ OpenAPI Reference: https://spec.openapis.org/oas/v3.1.0#operation-object
23
+
24
+ Extensions:
25
+ - x-airbyte-entity: Entity name (Airbyte extension)
26
+ - x-airbyte-action: Semantic action (Airbyte extension)
27
+ - x-airbyte-path-override: Path override (Airbyte extension)
28
+ - x-airbyte-record-extractor: JSONPath to extract records from response (Airbyte extension)
29
+
30
+ Future extensions (not yet active):
31
+ - x-airbyte-pagination: Pagination configuration for list operations
32
+ """
33
+
34
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
35
+
36
+ # Standard OpenAPI fields
37
+ tags: List[str] | None = None
38
+ summary: str | None = None
39
+ description: str | None = None
40
+ external_docs: Dict[str, Any] | None = Field(None, alias="externalDocs")
41
+ operation_id: str | None = Field(None, alias="operationId")
42
+ parameters: List[Parameter] | None = None
43
+ request_body: RequestBody | None = Field(None, alias="requestBody")
44
+ responses: Dict[str, Response] = Field(default_factory=dict)
45
+ callbacks: Dict[str, Any] | None = None
46
+ deprecated: bool | None = None
47
+ security: List[SecurityRequirement] | None = None
48
+ servers: List[Any] | None = None # Can override root servers
49
+
50
+ # Airbyte extensions
51
+ x_airbyte_entity: str = Field(..., alias="x-airbyte-entity")
52
+ x_airbyte_action: ActionTypeLiteral = Field(..., alias="x-airbyte-action")
53
+ x_airbyte_path_override: PathOverrideConfig | None = Field(
54
+ None,
55
+ alias="x-airbyte-path-override",
56
+ description=("Override path for HTTP requests when OpenAPI path differs from actual endpoint"),
57
+ )
58
+ x_airbyte_record_extractor: str | None = Field(
59
+ None,
60
+ alias="x-airbyte-record-extractor",
61
+ description=(
62
+ "JSONPath expression to extract records from API response envelopes. "
63
+ "When specified, executor extracts data at this path instead of returning "
64
+ "full response. Returns array for list/api_search actions, single record for "
65
+ "get/create/update/delete actions."
66
+ ),
67
+ )
68
+ x_airbyte_meta_extractor: Dict[str, str] | None = Field(
69
+ None,
70
+ alias="x-airbyte-meta-extractor",
71
+ description=(
72
+ "Dictionary mapping field names to JSONPath expressions for extracting "
73
+ "metadata (pagination info, request IDs, etc.) from API response envelopes. "
74
+ "Each key becomes a field in ExecutionResult.meta with the value extracted "
75
+ "using the corresponding JSONPath expression. "
76
+ "Example: {'pagination': '$.pagination', 'request_id': '$.requestId'}"
77
+ ),
78
+ )
79
+ x_airbyte_file_url: str | None = Field(None, alias="x-airbyte-file-url")
80
+ x_airbyte_untested: bool | None = Field(
81
+ None,
82
+ alias="x-airbyte-untested",
83
+ description=(
84
+ "Mark operation as untested to skip cassette validation in readiness checks. "
85
+ "Use this for operations that cannot be recorded (e.g., webhooks, real-time streams). "
86
+ "Validation will generate a warning instead of an error when cassettes are missing."
87
+ ),
88
+ )
89
+ x_airbyte_preferred_for_check: bool | None = Field(
90
+ None,
91
+ alias="x-airbyte-preferred-for-check",
92
+ description=(
93
+ "Mark this list operation as the preferred operation for health checks. "
94
+ "When the CHECK action is executed, this operation will be used instead of "
95
+ "falling back to the first available list operation. Choose a lightweight, "
96
+ "always-available endpoint (e.g., users, accounts)."
97
+ ),
98
+ )
99
+
100
+ # Future extensions (commented out, defined for future use)
101
+ # from .extensions import PaginationConfig
102
+ # x_pagination: Optional[PaginationConfig] = Field(None, alias="x-airbyte-pagination")
103
+
104
+ @model_validator(mode="after")
105
+ def validate_download_action_requirements(self) -> "Operation":
106
+ """
107
+ Validate download operation requirements.
108
+
109
+ Rules:
110
+ - If x-airbyte-action is "download":
111
+ - x-airbyte-file-url must be non-empty if provided
112
+ - If x-airbyte-action is not "download":
113
+ - x-airbyte-file-url must not be present
114
+ """
115
+ action = self.x_airbyte_action
116
+ file_url = self.x_airbyte_file_url
117
+
118
+ if action == "download":
119
+ # If file_url is provided, it must be non-empty
120
+ if file_url is not None and not file_url.strip():
121
+ raise ValueError("x-airbyte-file-url must be non-empty when provided for download operations")
122
+ else:
123
+ # Non-download actions cannot have file_url
124
+ if file_url is not None:
125
+ raise ValueError(f"x-airbyte-file-url can only be used with x-airbyte-action: download, but action is '{action}'")
126
+
127
+ return self
128
+
129
+
130
+ class PathItem(BaseModel):
131
+ """
132
+ Path item containing operations for different HTTP methods.
133
+
134
+ OpenAPI Reference: https://spec.openapis.org/oas/v3.1.0#path-item-object
135
+ """
136
+
137
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
138
+
139
+ # Common fields for all operations
140
+ summary: str | None = None
141
+ description: str | None = None
142
+ servers: List[Any] | None = None
143
+ parameters: List[Parameter] | None = None
144
+
145
+ # HTTP methods (all optional)
146
+ get: Operation | None = None
147
+ put: Operation | None = None
148
+ post: Operation | None = None
149
+ delete: Operation | None = None
150
+ options: Operation | None = None
151
+ head: Operation | None = None
152
+ patch: Operation | None = None
153
+ trace: Operation | None = None
154
+
155
+ # Reference support
156
+ ref: str | None = Field(None, alias="$ref")
@@ -0,0 +1,236 @@
1
+ """
2
+ Security scheme models for OpenAPI 3.1.
3
+
4
+ References:
5
+ - https://spec.openapis.org/oas/v3.1.0#security-scheme-object
6
+ - https://spec.openapis.org/oas/v3.1.0#oauth-flows-object
7
+ """
8
+
9
+ from typing import Any, Dict, List, Literal
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
12
+
13
+
14
+ class OAuth2Flow(BaseModel):
15
+ """
16
+ OAuth 2.0 flow configuration.
17
+
18
+ OpenAPI Reference: https://spec.openapis.org/oas/v3.1.0#oauth-flow-object
19
+ """
20
+
21
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
22
+
23
+ authorization_url: str | None = Field(None, alias="authorizationUrl")
24
+ token_url: str | None = Field(None, alias="tokenUrl")
25
+ refresh_url: str | None = Field(None, alias="refreshUrl")
26
+ scopes: Dict[str, str] = Field(default_factory=dict)
27
+
28
+
29
+ class OAuth2Flows(BaseModel):
30
+ """
31
+ Collection of OAuth 2.0 flows.
32
+
33
+ OpenAPI Reference: https://spec.openapis.org/oas/v3.1.0#oauth-flows-object
34
+ """
35
+
36
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
37
+
38
+ implicit: OAuth2Flow | None = None
39
+ password: OAuth2Flow | None = None
40
+ client_credentials: OAuth2Flow | None = Field(None, alias="clientCredentials")
41
+ authorization_code: OAuth2Flow | None = Field(None, alias="authorizationCode")
42
+
43
+
44
+ class AuthConfigFieldSpec(BaseModel):
45
+ """
46
+ Specification for a user-facing authentication config field.
47
+
48
+ This defines a single input field that users provide for authentication.
49
+ """
50
+
51
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
52
+
53
+ type: Literal["string", "integer", "boolean", "number"] = "string"
54
+ title: str | None = None
55
+ description: str | None = None
56
+ format: str | None = None # e.g., "email", "uri"
57
+ pattern: str | None = None # Regex validation
58
+ default: Any | None = None
59
+
60
+
61
+ class AuthConfigOption(BaseModel):
62
+ """
63
+ A single authentication configuration option.
64
+
65
+ Defines user-facing fields and how they map to auth parameters.
66
+ """
67
+
68
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
69
+
70
+ title: str | None = None
71
+ description: str | None = None
72
+ type: Literal["object"] = "object"
73
+ required: List[str] = Field(default_factory=list)
74
+ properties: Dict[str, AuthConfigFieldSpec] = Field(default_factory=dict)
75
+ auth_mapping: Dict[str, str] = Field(
76
+ default_factory=dict,
77
+ description="Mapping from auth parameters (e.g., 'username', 'password', 'token') to template strings using ${field} syntax",
78
+ )
79
+ replication_auth_key_mapping: Dict[str, str] | None = Field(
80
+ None,
81
+ description="Mapping from source config paths (e.g., 'credentials.api_key') to auth config keys for direct connectors",
82
+ )
83
+
84
+
85
+ class AirbyteAuthConfig(BaseModel):
86
+ """
87
+ Airbyte auth configuration extension (x-airbyte-auth-config).
88
+
89
+ Defines user-facing authentication configuration and how it maps to
90
+ the underlying OpenAPI security scheme.
91
+
92
+ Either a single auth option or multiple options via oneOf.
93
+ """
94
+
95
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
96
+
97
+ # Single option fields
98
+ title: str | None = None
99
+ description: str | None = None
100
+ type: Literal["object"] | None = None
101
+ required: List[str] | None = None
102
+ properties: Dict[str, AuthConfigFieldSpec] | None = None
103
+ auth_mapping: Dict[str, str] | None = None
104
+
105
+ # Replication connector auth mapping
106
+ replication_auth_key_mapping: Dict[str, str] | None = Field(
107
+ None,
108
+ description="Mapping from source config paths (e.g., 'credentials.api_key') to auth config keys for direct connectors",
109
+ )
110
+
111
+ # Additional headers to inject alongside OAuth2 Bearer token
112
+ additional_headers: Dict[str, str] | None = Field(
113
+ None,
114
+ description=(
115
+ "Extra headers to inject with auth. Values support Jinja2 {{ variable }} template syntax "
116
+ "to reference secrets. Example: {'Amazon-Advertising-API-ClientId': '{{ client_id }}'}"
117
+ ),
118
+ )
119
+
120
+ # Replication connector auth constants
121
+ replication_auth_key_constants: Dict[str, Any] | None = Field(
122
+ None,
123
+ description="Constant values to always inject at source config paths (e.g., 'credentials.auth_type': 'OAuth2.0')",
124
+ )
125
+ # Multiple options (oneOf)
126
+ one_of: List[AuthConfigOption] | None = Field(None, alias="oneOf")
127
+
128
+ @model_validator(mode="after")
129
+ def validate_config_structure(self) -> "AirbyteAuthConfig":
130
+ """Validate that either single option or oneOf is provided, not both."""
131
+ has_single = self.type is not None or self.properties is not None or self.auth_mapping is not None
132
+ has_one_of = self.one_of is not None and len(self.one_of) > 0
133
+
134
+ if not has_single and not has_one_of:
135
+ raise ValueError("Either single auth option (type/properties/auth_mapping) or oneOf must be provided")
136
+
137
+ if has_single and has_one_of:
138
+ raise ValueError("Cannot have both single auth option and oneOf")
139
+
140
+ if has_single:
141
+ # Validate single option has required fields
142
+ if self.type != "object":
143
+ raise ValueError("Single auth option must have type='object'")
144
+ if not self.properties:
145
+ raise ValueError("Single auth option must have properties")
146
+ if not self.auth_mapping:
147
+ raise ValueError("Single auth option must have auth_mapping")
148
+
149
+ return self
150
+
151
+
152
+ class SecurityScheme(BaseModel):
153
+ """
154
+ Security scheme definition.
155
+
156
+ OpenAPI Reference: https://spec.openapis.org/oas/v3.1.0#security-scheme-object
157
+
158
+ Supported Types:
159
+ - apiKey: API key in header/query/cookie
160
+ - http: HTTP authentication (basic, bearer, digest, etc.)
161
+ - oauth2: OAuth 2.0 flows
162
+
163
+ Extensions:
164
+ - x-airbyte-token-path: JSON path to extract token from auth response (Airbyte extension)
165
+ - x-airbyte-token-refresh: OAuth2 token refresh configuration (dict with auth_style, body_format)
166
+ - x-airbyte-auth-config: User-facing authentication configuration (Airbyte extension)
167
+
168
+ Future extensions (not yet active):
169
+ - x-grant-type: OAuth grant type for refresh tokens
170
+ - x-refresh-endpoint: Custom refresh endpoint URL
171
+ """
172
+
173
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
174
+
175
+ # Standard OpenAPI fields
176
+ type: Literal["apiKey", "http", "oauth2", "openIdConnect"]
177
+ description: str | None = None
178
+
179
+ # apiKey specific
180
+ name: str | None = None
181
+ in_: Literal["query", "header", "cookie"] | None = Field(None, alias="in")
182
+
183
+ # http specific
184
+ scheme: str | None = None # e.g., "basic", "bearer", "digest"
185
+ bearer_format: str | None = Field(None, alias="bearerFormat")
186
+
187
+ # oauth2 specific
188
+ flows: OAuth2Flows | None = None
189
+
190
+ # openIdConnect specific
191
+ open_id_connect_url: str | None = Field(None, alias="openIdConnectUrl")
192
+
193
+ # Airbyte extensions
194
+ x_token_path: str | None = Field(None, alias="x-airbyte-token-path")
195
+ x_token_refresh: Dict[str, Any] | None = Field(None, alias="x-airbyte-token-refresh")
196
+ x_airbyte_auth_config: AirbyteAuthConfig | None = Field(None, alias="x-airbyte-auth-config")
197
+ x_airbyte_token_extract: List[str] | None = Field(
198
+ None,
199
+ alias="x-airbyte-token-extract",
200
+ description="List of fields to extract from OAuth2 token responses and use as server variables",
201
+ )
202
+
203
+ @field_validator("x_airbyte_token_extract", mode="after")
204
+ @classmethod
205
+ def validate_token_extract(cls, v: List[str] | None) -> List[str] | None:
206
+ """Validate x-airbyte-token-extract has no duplicates."""
207
+ if v is not None:
208
+ if len(v) != len(set(v)):
209
+ duplicates = [x for x in v if v.count(x) > 1]
210
+ raise ValueError(f"x-airbyte-token-extract contains duplicate fields: {set(duplicates)}")
211
+ return v
212
+
213
+ # Future extensions (commented out, defined for future use)
214
+ # x_grant_type: Optional[Literal["refresh_token", "client_credentials"]] = Field(None, alias="x-grant-type")
215
+ # x_refresh_endpoint: Optional[str] = Field(None, alias="x-refresh-endpoint")
216
+
217
+ @model_validator(mode="after")
218
+ def validate_security_scheme(self) -> "SecurityScheme":
219
+ """Validate that required fields are present based on security type."""
220
+ if self.type == "apiKey":
221
+ if not self.name or not self.in_:
222
+ raise ValueError("apiKey type requires 'name' and 'in' fields")
223
+ elif self.type == "http":
224
+ if not self.scheme:
225
+ raise ValueError("http type requires 'scheme' field")
226
+ elif self.type == "oauth2":
227
+ if not self.flows:
228
+ raise ValueError("oauth2 type requires 'flows' field")
229
+ elif self.type == "openIdConnect":
230
+ if not self.open_id_connect_url:
231
+ raise ValueError("openIdConnect type requires 'openIdConnectUrl' field")
232
+ return self
233
+
234
+
235
+ # SecurityRequirement is a dict mapping security scheme name to list of scopes
236
+ SecurityRequirement = Dict[str, List[str]]
@@ -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