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,1121 @@
1
+ """Load and parse connector YAML definitions into ConnectorModel objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any
9
+ from uuid import UUID
10
+
11
+ import jsonref
12
+ import yaml
13
+ from pydantic import ValidationError
14
+
15
+ from .constants import (
16
+ OPENAPI_DEFAULT_VERSION,
17
+ OPENAPI_VERSION_PREFIX,
18
+ )
19
+
20
+ from .schema import OpenAPIConnector
21
+ from .schema.components import GraphQLBodyConfig, RequestBody
22
+ from .schema.security import AirbyteAuthConfig, AuthConfigFieldSpec
23
+ from .types import (
24
+ Action,
25
+ AuthConfig,
26
+ AuthOption,
27
+ AuthType,
28
+ ConnectorModel,
29
+ ContentType,
30
+ EndpointDefinition,
31
+ EntityDefinition,
32
+ )
33
+
34
+
35
+ class ConnectorModelLoaderError(Exception):
36
+ """Base exception for connector model loading errors."""
37
+
38
+ pass
39
+
40
+
41
+ class InvalidYAMLError(ConnectorModelLoaderError):
42
+ """Raised when YAML syntax is invalid."""
43
+
44
+ pass
45
+
46
+
47
+ class InvalidOpenAPIError(ConnectorModelLoaderError):
48
+ """Raised when OpenAPI specification is invalid."""
49
+
50
+ pass
51
+
52
+
53
+ class DuplicateEntityError(ConnectorModelLoaderError):
54
+ """Raised when duplicate entity names are detected."""
55
+
56
+ pass
57
+
58
+
59
+ class TokenExtractValidationError(ConnectorModelLoaderError):
60
+ """Raised when x-airbyte-token-extract references invalid server variables."""
61
+
62
+ pass
63
+
64
+
65
+ # Expected auth_mapping keys for each auth type.
66
+ # These are the auth parameters that each security scheme expects, NOT the user's credential field names.
67
+ EXPECTED_AUTH_MAPPING_KEYS: dict[AuthType, set[str]] = {
68
+ AuthType.BEARER: {"token"},
69
+ AuthType.BASIC: {"username", "password"},
70
+ AuthType.API_KEY: {"api_key"},
71
+ AuthType.OAUTH2: {"access_token", "refresh_token", "client_id", "client_secret"},
72
+ }
73
+
74
+
75
+ def _validate_auth_mapping_keys(
76
+ auth_type: AuthType,
77
+ auth_config: AirbyteAuthConfig | None,
78
+ scheme_name: str = "default",
79
+ ) -> None:
80
+ """Validate that auth_mapping keys match expected parameters for the auth type.
81
+
82
+ The auth_mapping keys must be the parameters expected by the security scheme
83
+ (e.g., "token" for bearer), not the user's credential field names.
84
+
85
+ Args:
86
+ auth_type: The authentication type
87
+ auth_config: The x-airbyte-auth-config containing auth_mapping
88
+ scheme_name: Name of the security scheme for error messages
89
+
90
+ Raises:
91
+ InvalidOpenAPIError: If auth_mapping keys don't match expected parameters
92
+ """
93
+ if auth_config is None or auth_config.auth_mapping is None:
94
+ return # No explicit auth_mapping, will use defaults
95
+
96
+ expected_keys = EXPECTED_AUTH_MAPPING_KEYS.get(auth_type)
97
+ if expected_keys is None:
98
+ return # Unknown auth type, skip validation
99
+
100
+ actual_keys = set(auth_config.auth_mapping.keys())
101
+ invalid_keys = actual_keys - expected_keys
102
+
103
+ if invalid_keys:
104
+ raise InvalidOpenAPIError(
105
+ f"Invalid auth_mapping keys for {auth_type.value} auth in scheme '{scheme_name}': {invalid_keys}. "
106
+ f"Expected keys for {auth_type.value}: {sorted(expected_keys)}. "
107
+ f"Note: auth_mapping keys must be the auth parameters (e.g., 'token' for bearer), "
108
+ f'not your credential field names. Use template syntax to map: token: "${{your_field}}"'
109
+ )
110
+
111
+
112
+ def extract_path_params(path: str) -> list[str]:
113
+ """Extract parameter names from path template.
114
+
115
+ Example: '/v1/customers/{id}/invoices/{invoice_id}' -> ['id', 'invoice_id']
116
+ """
117
+ return re.findall(r"\{(\w+)\}", path)
118
+
119
+
120
+ def resolve_schema_refs(schema: Any, spec_dict: dict) -> dict[str, Any]:
121
+ """Resolve all $ref references in a schema using jsonref.
122
+
123
+ This handles:
124
+ - Simple $refs to components/schemas
125
+ - Nested $refs within schemas
126
+ - Circular references (jsonref handles these gracefully)
127
+
128
+ Args:
129
+ schema: The schema that may contain $refs (can be dict or Pydantic model)
130
+ spec_dict: The full OpenAPI spec as a dict (for reference resolution)
131
+
132
+ Returns:
133
+ Resolved schema as a dictionary with all $refs replaced by their definitions
134
+ """
135
+ if not schema:
136
+ return {}
137
+
138
+ # Convert schema to dict if it's a Pydantic model
139
+ if hasattr(schema, "model_dump"):
140
+ schema_dict = schema.model_dump(by_alias=True, exclude_none=True)
141
+ elif isinstance(schema, dict):
142
+ schema_dict = schema
143
+ else:
144
+ return {}
145
+
146
+ # If there are no $refs, return as-is
147
+ if "$ref" not in str(schema_dict):
148
+ return schema_dict
149
+
150
+ # Use jsonref to resolve all references
151
+ # We need to embed the schema in the spec for proper reference resolution
152
+ temp_spec = spec_dict.copy()
153
+ temp_spec["__temp_schema__"] = schema_dict
154
+
155
+ try:
156
+ # Resolve all references
157
+ resolved_spec = jsonref.replace_refs( # type: ignore[union-attr]
158
+ temp_spec,
159
+ base_uri="",
160
+ jsonschema=True, # Use JSONSchema draft 7 semantics
161
+ lazy_load=False, # Resolve everything immediately
162
+ )
163
+
164
+ # Extract our resolved schema
165
+ resolved_schema = dict(resolved_spec.get("__temp_schema__", {}))
166
+
167
+ # Remove any remaining jsonref proxy objects by converting to plain dict
168
+ return _deproxy_schema(resolved_schema)
169
+ except (AttributeError, KeyError, RecursionError, Exception):
170
+ # If resolution fails, return the original schema
171
+ # This allows the system to continue even with malformed $refs
172
+ # AttributeError covers the case where jsonref might be None
173
+ # Exception catches jsonref.JsonRefError and other jsonref exceptions
174
+ return schema_dict
175
+
176
+
177
+ def _deproxy_schema(obj: Any) -> Any:
178
+ """Recursively convert jsonref proxy objects to plain dicts/lists.
179
+
180
+ jsonref returns proxy objects that behave like dicts but aren't actual dicts.
181
+ This converts them to plain Python objects for consistent behavior.
182
+ """
183
+ if isinstance(obj, dict) or (hasattr(obj, "__subject__") and hasattr(obj, "keys")):
184
+ # Handle both dicts and jsonref proxy objects
185
+ try:
186
+ return {str(k): _deproxy_schema(v) for k, v in obj.items()}
187
+ except (AttributeError, TypeError):
188
+ return obj
189
+ elif isinstance(obj, (list, tuple)):
190
+ return [_deproxy_schema(item) for item in obj]
191
+ else:
192
+ return obj
193
+
194
+
195
+ def _type_includes(type_value: Any, target: str) -> bool:
196
+ if isinstance(type_value, list):
197
+ return target in type_value
198
+ return type_value == target
199
+
200
+
201
+ def _flatten_cache_properties(properties: dict[str, Any], prefix: str) -> list[str]:
202
+ entries: list[str] = []
203
+ for prop_name, prop in properties.items():
204
+ path = f"{prefix}{prop_name}" if prefix else prop_name
205
+ entries.append(path)
206
+
207
+ prop_type = getattr(prop, "type", None) if not isinstance(prop, dict) else prop.get("type")
208
+ prop_properties = getattr(prop, "properties", None) if not isinstance(prop, dict) else prop.get("properties")
209
+
210
+ if _type_includes(prop_type, "array"):
211
+ array_path = f"{path}[]"
212
+ entries.append(array_path)
213
+ if isinstance(prop_properties, dict):
214
+ entries.extend(_flatten_cache_properties(prop_properties, prefix=f"{array_path}."))
215
+ elif isinstance(prop_properties, dict):
216
+ entries.extend(_flatten_cache_properties(prop_properties, prefix=f"{path}."))
217
+
218
+ return entries
219
+
220
+
221
+ def _flatten_cache_field_paths(field: Any) -> list[str]:
222
+ field_name = getattr(field, "name", None) if not isinstance(field, dict) else field.get("name")
223
+ if not isinstance(field_name, str) or not field_name:
224
+ return []
225
+
226
+ field_type = getattr(field, "type", None) if not isinstance(field, dict) else field.get("type")
227
+ field_properties = getattr(field, "properties", None) if not isinstance(field, dict) else field.get("properties")
228
+
229
+ entries = [field_name]
230
+ if _type_includes(field_type, "array"):
231
+ array_path = f"{field_name}[]"
232
+ entries.append(array_path)
233
+ if isinstance(field_properties, dict):
234
+ entries.extend(_flatten_cache_properties(field_properties, prefix=f"{array_path}."))
235
+ elif isinstance(field_properties, dict):
236
+ entries.extend(_flatten_cache_properties(field_properties, prefix=f"{field_name}."))
237
+
238
+ return entries
239
+
240
+
241
+ def _dedupe_strings(values: list[str]) -> list[str]:
242
+ seen: set[str] = set()
243
+ ordered: list[str] = []
244
+ for value in values:
245
+ if value not in seen:
246
+ seen.add(value)
247
+ ordered.append(value)
248
+ return ordered
249
+
250
+
251
+ def _extract_search_field_paths(spec: OpenAPIConnector) -> dict[str, list[str]]:
252
+ cache_config = getattr(spec.info, "x_airbyte_cache", None)
253
+ entities = getattr(cache_config, "entities", None)
254
+ if not isinstance(entities, list):
255
+ return {}
256
+
257
+ search_fields: dict[str, list[str]] = {}
258
+ for entity in entities:
259
+ entity_name = getattr(entity, "entity", None) if not isinstance(entity, dict) else entity.get("entity")
260
+ if not isinstance(entity_name, str) or not entity_name:
261
+ continue
262
+
263
+ fields = getattr(entity, "fields", None) if not isinstance(entity, dict) else entity.get("fields")
264
+ if not isinstance(fields, list):
265
+ continue
266
+
267
+ field_paths: list[str] = []
268
+ for field in fields:
269
+ field_paths.extend(_flatten_cache_field_paths(field))
270
+
271
+ search_fields[entity_name] = _dedupe_strings(field_paths)
272
+
273
+ return search_fields
274
+
275
+
276
+ def parse_openapi_spec(raw_config: dict) -> OpenAPIConnector:
277
+ """Parse OpenAPI specification from YAML.
278
+
279
+ Args:
280
+ raw_config: Raw YAML configuration
281
+
282
+ Returns:
283
+ Parsed OpenAPIConnector with full validation
284
+
285
+ Raises:
286
+ InvalidOpenAPIError: If OpenAPI spec is invalid or missing required fields
287
+ """
288
+ # Validate OpenAPI version
289
+ openapi_version = raw_config.get("openapi", "")
290
+ if not openapi_version:
291
+ raise InvalidOpenAPIError("Missing required field: 'openapi' version")
292
+
293
+ # Check if version is 3.1.x (we don't support 2.x or 3.0.x)
294
+ if not openapi_version.startswith(OPENAPI_VERSION_PREFIX):
295
+ raise InvalidOpenAPIError(f"Unsupported OpenAPI version: {openapi_version}. Only {OPENAPI_VERSION_PREFIX}x is supported.")
296
+
297
+ # Validate required top-level fields
298
+ if "info" not in raw_config:
299
+ raise InvalidOpenAPIError("Missing required field: 'info'")
300
+
301
+ if "paths" not in raw_config:
302
+ raise InvalidOpenAPIError("Missing required field: 'paths'")
303
+
304
+ # Validate paths is not empty
305
+ if not raw_config["paths"]:
306
+ raise InvalidOpenAPIError("OpenAPI spec must have at least one path definition")
307
+
308
+ # Parse with Pydantic validation
309
+ try:
310
+ spec = OpenAPIConnector(**raw_config)
311
+ except ValidationError as e:
312
+ raise InvalidOpenAPIError(f"OpenAPI validation failed: {e}")
313
+
314
+ return spec
315
+
316
+
317
+ def _extract_request_body_config(
318
+ request_body: RequestBody | None, spec_dict: dict[str, Any]
319
+ ) -> tuple[list[str], dict[str, Any] | None, dict[str, Any] | None, dict[str, Any]]:
320
+ """Extract request body configuration (GraphQL or standard).
321
+
322
+ Args:
323
+ request_body: RequestBody object from OpenAPI operation
324
+ spec_dict: Full OpenAPI spec dict for $ref resolution
325
+
326
+ Returns:
327
+ Tuple of (body_fields, request_schema, graphql_body, request_body_defaults)
328
+ - body_fields: List of field names for standard JSON/form bodies
329
+ - request_schema: Resolved request schema dict (for standard bodies)
330
+ - graphql_body: GraphQL body configuration dict (for GraphQL bodies)
331
+ - request_body_defaults: Default values for request body fields
332
+ """
333
+ body_fields: list[str] = []
334
+ request_schema: dict[str, Any] | None = None
335
+ graphql_body: dict[str, Any] | None = None
336
+ request_body_defaults: dict[str, Any] = {}
337
+
338
+ if not request_body:
339
+ return body_fields, request_schema, graphql_body, request_body_defaults
340
+
341
+ # Check for GraphQL extension and extract GraphQL body configuration
342
+ if request_body.x_airbyte_body_type:
343
+ body_type_config = request_body.x_airbyte_body_type
344
+
345
+ # Check if it's GraphQL type (it's a GraphQLBodyConfig Pydantic model)
346
+ if isinstance(body_type_config, GraphQLBodyConfig):
347
+ # Convert Pydantic model to dict, excluding None values
348
+ graphql_body = body_type_config.model_dump(exclude_none=True, by_alias=False)
349
+ return body_fields, request_schema, graphql_body, request_body_defaults
350
+
351
+ # Parse standard request body
352
+ for content_type_key, media_type in request_body.content.items():
353
+ # media_type is now a MediaType object with schema_ field
354
+ schema = media_type.schema_ or {}
355
+
356
+ # Resolve all $refs in the schema using jsonref
357
+ request_schema = resolve_schema_refs(schema, spec_dict)
358
+
359
+ # Extract body field names and defaults from resolved schema
360
+ if isinstance(request_schema, dict) and "properties" in request_schema:
361
+ body_fields = list(request_schema["properties"].keys())
362
+ # Extract default values for each property
363
+ for field_name, field_schema in request_schema["properties"].items():
364
+ if isinstance(field_schema, dict) and "default" in field_schema:
365
+ request_body_defaults[field_name] = field_schema["default"]
366
+
367
+ return body_fields, request_schema, graphql_body, request_body_defaults
368
+
369
+
370
+ def convert_openapi_to_connector_model(spec: OpenAPIConnector) -> ConnectorModel:
371
+ """Convert OpenAPI spec to ConnectorModel format.
372
+
373
+ Args:
374
+ spec: OpenAPI connector specification (fully validated)
375
+
376
+ Returns:
377
+ ConnectorModel with entities and endpoints
378
+ """
379
+ # Validate x-airbyte-token-extract against server variables
380
+ _validate_token_extract(spec)
381
+
382
+ # Convert spec to dict for jsonref resolution
383
+ spec_dict = spec.model_dump(by_alias=True, exclude_none=True)
384
+
385
+ # Extract connector name and version
386
+ name = spec.info.x_airbyte_connector_name or spec.info.title.lower().replace(" ", "-")
387
+ version = spec.info.version
388
+
389
+ # Parse authentication first to get token_extract fields
390
+ auth_config = _parse_auth_from_openapi(spec)
391
+
392
+ # Extract base URL from servers - keep variable placeholders intact
393
+ # Variables will be substituted at runtime by HTTPClient using:
394
+ # - config_values: for user-provided values like subdomain
395
+ # - token_extract: for OAuth dynamic values like instance_url
396
+ # DO NOT substitute defaults here - that would prevent runtime substitution
397
+ base_url = ""
398
+ if spec.servers:
399
+ base_url = spec.servers[0].url
400
+
401
+ # Group operations by entity
402
+ entities_map: dict[str, dict[str, EndpointDefinition]] = {}
403
+
404
+ for path, path_item in spec.paths.items():
405
+ # Check each HTTP method
406
+ for method_name in ["get", "post", "put", "delete", "patch"]:
407
+ operation = getattr(path_item, method_name, None)
408
+ if not operation:
409
+ continue
410
+
411
+ # Extract entity and action from x-airbyte-entity and x-airbyte-action
412
+ entity_name = operation.x_airbyte_entity
413
+ action_name = operation.x_airbyte_action
414
+ path_override = operation.x_airbyte_path_override
415
+ record_extractor = operation.x_airbyte_record_extractor
416
+ meta_extractor = operation.x_airbyte_meta_extractor
417
+
418
+ if not entity_name:
419
+ raise InvalidOpenAPIError(
420
+ f"Missing required x-airbyte-entity in operation {method_name.upper()} {path}. All operations must specify an entity."
421
+ )
422
+
423
+ if not action_name:
424
+ raise InvalidOpenAPIError(
425
+ f"Missing required x-airbyte-action in operation {method_name.upper()} {path}. All operations must specify an action."
426
+ )
427
+
428
+ # Convert to Action enum
429
+ try:
430
+ action = Action(action_name)
431
+ except ValueError:
432
+ # Provide clear error for invalid actions
433
+ valid_actions = ", ".join([a.value for a in Action])
434
+ raise InvalidOpenAPIError(
435
+ f"Invalid action '{action_name}' in operation {method_name.upper()} {path}. Valid actions are: {valid_actions}"
436
+ )
437
+
438
+ # Determine content type
439
+ content_type = ContentType.JSON
440
+ if operation.request_body and operation.request_body.content:
441
+ if "application/x-www-form-urlencoded" in operation.request_body.content:
442
+ content_type = ContentType.FORM_URLENCODED
443
+ elif "multipart/form-data" in operation.request_body.content:
444
+ content_type = ContentType.FORM_DATA
445
+
446
+ # Extract parameters with their schemas (including defaults)
447
+ path_params: list[str] = []
448
+ path_params_schema: dict[str, dict[str, Any]] = {}
449
+ query_params: list[str] = []
450
+ query_params_schema: dict[str, dict[str, Any]] = {}
451
+ deep_object_params: list[str] = []
452
+ header_params: list[str] = []
453
+ header_params_schema: dict[str, dict[str, Any]] = {}
454
+
455
+ if operation.parameters:
456
+ for param in operation.parameters:
457
+ param_schema = param.schema_ or {}
458
+ schema_info = {
459
+ "type": param_schema.get("type", "string"),
460
+ "required": param.required or False,
461
+ "default": param_schema.get("default"),
462
+ }
463
+
464
+ if param.in_ == "path":
465
+ path_params.append(param.name)
466
+ # Path params are always required
467
+ schema_info["required"] = True
468
+ path_params_schema[param.name] = schema_info
469
+ elif param.in_ == "query":
470
+ query_params.append(param.name)
471
+ query_params_schema[param.name] = schema_info
472
+ # Check if this is a deepObject style parameter
473
+ if hasattr(param, "style") and param.style == "deepObject":
474
+ deep_object_params.append(param.name)
475
+ elif param.in_ == "header":
476
+ header_params.append(param.name)
477
+ header_params_schema[param.name] = schema_info
478
+
479
+ # Extract body fields and defaults from request schema
480
+ body_fields, request_schema, graphql_body, request_body_defaults = _extract_request_body_config(operation.request_body, spec_dict)
481
+
482
+ # Extract response schema
483
+ response_schema = None
484
+ if "200" in operation.responses:
485
+ response = operation.responses["200"]
486
+ if response.content and "application/json" in response.content:
487
+ media_type = response.content["application/json"]
488
+ schema = media_type.schema_ if media_type else {}
489
+
490
+ # Resolve all $refs in the response schema using jsonref
491
+ response_schema = resolve_schema_refs(schema, spec_dict)
492
+
493
+ # Extract file_field for download operations
494
+ file_field = getattr(operation, "x_airbyte_file_url", None)
495
+
496
+ # Extract untested flag
497
+ untested = getattr(operation, "x_airbyte_untested", None) or False
498
+
499
+ # Extract preferred_for_check flag
500
+ preferred_for_check = getattr(operation, "x_airbyte_preferred_for_check", None) or False
501
+
502
+ # Create endpoint definition
503
+ endpoint = EndpointDefinition(
504
+ method=method_name.upper(),
505
+ action=action,
506
+ path=path,
507
+ path_override=path_override,
508
+ record_extractor=record_extractor,
509
+ meta_extractor=meta_extractor,
510
+ description=operation.description or operation.summary,
511
+ body_fields=body_fields,
512
+ query_params=query_params,
513
+ query_params_schema=query_params_schema,
514
+ deep_object_params=deep_object_params,
515
+ path_params=path_params,
516
+ path_params_schema=path_params_schema,
517
+ header_params=header_params,
518
+ header_params_schema=header_params_schema,
519
+ request_body_defaults=request_body_defaults,
520
+ content_type=content_type,
521
+ request_schema=request_schema,
522
+ response_schema=response_schema,
523
+ graphql_body=graphql_body,
524
+ file_field=file_field,
525
+ untested=untested,
526
+ preferred_for_check=preferred_for_check,
527
+ )
528
+
529
+ # Add to entities map
530
+ if entity_name not in entities_map:
531
+ entities_map[entity_name] = {}
532
+ entities_map[entity_name][action] = endpoint
533
+
534
+ # Note: No need to check for duplicate entity names - the dict structure
535
+ # automatically ensures uniqueness. If the OpenAPI spec contains duplicate
536
+ # operationIds, only the last one will be kept.
537
+
538
+ # Convert entities map to EntityDefinition list
539
+ entities = []
540
+ for entity_name, endpoints_dict in entities_map.items():
541
+ actions = list(endpoints_dict.keys())
542
+
543
+ # Get schema and stream_name from components if available
544
+ schema = None
545
+ entity_stream_name = None
546
+ if spec.components:
547
+ # Look for a schema matching the entity name
548
+ for schema_name, schema_def in spec.components.schemas.items():
549
+ if schema_def.x_airbyte_entity_name == entity_name or schema_name.lower() == entity_name.lower():
550
+ schema = schema_def.model_dump(by_alias=True)
551
+ entity_stream_name = schema_def.x_airbyte_stream_name
552
+ break
553
+
554
+ entity = EntityDefinition(
555
+ name=entity_name,
556
+ stream_name=entity_stream_name,
557
+ actions=actions,
558
+ endpoints=endpoints_dict,
559
+ schema=schema,
560
+ )
561
+ entities.append(entity)
562
+
563
+ # Extract retry config from x-airbyte-retry-config extension
564
+ retry_config = spec.info.x_airbyte_retry_config
565
+ connector_id = spec.info.x_airbyte_connector_id
566
+ if not connector_id:
567
+ raise InvalidOpenAPIError("Missing required x-airbyte-connector-id field")
568
+
569
+ search_field_paths = _extract_search_field_paths(spec)
570
+
571
+ # Create ConnectorModel
572
+ model = ConnectorModel(
573
+ id=connector_id,
574
+ name=name,
575
+ version=version,
576
+ base_url=base_url,
577
+ auth=auth_config,
578
+ entities=entities,
579
+ openapi_spec=spec,
580
+ retry_config=retry_config,
581
+ search_field_paths=search_field_paths,
582
+ )
583
+
584
+ return model
585
+
586
+
587
+ def _get_attribute_flexible(obj: Any, *names: str) -> Any:
588
+ """Get attribute from object, trying multiple name variants.
589
+
590
+ Supports both snake_case and camelCase attribute names.
591
+ Returns None if no variant is found.
592
+
593
+ Args:
594
+ obj: Object to get attribute from
595
+ *names: Attribute names to try in order
596
+
597
+ Returns:
598
+ Attribute value if found, None otherwise
599
+
600
+ Example:
601
+ # Try both "refresh_url" and "refreshUrl"
602
+ url = _get_attribute_flexible(flow, "refresh_url", "refreshUrl")
603
+ """
604
+ for name in names:
605
+ value = getattr(obj, name, None)
606
+ if value is not None:
607
+ return value
608
+ return None
609
+
610
+
611
+ def _select_oauth2_flow(flows: Any) -> Any:
612
+ """Select the best OAuth2 flow from available flows.
613
+
614
+ Prefers authorizationCode (most secure for web apps), but falls back
615
+ to other flow types if not available.
616
+
617
+ Args:
618
+ flows: OAuth2 flows object from OpenAPI spec
619
+
620
+ Returns:
621
+ Selected flow object, or None if no flows available
622
+ """
623
+ # Priority order: authorizationCode > clientCredentials > password > implicit
624
+ flow_names = [
625
+ ("authorization_code", "authorizationCode"), # Preferred
626
+ ("client_credentials", "clientCredentials"), # Server-to-server
627
+ ("password", "password"), # Resource owner
628
+ ("implicit", "implicit"), # Legacy, less secure
629
+ ]
630
+
631
+ for snake_case, camel_case in flow_names:
632
+ flow = _get_attribute_flexible(flows, snake_case, camel_case)
633
+ if flow:
634
+ return flow
635
+
636
+ return None
637
+
638
+
639
+ def _parse_oauth2_config(scheme: Any) -> dict[str, str]:
640
+ """Parse OAuth2 authentication configuration from OpenAPI scheme.
641
+
642
+ Extracts configuration from standard OAuth2 flows and custom x-airbyte-token-refresh
643
+ extension for additional refresh behavior customization.
644
+
645
+ Args:
646
+ scheme: OAuth2 security scheme from OpenAPI spec
647
+
648
+ Returns:
649
+ Dictionary with OAuth2 configuration including:
650
+ - header: Authorization header name (default: "Authorization")
651
+ - prefix: Token prefix (default: "Bearer")
652
+ - refresh_url: Token refresh endpoint (from flows)
653
+ - auth_style: How to send credentials (from x-airbyte-token-refresh)
654
+ - body_format: Request encoding (from x-airbyte-token-refresh)
655
+ """
656
+ config: dict[str, str] = {
657
+ "header": "Authorization",
658
+ "prefix": "Bearer",
659
+ }
660
+
661
+ # Extract flow information for refresh_url
662
+ if scheme.flows:
663
+ flow = _select_oauth2_flow(scheme.flows)
664
+ if flow:
665
+ # Try to get refresh URL (supports both naming conventions)
666
+ refresh_url = _get_attribute_flexible(flow, "refresh_url", "refreshUrl")
667
+ if refresh_url:
668
+ config["refresh_url"] = refresh_url
669
+
670
+ # Extract custom refresh configuration from x-airbyte-token-refresh extension
671
+ # Note: x_token_refresh is a Dict[str, Any], not a Pydantic model, so use .get()
672
+ x_token_refresh = getattr(scheme, "x_token_refresh", None)
673
+ if x_token_refresh:
674
+ auth_style = x_token_refresh.get("auth_style")
675
+ if auth_style:
676
+ config["auth_style"] = auth_style
677
+
678
+ body_format = x_token_refresh.get("body_format")
679
+ if body_format:
680
+ config["body_format"] = body_format
681
+
682
+ # Extract token_extract fields from x-airbyte-token-extract extension
683
+ x_token_extract = getattr(scheme, "x_airbyte_token_extract", None)
684
+ if x_token_extract:
685
+ config["token_extract"] = x_token_extract
686
+
687
+ # Extract additional_headers from x-airbyte-auth-config extension
688
+ x_auth_config = getattr(scheme, "x_airbyte_auth_config", None)
689
+ if x_auth_config:
690
+ additional_headers = getattr(x_auth_config, "additional_headers", None)
691
+ if additional_headers:
692
+ config["additional_headers"] = additional_headers
693
+
694
+ return config
695
+
696
+
697
+ def _validate_token_extract(spec: OpenAPIConnector) -> None:
698
+ """Validate x-airbyte-token-extract against server variables.
699
+
700
+ Ensures that fields specified in x-airbyte-token-extract match defined
701
+ server variables. This catches configuration errors at load time rather
702
+ than at runtime during token refresh.
703
+
704
+ Args:
705
+ spec: OpenAPI connector specification
706
+
707
+ Raises:
708
+ TokenExtractValidationError: If token_extract fields don't match server variables
709
+ """
710
+ # Get server variables
711
+ server_variables: set[str] = set()
712
+ if spec.servers:
713
+ for server in spec.servers:
714
+ if server.variables:
715
+ server_variables.update(server.variables.keys())
716
+
717
+ # Get token_extract from security scheme
718
+ if not spec.components or not spec.components.security_schemes:
719
+ return
720
+
721
+ for scheme_name, scheme in spec.components.security_schemes.items():
722
+ if scheme.type != "oauth2":
723
+ continue
724
+
725
+ token_extract = getattr(scheme, "x_airbyte_token_extract", None)
726
+ if not token_extract:
727
+ continue
728
+
729
+ # Validate each field matches a server variable
730
+ for field in token_extract:
731
+ if field not in server_variables:
732
+ raise TokenExtractValidationError(
733
+ f"x-airbyte-token-extract field '{field}' does not match any defined "
734
+ f"server variable. Available server variables: {sorted(server_variables) or 'none'}. "
735
+ f"Please define '{{{field}}}' in your server URL and add a variable definition."
736
+ )
737
+
738
+
739
+ def _generate_default_auth_config(auth_type: AuthType) -> AirbyteAuthConfig:
740
+ """Generate default x-airbyte-auth-config for an auth type.
741
+
742
+ When x-airbyte-auth-config is not explicitly defined in the OpenAPI spec,
743
+ we generate a sensible default that maps user-friendly field names to
744
+ the auth scheme's parameters.
745
+
746
+ Args:
747
+ auth_type: The authentication type (BEARER, BASIC, API_KEY)
748
+
749
+ Returns:
750
+ Default auth config spec with properties and auth_mapping
751
+ """
752
+ if auth_type == AuthType.BEARER:
753
+ return AirbyteAuthConfig(
754
+ title=None,
755
+ description=None,
756
+ type="object",
757
+ required=["token"],
758
+ properties={
759
+ "token": AuthConfigFieldSpec(
760
+ type="string",
761
+ title="Bearer Token",
762
+ description="Authentication bearer token",
763
+ format=None,
764
+ pattern=None,
765
+ default=None,
766
+ )
767
+ },
768
+ auth_mapping={"token": "${token}"},
769
+ oneOf=None,
770
+ )
771
+ elif auth_type == AuthType.BASIC:
772
+ return AirbyteAuthConfig(
773
+ title=None,
774
+ description=None,
775
+ type="object",
776
+ required=["username", "password"],
777
+ properties={
778
+ "username": AuthConfigFieldSpec(
779
+ type="string",
780
+ title="Username",
781
+ description="Authentication username",
782
+ format=None,
783
+ pattern=None,
784
+ default=None,
785
+ ),
786
+ "password": AuthConfigFieldSpec(
787
+ type="string",
788
+ title="Password",
789
+ description="Authentication password",
790
+ format=None,
791
+ pattern=None,
792
+ default=None,
793
+ ),
794
+ },
795
+ auth_mapping={"username": "${username}", "password": "${password}"},
796
+ oneOf=None,
797
+ )
798
+ elif auth_type == AuthType.API_KEY:
799
+ return AirbyteAuthConfig(
800
+ title=None,
801
+ description=None,
802
+ type="object",
803
+ required=["api_key"],
804
+ properties={
805
+ "api_key": AuthConfigFieldSpec(
806
+ type="string",
807
+ title="API Key",
808
+ description="API authentication key",
809
+ format=None,
810
+ pattern=None,
811
+ default=None,
812
+ )
813
+ },
814
+ auth_mapping={"api_key": "${api_key}"},
815
+ oneOf=None,
816
+ )
817
+ elif auth_type == AuthType.OAUTH2:
818
+ # OAuth2: No fields are strictly required to support both modes:
819
+ # 1. Full token mode: user provides access_token (and optionally refresh credentials)
820
+ # 2. Refresh-token-only mode: user provides refresh_token, client_id, client_secret
821
+ # The auth_mapping includes all fields, but apply_auth_mapping
822
+ # will skip mappings for fields not provided by the user.
823
+ return AirbyteAuthConfig(
824
+ title=None,
825
+ description=None,
826
+ type="object",
827
+ required=[],
828
+ properties={
829
+ "access_token": AuthConfigFieldSpec(
830
+ type="string",
831
+ title="Access Token",
832
+ description="OAuth2 access token",
833
+ format=None,
834
+ pattern=None,
835
+ default=None,
836
+ ),
837
+ "refresh_token": AuthConfigFieldSpec(
838
+ type="string",
839
+ title="Refresh Token",
840
+ description="OAuth2 refresh token (optional)",
841
+ format=None,
842
+ pattern=None,
843
+ default=None,
844
+ ),
845
+ "client_id": AuthConfigFieldSpec(
846
+ type="string",
847
+ title="Client ID",
848
+ description="OAuth2 client ID (optional)",
849
+ format=None,
850
+ pattern=None,
851
+ default=None,
852
+ ),
853
+ "client_secret": AuthConfigFieldSpec(
854
+ type="string",
855
+ title="Client Secret",
856
+ description="OAuth2 client secret (optional)",
857
+ format=None,
858
+ pattern=None,
859
+ default=None,
860
+ ),
861
+ },
862
+ auth_mapping={
863
+ "access_token": "${access_token}",
864
+ "refresh_token": "${refresh_token}",
865
+ "client_id": "${client_id}",
866
+ "client_secret": "${client_secret}",
867
+ },
868
+ oneOf=None,
869
+ )
870
+ else:
871
+ # Unknown auth type - return minimal config
872
+ return AirbyteAuthConfig(
873
+ title=None,
874
+ description=None,
875
+ type="object",
876
+ required=None,
877
+ properties={},
878
+ auth_mapping={},
879
+ oneOf=None,
880
+ )
881
+
882
+
883
+ def _parse_auth_from_openapi(spec: OpenAPIConnector) -> AuthConfig:
884
+ """Parse authentication configuration from OpenAPI spec.
885
+
886
+ Supports both single and multiple security schemes. For backwards compatibility,
887
+ single-scheme connectors continue to use the legacy AuthConfig format.
888
+ If no security schemes are defined, generates a default Bearer auth config.
889
+
890
+ Args:
891
+ spec: OpenAPI connector specification
892
+
893
+ Returns:
894
+ AuthConfig with either single or multiple auth options
895
+ """
896
+ if not spec.components or not spec.components.security_schemes:
897
+ # Backwards compatibility: generate default Bearer auth when no schemes defined
898
+ default_config = _generate_default_auth_config(AuthType.BEARER)
899
+ return AuthConfig(
900
+ type=AuthType.BEARER,
901
+ config={"header": "Authorization", "prefix": "Bearer"},
902
+ user_config_spec=default_config,
903
+ options=None,
904
+ )
905
+
906
+ schemes = spec.components.security_schemes
907
+
908
+ # Single scheme: backwards compatible mode
909
+ if len(schemes) == 1:
910
+ scheme_name, scheme = next(iter(schemes.items()))
911
+ return _parse_single_security_scheme(scheme)
912
+
913
+ # Multiple schemes: new multi-auth mode
914
+ options = []
915
+ for scheme_name, scheme in schemes.items():
916
+ try:
917
+ auth_option = _parse_security_scheme_to_option(scheme_name, scheme)
918
+ options.append(auth_option)
919
+ except Exception as e:
920
+ # Log warning but continue - skip invalid schemes
921
+ logger = logging.getLogger(__name__)
922
+ logger.warning(f"Skipping invalid security scheme '{scheme_name}': {e}")
923
+ continue
924
+
925
+ if not options:
926
+ raise InvalidOpenAPIError("No valid security schemes found. Connector must define at least one valid security scheme.")
927
+
928
+ return AuthConfig(
929
+ type=None,
930
+ config={},
931
+ user_config_spec=None,
932
+ options=options,
933
+ )
934
+
935
+
936
+ def _parse_single_security_scheme(scheme: Any) -> AuthConfig:
937
+ """Parse a single security scheme into AuthConfig.
938
+
939
+ This extracts the existing single-scheme parsing logic for reuse.
940
+
941
+ Args:
942
+ scheme: SecurityScheme from OpenAPI spec
943
+
944
+ Returns:
945
+ AuthConfig in single-auth mode
946
+ """
947
+ auth_type = AuthType.API_KEY # Default
948
+ auth_config = {}
949
+
950
+ if scheme.type == "http":
951
+ if scheme.scheme == "bearer":
952
+ auth_type = AuthType.BEARER
953
+ auth_config = {"header": "Authorization", "prefix": "Bearer"}
954
+ elif scheme.scheme == "basic":
955
+ auth_type = AuthType.BASIC
956
+ auth_config = {}
957
+
958
+ elif scheme.type == "apiKey":
959
+ auth_type = AuthType.API_KEY
960
+ auth_config = {
961
+ "header": scheme.name or "Authorization",
962
+ "in": scheme.in_ or "header",
963
+ }
964
+
965
+ elif scheme.type == "oauth2":
966
+ # Parse OAuth2 configuration
967
+ oauth2_config = _parse_oauth2_config(scheme)
968
+ # Use explicit x-airbyte-auth-config if present, otherwise generate default
969
+ auth_config_obj = scheme.x_airbyte_auth_config or _generate_default_auth_config(AuthType.OAUTH2)
970
+ # Validate auth_mapping keys if explicitly provided
971
+ if scheme.x_airbyte_auth_config:
972
+ _validate_auth_mapping_keys(AuthType.OAUTH2, scheme.x_airbyte_auth_config)
973
+ return AuthConfig(
974
+ type=AuthType.OAUTH2,
975
+ config=oauth2_config,
976
+ user_config_spec=auth_config_obj,
977
+ options=None,
978
+ )
979
+
980
+ # Use explicit x-airbyte-auth-config if present, otherwise generate default
981
+ auth_config_obj = scheme.x_airbyte_auth_config or _generate_default_auth_config(auth_type)
982
+
983
+ # Validate auth_mapping keys if explicitly provided
984
+ if scheme.x_airbyte_auth_config:
985
+ _validate_auth_mapping_keys(auth_type, scheme.x_airbyte_auth_config)
986
+
987
+ return AuthConfig(
988
+ type=auth_type,
989
+ config=auth_config,
990
+ user_config_spec=auth_config_obj,
991
+ options=None,
992
+ )
993
+
994
+
995
+ def _parse_security_scheme_to_option(scheme_name: str, scheme: Any) -> AuthOption:
996
+ """Parse a security scheme into an AuthOption for multi-auth connectors.
997
+
998
+ Args:
999
+ scheme_name: Name of the security scheme (e.g., "githubOAuth")
1000
+ scheme: SecurityScheme from OpenAPI spec
1001
+
1002
+ Returns:
1003
+ AuthOption containing the parsed configuration
1004
+
1005
+ Raises:
1006
+ ValueError: If scheme is invalid or unsupported
1007
+ """
1008
+ # Parse using existing single-scheme logic
1009
+ single_auth = _parse_single_security_scheme(scheme)
1010
+
1011
+ # Convert to AuthOption
1012
+ return AuthOption(
1013
+ scheme_name=scheme_name,
1014
+ type=single_auth.type,
1015
+ config=single_auth.config,
1016
+ user_config_spec=single_auth.user_config_spec,
1017
+ untested=getattr(scheme, "x_airbyte_untested", False),
1018
+ )
1019
+
1020
+
1021
+ def load_connector_model(definition_path: str | Path) -> ConnectorModel:
1022
+ """Load connector model from YAML definition file.
1023
+
1024
+ Supports both OpenAPI 3.1 format and legacy format.
1025
+
1026
+ Args:
1027
+ definition_path: Path to connector.yaml file
1028
+
1029
+ Returns:
1030
+ Parsed ConnectorModel
1031
+
1032
+ Raises:
1033
+ FileNotFoundError: If definition file doesn't exist
1034
+ ValueError: If YAML is invalid
1035
+ """
1036
+ definition_path = Path(definition_path)
1037
+
1038
+ if not definition_path.exists():
1039
+ raise FileNotFoundError(f"Connector definition not found: {definition_path}")
1040
+
1041
+ # Load YAML with error handling
1042
+ try:
1043
+ with open(definition_path) as f:
1044
+ raw_definition = yaml.safe_load(f)
1045
+ except yaml.YAMLError as e:
1046
+ raise InvalidYAMLError(f"Invalid YAML syntax in {definition_path}: {e}")
1047
+ except Exception as e:
1048
+ raise ConnectorModelLoaderError(f"Error reading definition file {definition_path}: {e}")
1049
+
1050
+ if not raw_definition:
1051
+ raise ValueError("Invalid connector.yaml: empty file")
1052
+
1053
+ # Detect format: OpenAPI if 'openapi' key exists
1054
+ if "openapi" in raw_definition:
1055
+ spec = parse_openapi_spec(raw_definition)
1056
+ return convert_openapi_to_connector_model(spec)
1057
+
1058
+ # Legacy format
1059
+ if "connector" not in raw_definition:
1060
+ raise ValueError("Invalid connector.yaml: missing 'connector' or 'openapi' key")
1061
+
1062
+ # Parse connector metadata
1063
+ connector_meta = raw_definition["connector"]
1064
+
1065
+ # Parse auth config
1066
+ auth_config = raw_definition.get("auth", {})
1067
+
1068
+ # Parse entities
1069
+ entities = []
1070
+ for entity_data in raw_definition.get("entities", []):
1071
+ # Parse endpoints for each action
1072
+ endpoints_dict = {}
1073
+ for action_str in entity_data.get("actions", []):
1074
+ action = Action(action_str)
1075
+ endpoint_data = entity_data["endpoints"].get(action_str)
1076
+
1077
+ if endpoint_data:
1078
+ # Extract path parameters from the path template
1079
+ path_params = extract_path_params(endpoint_data["path"])
1080
+
1081
+ endpoint = EndpointDefinition(
1082
+ method=endpoint_data["method"],
1083
+ path=endpoint_data["path"],
1084
+ description=endpoint_data.get("description"),
1085
+ body_fields=endpoint_data.get("body_fields", []),
1086
+ query_params=endpoint_data.get("query_params", []),
1087
+ path_params=path_params,
1088
+ graphql_body=None, # GraphQL only supported in OpenAPI format (via x-airbyte-body-type)
1089
+ )
1090
+ endpoints_dict[action] = endpoint
1091
+
1092
+ entity = EntityDefinition(
1093
+ name=entity_data["name"],
1094
+ actions=[Action(a) for a in entity_data["actions"]],
1095
+ endpoints=endpoints_dict,
1096
+ schema=entity_data.get("schema"),
1097
+ )
1098
+ entities.append(entity)
1099
+
1100
+ # Get connector ID
1101
+ connector_id_value = connector_meta.get("id")
1102
+ if connector_id_value:
1103
+ # Try to parse as UUID (handles string UUIDs)
1104
+ if isinstance(connector_id_value, str):
1105
+ connector_id = UUID(connector_id_value)
1106
+ else:
1107
+ connector_id = connector_id_value
1108
+ else:
1109
+ raise ValueError
1110
+
1111
+ # Build ConnectorModel
1112
+ model = ConnectorModel(
1113
+ id=connector_id,
1114
+ name=connector_meta["name"],
1115
+ version=connector_meta.get("version", OPENAPI_DEFAULT_VERSION),
1116
+ base_url=raw_definition.get("base_url", connector_meta.get("base_url", "")),
1117
+ auth=auth_config,
1118
+ entities=entities,
1119
+ )
1120
+
1121
+ return model