airbyte-agent-airtable 0.1.5__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.

Potentially problematic release.


This version of airbyte-agent-airtable might be problematic. Click here for more details.

Files changed (58) hide show
  1. airbyte_agent_airtable/__init__.py +81 -0
  2. airbyte_agent_airtable/_vendored/__init__.py +1 -0
  3. airbyte_agent_airtable/_vendored/connector_sdk/__init__.py +82 -0
  4. airbyte_agent_airtable/_vendored/connector_sdk/auth_strategies.py +1171 -0
  5. airbyte_agent_airtable/_vendored/connector_sdk/auth_template.py +135 -0
  6. airbyte_agent_airtable/_vendored/connector_sdk/cloud_utils/__init__.py +5 -0
  7. airbyte_agent_airtable/_vendored/connector_sdk/cloud_utils/client.py +338 -0
  8. airbyte_agent_airtable/_vendored/connector_sdk/connector_model_loader.py +1121 -0
  9. airbyte_agent_airtable/_vendored/connector_sdk/constants.py +78 -0
  10. airbyte_agent_airtable/_vendored/connector_sdk/exceptions.py +23 -0
  11. airbyte_agent_airtable/_vendored/connector_sdk/executor/__init__.py +31 -0
  12. airbyte_agent_airtable/_vendored/connector_sdk/executor/hosted_executor.py +230 -0
  13. airbyte_agent_airtable/_vendored/connector_sdk/executor/local_executor.py +1848 -0
  14. airbyte_agent_airtable/_vendored/connector_sdk/executor/models.py +202 -0
  15. airbyte_agent_airtable/_vendored/connector_sdk/extensions.py +693 -0
  16. airbyte_agent_airtable/_vendored/connector_sdk/http/__init__.py +37 -0
  17. airbyte_agent_airtable/_vendored/connector_sdk/http/adapters/__init__.py +9 -0
  18. airbyte_agent_airtable/_vendored/connector_sdk/http/adapters/httpx_adapter.py +260 -0
  19. airbyte_agent_airtable/_vendored/connector_sdk/http/config.py +98 -0
  20. airbyte_agent_airtable/_vendored/connector_sdk/http/exceptions.py +119 -0
  21. airbyte_agent_airtable/_vendored/connector_sdk/http/protocols.py +114 -0
  22. airbyte_agent_airtable/_vendored/connector_sdk/http/response.py +104 -0
  23. airbyte_agent_airtable/_vendored/connector_sdk/http_client.py +693 -0
  24. airbyte_agent_airtable/_vendored/connector_sdk/introspection.py +481 -0
  25. airbyte_agent_airtable/_vendored/connector_sdk/logging/__init__.py +11 -0
  26. airbyte_agent_airtable/_vendored/connector_sdk/logging/logger.py +273 -0
  27. airbyte_agent_airtable/_vendored/connector_sdk/logging/types.py +93 -0
  28. airbyte_agent_airtable/_vendored/connector_sdk/observability/__init__.py +11 -0
  29. airbyte_agent_airtable/_vendored/connector_sdk/observability/config.py +179 -0
  30. airbyte_agent_airtable/_vendored/connector_sdk/observability/models.py +19 -0
  31. airbyte_agent_airtable/_vendored/connector_sdk/observability/redactor.py +81 -0
  32. airbyte_agent_airtable/_vendored/connector_sdk/observability/session.py +103 -0
  33. airbyte_agent_airtable/_vendored/connector_sdk/performance/__init__.py +6 -0
  34. airbyte_agent_airtable/_vendored/connector_sdk/performance/instrumentation.py +57 -0
  35. airbyte_agent_airtable/_vendored/connector_sdk/performance/metrics.py +93 -0
  36. airbyte_agent_airtable/_vendored/connector_sdk/schema/__init__.py +75 -0
  37. airbyte_agent_airtable/_vendored/connector_sdk/schema/base.py +212 -0
  38. airbyte_agent_airtable/_vendored/connector_sdk/schema/components.py +244 -0
  39. airbyte_agent_airtable/_vendored/connector_sdk/schema/connector.py +120 -0
  40. airbyte_agent_airtable/_vendored/connector_sdk/schema/extensions.py +301 -0
  41. airbyte_agent_airtable/_vendored/connector_sdk/schema/operations.py +156 -0
  42. airbyte_agent_airtable/_vendored/connector_sdk/schema/security.py +241 -0
  43. airbyte_agent_airtable/_vendored/connector_sdk/secrets.py +182 -0
  44. airbyte_agent_airtable/_vendored/connector_sdk/telemetry/__init__.py +10 -0
  45. airbyte_agent_airtable/_vendored/connector_sdk/telemetry/config.py +32 -0
  46. airbyte_agent_airtable/_vendored/connector_sdk/telemetry/events.py +59 -0
  47. airbyte_agent_airtable/_vendored/connector_sdk/telemetry/tracker.py +155 -0
  48. airbyte_agent_airtable/_vendored/connector_sdk/types.py +274 -0
  49. airbyte_agent_airtable/_vendored/connector_sdk/utils.py +127 -0
  50. airbyte_agent_airtable/_vendored/connector_sdk/validation.py +997 -0
  51. airbyte_agent_airtable/_vendored/connector_sdk/validation_replication.py +970 -0
  52. airbyte_agent_airtable/connector.py +834 -0
  53. airbyte_agent_airtable/connector_model.py +365 -0
  54. airbyte_agent_airtable/models.py +219 -0
  55. airbyte_agent_airtable/types.py +367 -0
  56. airbyte_agent_airtable-0.1.5.dist-info/METADATA +140 -0
  57. airbyte_agent_airtable-0.1.5.dist-info/RECORD +58 -0
  58. airbyte_agent_airtable-0.1.5.dist-info/WHEEL +4 -0
@@ -0,0 +1,155 @@
1
+ """Anonymous telemetry tracker using Segment."""
2
+
3
+ import logging
4
+ import platform
5
+ import sys
6
+ from datetime import UTC, 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.now(UTC),
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.now(UTC),
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.now(UTC),
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,274 @@
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
+ Standard CRUD actions:
21
+ GET, CREATE, UPDATE, DELETE, LIST
22
+
23
+ Special actions:
24
+ API_SEARCH - Search via API endpoint
25
+ DOWNLOAD - Download file content
26
+ AUTHORIZE - OAuth authorization flow
27
+ """
28
+
29
+ GET = "get"
30
+ CREATE = "create"
31
+ UPDATE = "update"
32
+ DELETE = "delete"
33
+ LIST = "list"
34
+ API_SEARCH = "api_search"
35
+ DOWNLOAD = "download"
36
+ AUTHORIZE = "authorize"
37
+
38
+
39
+ class AuthType(str, Enum):
40
+ """Supported authentication types."""
41
+
42
+ API_KEY = "api_key"
43
+ BEARER = "bearer"
44
+ HTTP = "http"
45
+ BASIC = "basic"
46
+ OAUTH2 = "oauth2"
47
+
48
+
49
+ class ContentType(str, Enum):
50
+ """Supported content types for request bodies."""
51
+
52
+ JSON = "application/json"
53
+ FORM_URLENCODED = "application/x-www-form-urlencoded"
54
+ FORM_DATA = "multipart/form-data"
55
+
56
+
57
+ class ParameterLocation(str, Enum):
58
+ """Location of operation parameters."""
59
+
60
+ PATH = "path"
61
+ QUERY = "query"
62
+ HEADER = "header"
63
+ COOKIE = "cookie"
64
+
65
+
66
+ # All comprehensive OpenAPI 3.0 models are now in connector_sdk.schema package
67
+ # Import from connector_sdk.schema for: OpenAPIConnector, Components, Schema, Operation, etc.
68
+
69
+
70
+ class AuthOption(BaseModel):
71
+ """A single authentication option in a multi-auth connector.
72
+
73
+ Represents one security scheme from OpenAPI components.securitySchemes.
74
+ Each option defines a complete authentication method with its own type,
75
+ configuration, and user-facing credential specification.
76
+
77
+ Example:
78
+ For a connector supporting both OAuth2 and API Key auth:
79
+ - AuthOption(scheme_name="oauth", type=OAUTH2, ...)
80
+ - AuthOption(scheme_name="apikey", type=BEARER, ...)
81
+ """
82
+
83
+ scheme_name: str = Field(description="Security scheme name from OpenAPI spec (e.g., 'githubOAuth', 'githubPAT')")
84
+ type: AuthType = Field(description="Authentication type for this option")
85
+ config: dict[str, Any] = Field(
86
+ default_factory=dict,
87
+ description="Auth-specific configuration (e.g., OAuth2 refresh settings)",
88
+ )
89
+ user_config_spec: AirbyteAuthConfig | None = Field(
90
+ None,
91
+ description="User-facing credential specification from x-airbyte-auth-config",
92
+ )
93
+ untested: bool = Field(
94
+ False,
95
+ description="Mark this auth scheme as untested to skip cassette coverage validation",
96
+ )
97
+
98
+
99
+ class AuthConfig(BaseModel):
100
+ """Authentication configuration supporting single or multiple auth methods.
101
+
102
+ Connectors can define either:
103
+ - Single auth: One authentication method (backwards compatible)
104
+ - Multi-auth: Multiple authentication methods (user/agent selects one)
105
+
106
+ For single-auth connectors (most common):
107
+ AuthConfig(type=OAUTH2, config={...}, user_config_spec={...})
108
+
109
+ For multi-auth connectors:
110
+ AuthConfig(options=[
111
+ AuthOption(scheme_name="oauth", type=OAUTH2, ...),
112
+ AuthOption(scheme_name="apikey", type=BEARER, ...)
113
+ ])
114
+ """
115
+
116
+ # Single-auth mode (backwards compatible)
117
+ type: AuthType | None = Field(
118
+ None,
119
+ description="Authentication type (single-auth mode only)",
120
+ )
121
+ config: dict[str, Any] = Field(
122
+ default_factory=dict,
123
+ description="Auth configuration (single-auth mode only)",
124
+ )
125
+ user_config_spec: AirbyteAuthConfig | None = Field(
126
+ None,
127
+ description="User-facing config spec from x-airbyte-auth-config (single-auth mode)",
128
+ )
129
+
130
+ # Multi-auth mode
131
+ options: list[AuthOption] | None = Field(
132
+ None,
133
+ description="Multiple authentication options (multi-auth mode only)",
134
+ )
135
+
136
+ def is_multi_auth(self) -> bool:
137
+ """Check if this configuration supports multiple authentication methods.
138
+
139
+ Returns:
140
+ True if multiple auth options are available, False for single-auth
141
+ """
142
+ return self.options is not None and len(self.options) > 0
143
+
144
+ def get_single_option(self) -> AuthOption:
145
+ """Get single auth option (for backwards compatibility).
146
+
147
+ Converts single-auth config to AuthOption format for uniform handling.
148
+
149
+ Returns:
150
+ AuthOption containing the single auth configuration
151
+
152
+ Raises:
153
+ ValueError: If this is a multi-auth config or invalid
154
+ """
155
+ if self.is_multi_auth():
156
+ raise ValueError("Cannot call get_single_option() on multi-auth config. Use options list instead.")
157
+
158
+ if self.type is None:
159
+ raise ValueError("Invalid AuthConfig: neither single-auth nor multi-auth")
160
+
161
+ return AuthOption(
162
+ scheme_name="default",
163
+ type=self.type,
164
+ config=self.config,
165
+ user_config_spec=self.user_config_spec,
166
+ )
167
+
168
+
169
+ # Executor types (used by executor.py)
170
+ class EndpointDefinition(BaseModel):
171
+ """Definition of an API endpoint."""
172
+
173
+ method: str # GET, POST, PUT, DELETE, etc.
174
+ path: str # e.g., /v1/customers/{id} (OpenAPI path)
175
+ path_override: PathOverrideConfig | None = Field(
176
+ None,
177
+ description=("Path override config from x-airbyte-path-override. When set, overrides the path for actual HTTP requests."),
178
+ )
179
+ action: Action | None = None # Semantic action (get, list, create, update, delete)
180
+ description: str | None = None
181
+ body_fields: list[str] = Field(default_factory=list) # For POST/PUT
182
+ query_params: list[str] = Field(default_factory=list) # For GET
183
+ query_params_schema: dict[str, dict[str, Any]] = Field(
184
+ default_factory=dict,
185
+ description="Schema for query params including defaults: {name: {type, default, required}}",
186
+ )
187
+ deep_object_params: list[str] = Field(
188
+ default_factory=list,
189
+ description="Query parameters using deepObject style (e.g., filter[key]=value)",
190
+ ) # For GET with deepObject query params
191
+ path_params: list[str] = Field(default_factory=list) # Extracted from path
192
+ path_params_schema: dict[str, dict[str, Any]] = Field(
193
+ default_factory=dict,
194
+ description="Schema for path params including defaults: {name: {type, default, required}}",
195
+ )
196
+ header_params: list[str] = Field(default_factory=list) # Header parameters from OpenAPI
197
+ header_params_schema: dict[str, dict[str, Any]] = Field(
198
+ default_factory=dict,
199
+ description="Schema for header params including defaults: {name: {type, default, required}}",
200
+ )
201
+ request_body_defaults: dict[str, Any] = Field(
202
+ default_factory=dict,
203
+ description="Default values for request body fields from OpenAPI schema",
204
+ )
205
+ content_type: ContentType = ContentType.JSON
206
+ request_schema: dict[str, Any] | None = None
207
+ response_schema: dict[str, Any] | None = None
208
+
209
+ # GraphQL support (Airbyte extension)
210
+ graphql_body: dict[str, Any] | None = Field(
211
+ None,
212
+ description="GraphQL body configuration from x-airbyte-body-type extension",
213
+ )
214
+
215
+ # Record extractor support (Airbyte extension)
216
+ record_extractor: str | None = Field(
217
+ None,
218
+ description="JSONPath expression to extract records from response envelopes",
219
+ )
220
+
221
+ # Metadata extractor support (Airbyte extension)
222
+ meta_extractor: dict[str, str] | None = Field(
223
+ None,
224
+ description="Dictionary mapping field names to JSONPath expressions for extracting metadata from response envelopes",
225
+ )
226
+
227
+ # Download support (Airbyte extension)
228
+ file_field: str | None = Field(
229
+ None,
230
+ description="Field in metadata response containing download URL (from x-airbyte-file-url extension)",
231
+ )
232
+
233
+ # Test validation support (Airbyte extension)
234
+ untested: bool = Field(
235
+ False,
236
+ description="Mark operation as untested to skip cassette validation (from x-airbyte-untested extension)",
237
+ )
238
+
239
+ # Health check support (Airbyte extension)
240
+ preferred_for_check: bool = Field(
241
+ False,
242
+ description="Mark this list operation as preferred for health checks (from x-airbyte-preferred-for-check extension)",
243
+ )
244
+
245
+
246
+ class EntityDefinition(BaseModel):
247
+ """Definition of an API entity."""
248
+
249
+ model_config = {"populate_by_name": True}
250
+
251
+ name: str
252
+ stream_name: str | None = Field(
253
+ default=None,
254
+ description="Airbyte stream name for cache lookup (from x-airbyte-stream-name schema extension)",
255
+ )
256
+ actions: list[Action]
257
+ endpoints: dict[Action, EndpointDefinition]
258
+ entity_schema: dict[str, Any] | None = Field(default=None, alias="schema")
259
+
260
+
261
+ class ConnectorModel(BaseModel):
262
+ """Complete connector model loaded from YAML definition."""
263
+
264
+ model_config = ConfigDict(use_enum_values=True)
265
+
266
+ id: UUID
267
+ name: str
268
+ version: str = OPENAPI_DEFAULT_VERSION
269
+ base_url: str
270
+ auth: AuthConfig
271
+ entities: list[EntityDefinition]
272
+ openapi_spec: Any | None = None # Optional reference to OpenAPIConnector
273
+ retry_config: RetryConfig | None = None # Optional retry configuration
274
+ search_field_paths: dict[str, list[str]] | None = None
@@ -0,0 +1,127 @@
1
+ """Utility functions for working with connectors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncIterator
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from .types import AuthOption
11
+
12
+
13
+ async def save_download(
14
+ download_iterator: AsyncIterator[bytes],
15
+ path: str | Path,
16
+ *,
17
+ overwrite: bool = False,
18
+ ) -> Path:
19
+ """Save a download iterator to a file.
20
+
21
+ Args:
22
+ download_iterator: AsyncIterator[bytes] from a download operation
23
+ path: File path where content should be saved
24
+ overwrite: Whether to overwrite existing file (default: False)
25
+
26
+ Returns:
27
+ Absolute Path to the saved file
28
+
29
+ Raises:
30
+ FileExistsError: If file exists and overwrite=False
31
+ OSError: If file cannot be written
32
+
33
+ Example:
34
+ >>> from .utils import save_download
35
+ >>>
36
+ >>> # Download and save a file
37
+ >>> result = await connector.download_article_attachment(id="123")
38
+ >>> file_path = await save_download(result, "./downloads/attachment.pdf")
39
+ >>> print(f"Downloaded to {file_path}")
40
+ Downloaded to /absolute/path/to/downloads/attachment.pdf
41
+ >>>
42
+ >>> # Overwrite existing file
43
+ >>> file_path = await save_download(result, "./downloads/attachment.pdf", overwrite=True)
44
+ """
45
+ # Convert to Path object
46
+ file_path = Path(path).expanduser().resolve()
47
+
48
+ # Check if file exists
49
+ if file_path.exists() and not overwrite:
50
+ raise FileExistsError(f"File already exists: {file_path}. Use overwrite=True to replace it.")
51
+
52
+ # Create parent directories if needed
53
+ file_path.parent.mkdir(parents=True, exist_ok=True)
54
+
55
+ # Stream content to file
56
+ try:
57
+ with open(file_path, "wb") as f:
58
+ async for chunk in download_iterator:
59
+ f.write(chunk)
60
+ except Exception as e:
61
+ # Clean up partial file on error
62
+ if file_path.exists():
63
+ file_path.unlink()
64
+ raise OSError(f"Failed to write file {file_path}: {e}") from e
65
+
66
+ return file_path
67
+
68
+
69
+ def find_matching_auth_options(
70
+ provided_keys: set[str],
71
+ auth_options: list[AuthOption],
72
+ ) -> list[AuthOption]:
73
+ """Find auth options that match the provided credential keys.
74
+
75
+ This is the single source of truth for auth scheme inference logic,
76
+ used by both the executor (at runtime) and validation (for cassettes).
77
+
78
+ Matching logic:
79
+ - An option matches if all its required fields are present in provided_keys
80
+ - Options with no required fields match any credentials
81
+
82
+ Args:
83
+ provided_keys: Set of credential/auth_config keys
84
+ auth_options: List of AuthOption from the connector model
85
+
86
+ Returns:
87
+ List of AuthOption that match the provided keys
88
+ """
89
+ matching_options: list[AuthOption] = []
90
+
91
+ for option in auth_options:
92
+ if option.user_config_spec and option.user_config_spec.required:
93
+ required_fields = set(option.user_config_spec.required)
94
+ if required_fields.issubset(provided_keys):
95
+ matching_options.append(option)
96
+ elif not option.user_config_spec or not option.user_config_spec.required:
97
+ # Option has no required fields - it matches any credentials
98
+ matching_options.append(option)
99
+
100
+ return matching_options
101
+
102
+
103
+ def infer_auth_scheme_name(
104
+ provided_keys: set[str],
105
+ auth_options: list[AuthOption],
106
+ ) -> str | None:
107
+ """Infer the auth scheme name from provided credential keys.
108
+
109
+ Uses find_matching_auth_options to find matches, then returns
110
+ the scheme name only if exactly one option matches.
111
+
112
+ Args:
113
+ provided_keys: Set of credential/auth_config keys
114
+ auth_options: List of AuthOption from the connector model
115
+
116
+ Returns:
117
+ The scheme_name if exactly one match, None otherwise
118
+ """
119
+ if not provided_keys or not auth_options:
120
+ return None
121
+
122
+ matching = find_matching_auth_options(provided_keys, auth_options)
123
+
124
+ if len(matching) == 1:
125
+ return matching[0].scheme_name
126
+
127
+ return None