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.
- airbyte_agent_mailchimp/__init__.py +217 -0
- airbyte_agent_mailchimp/_vendored/__init__.py +1 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/__init__.py +82 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/auth_strategies.py +1120 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/auth_template.py +135 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/cloud_utils/__init__.py +5 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/cloud_utils/client.py +213 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/connector_model_loader.py +965 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/constants.py +78 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/exceptions.py +23 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/executor/__init__.py +31 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/executor/hosted_executor.py +196 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/executor/local_executor.py +1641 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/executor/models.py +190 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/extensions.py +693 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/http/__init__.py +37 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/http/adapters/__init__.py +9 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/http/adapters/httpx_adapter.py +251 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/http/config.py +98 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/http/exceptions.py +119 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/http/protocols.py +114 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/http/response.py +104 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/http_client.py +686 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/introspection.py +262 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/logging/__init__.py +11 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/logging/logger.py +264 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/logging/types.py +92 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/observability/__init__.py +11 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/observability/config.py +179 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/observability/models.py +19 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/observability/redactor.py +81 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/observability/session.py +103 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/performance/__init__.py +6 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/performance/instrumentation.py +57 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/performance/metrics.py +93 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/schema/__init__.py +75 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/schema/base.py +164 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/schema/components.py +239 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/schema/connector.py +120 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/schema/extensions.py +230 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/schema/operations.py +146 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/schema/security.py +223 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/secrets.py +182 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/telemetry/__init__.py +10 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/telemetry/config.py +32 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/telemetry/events.py +59 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/telemetry/tracker.py +155 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/types.py +245 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/utils.py +60 -0
- airbyte_agent_mailchimp/_vendored/connector_sdk/validation.py +822 -0
- airbyte_agent_mailchimp/connector.py +1378 -0
- airbyte_agent_mailchimp/connector_model.py +4749 -0
- airbyte_agent_mailchimp/models.py +956 -0
- airbyte_agent_mailchimp/types.py +164 -0
- airbyte_agent_mailchimp-0.1.4.dist-info/METADATA +119 -0
- airbyte_agent_mailchimp-0.1.4.dist-info/RECORD +57 -0
- airbyte_agent_mailchimp-0.1.4.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Template engine for auth_mapping in x-airbyte-auth-config.
|
|
3
|
+
|
|
4
|
+
Handles template substitution for mapping user-provided config values to auth parameters.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Dict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MissingVariableError(ValueError):
|
|
12
|
+
"""Raised when a template variable is not found in config.
|
|
13
|
+
|
|
14
|
+
Extends ValueError for backwards compatibility with code that catches ValueError.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, var_name: str, available_fields: list):
|
|
18
|
+
self.var_name = var_name
|
|
19
|
+
self.available_fields = available_fields
|
|
20
|
+
super().__init__(f"Template variable '${{{var_name}}}' not found in config. Available fields: {available_fields}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def apply_template(template: str, values: Dict[str, str]) -> str:
|
|
24
|
+
"""
|
|
25
|
+
Apply template substitution for auth_mapping.
|
|
26
|
+
|
|
27
|
+
Template syntax:
|
|
28
|
+
- ${variable}: Replaced with value from the values dict
|
|
29
|
+
- Any other text: Used as-is (constants or concatenation)
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
>>> apply_template("${api_key}", {"api_key": "abc123"})
|
|
33
|
+
'abc123'
|
|
34
|
+
|
|
35
|
+
>>> apply_template("${email}/token", {"email": "user@example.com"})
|
|
36
|
+
'user@example.com/token'
|
|
37
|
+
|
|
38
|
+
>>> apply_template("api_token", {})
|
|
39
|
+
'api_token'
|
|
40
|
+
|
|
41
|
+
>>> apply_template("", {})
|
|
42
|
+
''
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
template: Template string with ${variable} placeholders
|
|
46
|
+
values: Dict of variable names to values
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Resolved template string
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
MissingVariableError: If template contains unresolved variables
|
|
53
|
+
"""
|
|
54
|
+
if not template:
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
# Check if it's a pure constant (no variables)
|
|
58
|
+
if "${" not in template:
|
|
59
|
+
return template
|
|
60
|
+
|
|
61
|
+
# Find all variable references
|
|
62
|
+
variable_pattern = re.compile(r"\$\{([^}]+)\}")
|
|
63
|
+
matches = variable_pattern.findall(template)
|
|
64
|
+
|
|
65
|
+
# Substitute all ${var} with values
|
|
66
|
+
result = template
|
|
67
|
+
for var_name in matches:
|
|
68
|
+
if var_name not in values:
|
|
69
|
+
raise MissingVariableError(var_name, list(values.keys()))
|
|
70
|
+
# Replace the variable with its value
|
|
71
|
+
result = result.replace(f"${{{var_name}}}", values[var_name])
|
|
72
|
+
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def apply_auth_mapping(
|
|
77
|
+
auth_mapping: Dict[str, str],
|
|
78
|
+
user_config: Dict[str, str],
|
|
79
|
+
required_fields: list | None = None,
|
|
80
|
+
) -> Dict[str, str]:
|
|
81
|
+
"""
|
|
82
|
+
Apply auth_mapping templates to user config.
|
|
83
|
+
|
|
84
|
+
Takes the auth_mapping from x-airbyte-auth-config and user-provided config,
|
|
85
|
+
and returns the mapped auth parameters. Optional fields (not in required_fields)
|
|
86
|
+
are skipped if their template variables are not provided.
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
>>> auth_mapping = {
|
|
90
|
+
... "username": "${api_key}",
|
|
91
|
+
... "password": ""
|
|
92
|
+
... }
|
|
93
|
+
>>> user_config = {"api_key": "my_key_123"}
|
|
94
|
+
>>> apply_auth_mapping(auth_mapping, user_config)
|
|
95
|
+
{'username': 'my_key_123', 'password': ''}
|
|
96
|
+
|
|
97
|
+
>>> # Optional fields are skipped if not provided
|
|
98
|
+
>>> auth_mapping = {
|
|
99
|
+
... "access_token": "${access_token}",
|
|
100
|
+
... "refresh_token": "${refresh_token}",
|
|
101
|
+
... }
|
|
102
|
+
>>> user_config = {"access_token": "abc123"}
|
|
103
|
+
>>> apply_auth_mapping(auth_mapping, user_config, required_fields=["access_token"])
|
|
104
|
+
{'access_token': 'abc123'}
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
auth_mapping: Dict mapping auth parameters to template strings
|
|
108
|
+
user_config: Dict of user-provided field values
|
|
109
|
+
required_fields: List of required field names. If a template references
|
|
110
|
+
a variable not in user_config and that variable is not required,
|
|
111
|
+
the mapping is skipped. Behavior:
|
|
112
|
+
- None: all fields are treated as required (backward compatible)
|
|
113
|
+
- []: no fields are required (all optional)
|
|
114
|
+
- ["foo"]: only "foo" is required
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Dict of resolved auth parameters
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
MissingVariableError: If a required template variable is not found
|
|
121
|
+
"""
|
|
122
|
+
resolved = {}
|
|
123
|
+
required_set = set(required_fields) if required_fields is not None else None
|
|
124
|
+
|
|
125
|
+
for param, template in auth_mapping.items():
|
|
126
|
+
try:
|
|
127
|
+
resolved[param] = apply_template(template, user_config)
|
|
128
|
+
except MissingVariableError as e:
|
|
129
|
+
# If the missing variable is not in required fields, skip this mapping
|
|
130
|
+
if required_set is not None and e.var_name not in required_set:
|
|
131
|
+
continue
|
|
132
|
+
# Otherwise, re-raise the error
|
|
133
|
+
raise
|
|
134
|
+
|
|
135
|
+
return resolved
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""AirbyteCloudClient for Airbyte Platform API integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AirbyteCloudClient:
|
|
12
|
+
"""Client for interacting with Airbyte Platform APIs.
|
|
13
|
+
|
|
14
|
+
Handles authentication, token caching, and API calls to:
|
|
15
|
+
- Get bearer tokens for authentication
|
|
16
|
+
- Look up connectors for users
|
|
17
|
+
- Execute connectors via the cloud API
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
client = AirbyteCloudClient(
|
|
21
|
+
client_id="your-client-id",
|
|
22
|
+
client_secret="your-client-secret"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Get a connector ID
|
|
26
|
+
connector_id = await client.get_connector_id(
|
|
27
|
+
external_user_id="user-123",
|
|
28
|
+
connector_definition_id="550e8400-e29b-41d4-a716-446655440000"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Execute the connector
|
|
32
|
+
result = await client.execute_connector(
|
|
33
|
+
connector_id=connector_id,
|
|
34
|
+
entity="customers",
|
|
35
|
+
action="list",
|
|
36
|
+
params={"limit": 10}
|
|
37
|
+
)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
AUTH_BASE_URL = "https://cloud.airbyte.com" # For token endpoint
|
|
41
|
+
API_BASE_URL = "https://api.airbyte.ai" # For instance lookup & execution
|
|
42
|
+
|
|
43
|
+
def __init__(self, client_id: str, client_secret: str):
|
|
44
|
+
"""Initialize AirbyteCloudClient.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
client_id: Airbyte client ID for authentication
|
|
48
|
+
client_secret: Airbyte client secret for authentication
|
|
49
|
+
"""
|
|
50
|
+
self._client_id = client_id
|
|
51
|
+
self._client_secret = client_secret
|
|
52
|
+
|
|
53
|
+
# Token cache (instance-level)
|
|
54
|
+
self._cached_token: str | None = None
|
|
55
|
+
self._token_expires_at: datetime | None = None
|
|
56
|
+
self._http_client = httpx.AsyncClient(
|
|
57
|
+
timeout=httpx.Timeout(300.0), # 5 minute timeout
|
|
58
|
+
follow_redirects=True,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def get_bearer_token(self) -> str:
|
|
62
|
+
"""Get bearer token for API authentication.
|
|
63
|
+
|
|
64
|
+
Caches the token and only requests a new one when the cached token
|
|
65
|
+
is expired or missing. Adds a 60-second buffer before expiration
|
|
66
|
+
to avoid edge cases.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Bearer token string
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
httpx.HTTPStatusError: If the token request fails with 4xx/5xx
|
|
73
|
+
httpx.RequestError: If the network request fails
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
token = await client.get_bearer_token()
|
|
77
|
+
# Use token in Authorization header: f"Bearer {token}"
|
|
78
|
+
"""
|
|
79
|
+
# Check if we have a cached token that hasn't expired
|
|
80
|
+
if self._cached_token and self._token_expires_at:
|
|
81
|
+
# Add 60 second buffer before expiration to avoid edge cases
|
|
82
|
+
now = datetime.now()
|
|
83
|
+
if now < self._token_expires_at:
|
|
84
|
+
# Token is still valid, return cached version
|
|
85
|
+
return self._cached_token
|
|
86
|
+
|
|
87
|
+
# Token is missing or expired, fetch a new one
|
|
88
|
+
url = f"{self.AUTH_BASE_URL}/api/v1/applications/token"
|
|
89
|
+
request_body = {
|
|
90
|
+
"client_id": self._client_id,
|
|
91
|
+
"client_secret": self._client_secret,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
response = await self._http_client.post(url, json=request_body)
|
|
95
|
+
response.raise_for_status()
|
|
96
|
+
|
|
97
|
+
data = response.json()
|
|
98
|
+
access_token = data["access_token"]
|
|
99
|
+
expires_in = 15 * 60 # default 15 min expiry time * 60 seconds
|
|
100
|
+
|
|
101
|
+
# Calculate expiration time with 60 second buffer
|
|
102
|
+
expires_at = datetime.now() + timedelta(seconds=expires_in - 60)
|
|
103
|
+
self._cached_token = access_token
|
|
104
|
+
self._token_expires_at = expires_at
|
|
105
|
+
|
|
106
|
+
return access_token
|
|
107
|
+
|
|
108
|
+
async def get_connector_id(
|
|
109
|
+
self,
|
|
110
|
+
external_user_id: str,
|
|
111
|
+
connector_definition_id: str,
|
|
112
|
+
) -> str:
|
|
113
|
+
"""Get connector ID for a user.
|
|
114
|
+
|
|
115
|
+
Looks up the connector that belongs to the specified user
|
|
116
|
+
and connector definition. Validates that exactly one connector exists.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
external_user_id: User identifier in the Airbyte system
|
|
120
|
+
connector_definition_id: UUID of the connector definition
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Connector ID (UUID string)
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
ValueError: If 0 or more than 1 connector is found
|
|
127
|
+
httpx.HTTPStatusError: If API returns 4xx/5xx status code
|
|
128
|
+
httpx.RequestError: If network request fails
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
connector_id = await client.get_connector_id(
|
|
132
|
+
external_user_id="user-123",
|
|
133
|
+
connector_definition_id="550e8400-e29b-41d4-a716-446655440000"
|
|
134
|
+
)
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
token = await self.get_bearer_token()
|
|
138
|
+
url = f"{self.API_BASE_URL}/api/v1/connectors/connectors_for_user"
|
|
139
|
+
params = {
|
|
140
|
+
"external_user_id": external_user_id,
|
|
141
|
+
"definition_id": connector_definition_id,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
145
|
+
response = await self._http_client.get(url, params=params, headers=headers)
|
|
146
|
+
response.raise_for_status()
|
|
147
|
+
|
|
148
|
+
data = response.json()
|
|
149
|
+
connectors = data["connectors"]
|
|
150
|
+
|
|
151
|
+
if len(connectors) == 0:
|
|
152
|
+
raise ValueError(f"No connector found for user '{external_user_id}' and connector definition '{connector_definition_id}'")
|
|
153
|
+
|
|
154
|
+
if len(connectors) > 1:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"Multiple connectors found for user '{external_user_id}' "
|
|
157
|
+
f"and connector definition '{connector_definition_id}'. Expected exactly 1, "
|
|
158
|
+
f"found {len(connectors)}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
connector_id = connectors[0]["id"]
|
|
162
|
+
return connector_id
|
|
163
|
+
|
|
164
|
+
async def execute_connector(
|
|
165
|
+
self,
|
|
166
|
+
connector_id: str,
|
|
167
|
+
entity: str,
|
|
168
|
+
action: str,
|
|
169
|
+
params: dict[str, Any] | None,
|
|
170
|
+
) -> dict[str, Any]:
|
|
171
|
+
"""Execute a connector operation.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
connector_id: Connector UUID (source ID)
|
|
175
|
+
entity: Entity name (e.g., "customers", "invoices")
|
|
176
|
+
action: Operation action (e.g., "list", "get", "create")
|
|
177
|
+
params: Optional parameters for the operation
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Raw JSON response dict from the API
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
httpx.HTTPStatusError: If API returns 4xx/5xx status code
|
|
184
|
+
httpx.RequestError: If network request fails
|
|
185
|
+
|
|
186
|
+
Example:
|
|
187
|
+
result = await client.execute_connector(
|
|
188
|
+
connector_id="550e8400-e29b-41d4-a716-446655440000",
|
|
189
|
+
entity="customers",
|
|
190
|
+
action="list",
|
|
191
|
+
params={"limit": 10}
|
|
192
|
+
)
|
|
193
|
+
"""
|
|
194
|
+
token = await self.get_bearer_token()
|
|
195
|
+
url = f"{self.API_BASE_URL}/api/v1/connectors/sources/{connector_id}/execute"
|
|
196
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
197
|
+
request_body = {
|
|
198
|
+
"entity": entity,
|
|
199
|
+
"action": action,
|
|
200
|
+
"params": params,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
response = await self._http_client.post(url, json=request_body, headers=headers)
|
|
204
|
+
response.raise_for_status()
|
|
205
|
+
|
|
206
|
+
return response.json()
|
|
207
|
+
|
|
208
|
+
async def close(self):
|
|
209
|
+
"""Close the HTTP client.
|
|
210
|
+
|
|
211
|
+
Call this when you're done using the client to clean up resources.
|
|
212
|
+
"""
|
|
213
|
+
await self._http_client.aclose()
|