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,834 @@
1
+ """
2
+ Airtable connector.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import inspect
8
+ import json
9
+ import logging
10
+ from functools import wraps
11
+ from typing import TYPE_CHECKING, Any, Callable, Mapping, TypeVar, overload
12
+ try:
13
+ from typing import Literal
14
+ except ImportError:
15
+ from typing_extensions import Literal
16
+
17
+ from .connector_model import AirtableConnectorModel
18
+ from ._vendored.connector_sdk.introspection import describe_entities, generate_tool_description
19
+ from .types import (
20
+ BasesListParams,
21
+ RecordsGetParams,
22
+ RecordsListParams,
23
+ TablesListParams,
24
+ AirbyteSearchParams,
25
+ BasesSearchFilter,
26
+ BasesSearchQuery,
27
+ TablesSearchFilter,
28
+ TablesSearchQuery,
29
+ )
30
+ if TYPE_CHECKING:
31
+ from .models import AirtableAuthConfig
32
+
33
+ # Import response models and envelope models at runtime
34
+ from .models import (
35
+ AirtableCheckResult,
36
+ AirtableExecuteResult,
37
+ AirtableExecuteResultWithMeta,
38
+ BasesListResult,
39
+ TablesListResult,
40
+ RecordsListResult,
41
+ Base,
42
+ Record,
43
+ Table,
44
+ AirbyteSearchHit,
45
+ AirbyteSearchResult,
46
+ BasesSearchData,
47
+ BasesSearchResult,
48
+ TablesSearchData,
49
+ TablesSearchResult,
50
+ )
51
+
52
+ # TypeVar for decorator type preservation
53
+ _F = TypeVar("_F", bound=Callable[..., Any])
54
+
55
+ DEFAULT_MAX_OUTPUT_CHARS = 50_000 # ~50KB default, configurable per-tool
56
+
57
+
58
+ def _raise_output_too_large(message: str) -> None:
59
+ try:
60
+ from pydantic_ai import ModelRetry # type: ignore[import-not-found]
61
+ except Exception as exc:
62
+ raise RuntimeError(message) from exc
63
+ raise ModelRetry(message)
64
+
65
+
66
+ def _check_output_size(result: Any, max_chars: int | None, tool_name: str) -> Any:
67
+ if max_chars is None or max_chars <= 0:
68
+ return result
69
+
70
+ try:
71
+ serialized = json.dumps(result, default=str)
72
+ except (TypeError, ValueError):
73
+ return result
74
+
75
+ if len(serialized) > max_chars:
76
+ truncated_preview = serialized[:500] + "..." if len(serialized) > 500 else serialized
77
+ _raise_output_too_large(
78
+ f"Tool '{tool_name}' output too large ({len(serialized):,} chars, limit {max_chars:,}). "
79
+ "Please narrow your query by: using the 'fields' parameter to select only needed fields, "
80
+ "adding filters, or reducing the 'limit'. "
81
+ f"Preview: {truncated_preview}"
82
+ )
83
+
84
+ return result
85
+
86
+
87
+
88
+
89
+ class AirtableConnector:
90
+ """
91
+ Type-safe Airtable API connector.
92
+
93
+ Auto-generated from OpenAPI specification with full type safety.
94
+ """
95
+
96
+ connector_name = "airtable"
97
+ connector_version = "1.0.2"
98
+ vendored_sdk_version = "0.1.0" # Version of vendored connector-sdk
99
+
100
+ # Map of (entity, action) -> needs_envelope for envelope wrapping decision
101
+ _ENVELOPE_MAP = {
102
+ ("bases", "list"): True,
103
+ ("tables", "list"): True,
104
+ ("records", "list"): True,
105
+ ("records", "get"): None,
106
+ }
107
+
108
+ # Map of (entity, action) -> {python_param_name: api_param_name}
109
+ # Used to convert snake_case TypedDict keys to API parameter names in execute()
110
+ _PARAM_MAP = {
111
+ ('bases', 'list'): {'offset': 'offset'},
112
+ ('tables', 'list'): {'base_id': 'base_id'},
113
+ ('records', 'list'): {'base_id': 'base_id', 'table_id_or_name': 'table_id_or_name', 'offset': 'offset', 'page_size': 'pageSize', 'view': 'view', 'filter_by_formula': 'filterByFormula', 'sort': 'sort'},
114
+ ('records', 'get'): {'base_id': 'base_id', 'table_id_or_name': 'table_id_or_name', 'record_id': 'record_id'},
115
+ }
116
+
117
+ def __init__(
118
+ self,
119
+ auth_config: AirtableAuthConfig | None = None,
120
+ external_user_id: str | None = None,
121
+ airbyte_client_id: str | None = None,
122
+ airbyte_client_secret: str | None = None,
123
+ connector_id: str | None = None,
124
+ on_token_refresh: Any | None = None ):
125
+ """
126
+ Initialize a new airtable connector instance.
127
+
128
+ Supports both local and hosted execution modes:
129
+ - Local mode: Provide `auth_config` for direct API calls
130
+ - Hosted mode: Provide Airbyte credentials with either `connector_id` or `external_user_id`
131
+
132
+ Args:
133
+ auth_config: Typed authentication configuration (required for local mode)
134
+ external_user_id: External user ID (for hosted mode lookup)
135
+ airbyte_client_id: Airbyte OAuth client ID (required for hosted mode)
136
+ airbyte_client_secret: Airbyte OAuth client secret (required for hosted mode)
137
+ connector_id: Specific connector/source ID (for hosted mode, skips lookup)
138
+ on_token_refresh: Optional callback for OAuth2 token refresh persistence.
139
+ Called with new_tokens dict when tokens are refreshed. Can be sync or async.
140
+ Example: lambda tokens: save_to_database(tokens)
141
+ Examples:
142
+ # Local mode (direct API calls)
143
+ connector = AirtableConnector(auth_config=AirtableAuthConfig(personal_access_token="..."))
144
+ # Hosted mode with explicit connector_id (no lookup needed)
145
+ connector = AirtableConnector(
146
+ airbyte_client_id="client_abc123",
147
+ airbyte_client_secret="secret_xyz789",
148
+ connector_id="existing-source-uuid"
149
+ )
150
+
151
+ # Hosted mode with lookup by external_user_id
152
+ connector = AirtableConnector(
153
+ external_user_id="user-123",
154
+ airbyte_client_id="client_abc123",
155
+ airbyte_client_secret="secret_xyz789"
156
+ )
157
+
158
+ # Local mode with OAuth2 token refresh callback
159
+ def save_tokens(new_tokens: dict) -> None:
160
+ # Persist updated tokens to your storage (file, database, etc.)
161
+ with open("tokens.json", "w") as f:
162
+ json.dump(new_tokens, f)
163
+
164
+ connector = AirtableConnector(
165
+ auth_config=AirtableAuthConfig(access_token="...", refresh_token="..."),
166
+ on_token_refresh=save_tokens
167
+ )
168
+ """
169
+ # Hosted mode: Airbyte credentials + either connector_id OR external_user_id
170
+ is_hosted = airbyte_client_id and airbyte_client_secret and (connector_id or external_user_id)
171
+
172
+ if is_hosted:
173
+ from ._vendored.connector_sdk.executor import HostedExecutor
174
+ self._executor = HostedExecutor(
175
+ airbyte_client_id=airbyte_client_id,
176
+ airbyte_client_secret=airbyte_client_secret,
177
+ connector_id=connector_id,
178
+ external_user_id=external_user_id,
179
+ connector_definition_id=str(AirtableConnectorModel.id) if not connector_id else None,
180
+ )
181
+ else:
182
+ # Local mode: auth_config required
183
+ if not auth_config:
184
+ raise ValueError(
185
+ "Either provide Airbyte credentials (airbyte_client_id, airbyte_client_secret) with "
186
+ "connector_id or external_user_id for hosted mode, or auth_config for local mode"
187
+ )
188
+
189
+ from ._vendored.connector_sdk.executor import LocalExecutor
190
+
191
+ # Build config_values dict from server variables
192
+ config_values = None
193
+
194
+ self._executor = LocalExecutor(
195
+ model=AirtableConnectorModel,
196
+ auth_config=auth_config.model_dump() if auth_config else None,
197
+ config_values=config_values,
198
+ on_token_refresh=on_token_refresh
199
+ )
200
+
201
+ # Update base_url with server variables if provided
202
+
203
+ # Initialize entity query objects
204
+ self.bases = BasesQuery(self)
205
+ self.tables = TablesQuery(self)
206
+ self.records = RecordsQuery(self)
207
+
208
+ # ===== TYPED EXECUTE METHOD (Recommended Interface) =====
209
+
210
+ @overload
211
+ async def execute(
212
+ self,
213
+ entity: Literal["bases"],
214
+ action: Literal["list"],
215
+ params: "BasesListParams"
216
+ ) -> "BasesListResult": ...
217
+
218
+ @overload
219
+ async def execute(
220
+ self,
221
+ entity: Literal["tables"],
222
+ action: Literal["list"],
223
+ params: "TablesListParams"
224
+ ) -> "TablesListResult": ...
225
+
226
+ @overload
227
+ async def execute(
228
+ self,
229
+ entity: Literal["records"],
230
+ action: Literal["list"],
231
+ params: "RecordsListParams"
232
+ ) -> "RecordsListResult": ...
233
+
234
+ @overload
235
+ async def execute(
236
+ self,
237
+ entity: Literal["records"],
238
+ action: Literal["get"],
239
+ params: "RecordsGetParams"
240
+ ) -> "Record": ...
241
+
242
+
243
+ @overload
244
+ async def execute(
245
+ self,
246
+ entity: str,
247
+ action: Literal["list", "get", "search"],
248
+ params: Mapping[str, Any]
249
+ ) -> AirtableExecuteResult[Any] | AirtableExecuteResultWithMeta[Any, Any] | Any: ...
250
+
251
+ async def execute(
252
+ self,
253
+ entity: str,
254
+ action: Literal["list", "get", "search"],
255
+ params: Mapping[str, Any] | None = None
256
+ ) -> Any:
257
+ """
258
+ Execute an entity operation with full type safety.
259
+
260
+ This is the recommended interface for blessed connectors as it:
261
+ - Uses the same signature as non-blessed connectors
262
+ - Provides full IDE autocomplete for entity/action/params
263
+ - Makes migration from generic to blessed connectors seamless
264
+
265
+ Args:
266
+ entity: Entity name (e.g., "customers")
267
+ action: Operation action (e.g., "create", "get", "list")
268
+ params: Operation parameters (typed based on entity+action)
269
+
270
+ Returns:
271
+ Typed response based on the operation
272
+
273
+ Example:
274
+ customer = await connector.execute(
275
+ entity="customers",
276
+ action="get",
277
+ params={"id": "cus_123"}
278
+ )
279
+ """
280
+ from ._vendored.connector_sdk.executor import ExecutionConfig
281
+
282
+ # Remap parameter names from snake_case (TypedDict keys) to API parameter names
283
+ resolved_params = dict(params) if params is not None else None
284
+ if resolved_params:
285
+ param_map = self._PARAM_MAP.get((entity, action), {})
286
+ if param_map:
287
+ resolved_params = {param_map.get(k, k): v for k, v in resolved_params.items()}
288
+
289
+ # Use ExecutionConfig for both local and hosted executors
290
+ config = ExecutionConfig(
291
+ entity=entity,
292
+ action=action,
293
+ params=resolved_params
294
+ )
295
+
296
+ result = await self._executor.execute(config)
297
+
298
+ if not result.success:
299
+ raise RuntimeError(f"Execution failed: {result.error}")
300
+
301
+ # Check if this operation has extractors configured
302
+ has_extractors = self._ENVELOPE_MAP.get((entity, action), False)
303
+
304
+ if has_extractors:
305
+ # With extractors - return Pydantic envelope with data and meta
306
+ if result.meta is not None:
307
+ return AirtableExecuteResultWithMeta[Any, Any](
308
+ data=result.data,
309
+ meta=result.meta
310
+ )
311
+ else:
312
+ return AirtableExecuteResult[Any](data=result.data)
313
+ else:
314
+ # No extractors - return raw response data
315
+ return result.data
316
+
317
+ # ===== HEALTH CHECK METHOD =====
318
+
319
+ async def check(self) -> AirtableCheckResult:
320
+ """
321
+ Perform a health check to verify connectivity and credentials.
322
+
323
+ Executes a lightweight list operation (limit=1) to validate that
324
+ the connector can communicate with the API and credentials are valid.
325
+
326
+ Returns:
327
+ AirtableCheckResult with status ("healthy" or "unhealthy") and optional error message
328
+
329
+ Example:
330
+ result = await connector.check()
331
+ if result.status == "healthy":
332
+ print("Connection verified!")
333
+ else:
334
+ print(f"Check failed: {result.error}")
335
+ """
336
+ result = await self._executor.check()
337
+
338
+ if result.success and isinstance(result.data, dict):
339
+ return AirtableCheckResult(
340
+ status=result.data.get("status", "unhealthy"),
341
+ error=result.data.get("error"),
342
+ checked_entity=result.data.get("checked_entity"),
343
+ checked_action=result.data.get("checked_action"),
344
+ )
345
+ else:
346
+ return AirtableCheckResult(
347
+ status="unhealthy",
348
+ error=result.error or "Unknown error during health check",
349
+ )
350
+
351
+ # ===== INTROSPECTION METHODS =====
352
+
353
+ @classmethod
354
+ def tool_utils(
355
+ cls,
356
+ func: _F | None = None,
357
+ *,
358
+ update_docstring: bool = True,
359
+ enable_hosted_mode_features: bool = True,
360
+ max_output_chars: int | None = DEFAULT_MAX_OUTPUT_CHARS,
361
+ ) -> _F | Callable[[_F], _F]:
362
+ """
363
+ Decorator that adds tool utilities like docstring augmentation and output limits.
364
+
365
+ Usage:
366
+ @mcp.tool()
367
+ @AirtableConnector.tool_utils
368
+ async def execute(entity: str, action: str, params: dict):
369
+ ...
370
+
371
+ @mcp.tool()
372
+ @AirtableConnector.tool_utils(update_docstring=False, max_output_chars=None)
373
+ async def execute(entity: str, action: str, params: dict):
374
+ ...
375
+
376
+ Args:
377
+ update_docstring: When True, append connector capabilities to __doc__.
378
+ enable_hosted_mode_features: When False, omit hosted-mode search sections from docstrings.
379
+ max_output_chars: Max serialized output size before raising. Use None to disable.
380
+ """
381
+
382
+ def decorate(inner: _F) -> _F:
383
+ if update_docstring:
384
+ description = generate_tool_description(
385
+ AirtableConnectorModel,
386
+ enable_hosted_mode_features=enable_hosted_mode_features,
387
+ )
388
+ original_doc = inner.__doc__ or ""
389
+ if original_doc.strip():
390
+ full_doc = f"{original_doc.strip()}\n{description}"
391
+ else:
392
+ full_doc = description
393
+ else:
394
+ full_doc = ""
395
+
396
+ if inspect.iscoroutinefunction(inner):
397
+
398
+ @wraps(inner)
399
+ async def aw(*args: Any, **kwargs: Any) -> Any:
400
+ result = await inner(*args, **kwargs)
401
+ return _check_output_size(result, max_output_chars, inner.__name__)
402
+
403
+ wrapped = aw
404
+ else:
405
+
406
+ @wraps(inner)
407
+ def sw(*args: Any, **kwargs: Any) -> Any:
408
+ result = inner(*args, **kwargs)
409
+ return _check_output_size(result, max_output_chars, inner.__name__)
410
+
411
+ wrapped = sw
412
+
413
+ if update_docstring:
414
+ wrapped.__doc__ = full_doc
415
+ return wrapped # type: ignore[return-value]
416
+
417
+ if func is not None:
418
+ return decorate(func)
419
+ return decorate
420
+
421
+ def list_entities(self) -> list[dict[str, Any]]:
422
+ """
423
+ Get structured data about available entities, actions, and parameters.
424
+
425
+ Returns a list of entity descriptions with:
426
+ - entity_name: Name of the entity (e.g., "contacts", "deals")
427
+ - description: Entity description from the first endpoint
428
+ - available_actions: List of actions (e.g., ["list", "get", "create"])
429
+ - parameters: Dict mapping action -> list of parameter dicts
430
+
431
+ Example:
432
+ entities = connector.list_entities()
433
+ for entity in entities:
434
+ print(f"{entity['entity_name']}: {entity['available_actions']}")
435
+ """
436
+ return describe_entities(AirtableConnectorModel)
437
+
438
+ def entity_schema(self, entity: str) -> dict[str, Any] | None:
439
+ """
440
+ Get the JSON schema for an entity.
441
+
442
+ Args:
443
+ entity: Entity name (e.g., "contacts", "companies")
444
+
445
+ Returns:
446
+ JSON schema dict describing the entity structure, or None if not found.
447
+
448
+ Example:
449
+ schema = connector.entity_schema("contacts")
450
+ if schema:
451
+ print(f"Contact properties: {list(schema.get('properties', {}).keys())}")
452
+ """
453
+ entity_def = next(
454
+ (e for e in AirtableConnectorModel.entities if e.name == entity),
455
+ None
456
+ )
457
+ if entity_def is None:
458
+ logging.getLogger(__name__).warning(
459
+ f"Entity '{entity}' not found. Available entities: "
460
+ f"{[e.name for e in AirtableConnectorModel.entities]}"
461
+ )
462
+ return entity_def.entity_schema if entity_def else None
463
+
464
+ @property
465
+ def connector_id(self) -> str | None:
466
+ """Get the connector/source ID (only available in hosted mode).
467
+
468
+ Returns:
469
+ The connector ID if in hosted mode, None if in local mode.
470
+
471
+ Example:
472
+ connector = await AirtableConnector.create_hosted(...)
473
+ print(f"Created connector: {connector.connector_id}")
474
+ """
475
+ if hasattr(self, '_executor') and hasattr(self._executor, '_connector_id'):
476
+ return self._executor._connector_id
477
+ return None
478
+
479
+ # ===== HOSTED MODE FACTORY =====
480
+
481
+ @classmethod
482
+ async def create_hosted(
483
+ cls,
484
+ *,
485
+ external_user_id: str,
486
+ airbyte_client_id: str,
487
+ airbyte_client_secret: str,
488
+ auth_config: "AirtableAuthConfig",
489
+ name: str | None = None,
490
+ replication_config: dict[str, Any] | None = None,
491
+ source_template_id: str | None = None,
492
+ ) -> "AirtableConnector":
493
+ """
494
+ Create a new hosted connector on Airbyte Cloud.
495
+
496
+ This factory method:
497
+ 1. Creates a source on Airbyte Cloud with the provided credentials
498
+ 2. Returns a connector configured with the new connector_id
499
+
500
+ Args:
501
+ external_user_id: Workspace identifier in Airbyte Cloud
502
+ airbyte_client_id: Airbyte OAuth client ID
503
+ airbyte_client_secret: Airbyte OAuth client secret
504
+ auth_config: Typed auth config (same as local mode)
505
+ name: Optional source name (defaults to connector name + external_user_id)
506
+ replication_config: Optional replication settings dict.
507
+ Required for connectors with x-airbyte-replication-config (REPLICATION mode sources).
508
+ source_template_id: Source template ID. Required when organization has
509
+ multiple source templates for this connector type.
510
+
511
+ Returns:
512
+ A AirtableConnector instance configured in hosted mode
513
+
514
+ Example:
515
+ # Create a new hosted connector with API key auth
516
+ connector = await AirtableConnector.create_hosted(
517
+ external_user_id="my-workspace",
518
+ airbyte_client_id="client_abc",
519
+ airbyte_client_secret="secret_xyz",
520
+ auth_config=AirtableAuthConfig(personal_access_token="..."),
521
+ )
522
+
523
+ # Use the connector
524
+ result = await connector.execute("entity", "list", {})
525
+ """
526
+
527
+ from ._vendored.connector_sdk.cloud_utils import AirbyteCloudClient
528
+
529
+ client = AirbyteCloudClient(
530
+ client_id=airbyte_client_id,
531
+ client_secret=airbyte_client_secret,
532
+ )
533
+
534
+ try:
535
+ # Build credentials from auth_config (if provided)
536
+ credentials = auth_config.model_dump(exclude_none=True) if auth_config else None
537
+ replication_config_dict = replication_config.model_dump(exclude_none=True) if replication_config else None
538
+
539
+ # Create source on Airbyte Cloud
540
+ source_name = name or f"{cls.connector_name} - {external_user_id}"
541
+ source_id = await client.create_source(
542
+ name=source_name,
543
+ connector_definition_id=str(AirtableConnectorModel.id),
544
+ external_user_id=external_user_id,
545
+ credentials=credentials,
546
+ replication_config=replication_config_dict,
547
+ source_template_id=source_template_id,
548
+ )
549
+ finally:
550
+ await client.close()
551
+
552
+ # Return connector configured with the new connector_id
553
+ return cls(
554
+ airbyte_client_id=airbyte_client_id,
555
+ airbyte_client_secret=airbyte_client_secret,
556
+ connector_id=source_id,
557
+ )
558
+
559
+
560
+
561
+
562
+ class BasesQuery:
563
+ """
564
+ Query class for Bases entity operations.
565
+ """
566
+
567
+ def __init__(self, connector: AirtableConnector):
568
+ """Initialize query with connector reference."""
569
+ self._connector = connector
570
+
571
+ async def list(
572
+ self,
573
+ offset: str | None = None,
574
+ **kwargs
575
+ ) -> BasesListResult:
576
+ """
577
+ Returns a list of all bases the user has access to
578
+
579
+ Args:
580
+ offset: Pagination offset from previous response
581
+ **kwargs: Additional parameters
582
+
583
+ Returns:
584
+ BasesListResult
585
+ """
586
+ params = {k: v for k, v in {
587
+ "offset": offset,
588
+ **kwargs
589
+ }.items() if v is not None}
590
+
591
+ result = await self._connector.execute("bases", "list", params)
592
+ # Cast generic envelope to concrete typed result
593
+ return BasesListResult(
594
+ data=result.data
595
+ )
596
+
597
+
598
+
599
+ async def search(
600
+ self,
601
+ query: BasesSearchQuery,
602
+ limit: int | None = None,
603
+ cursor: str | None = None,
604
+ fields: list[list[str]] | None = None,
605
+ ) -> BasesSearchResult:
606
+ """
607
+ Search bases records from Airbyte cache.
608
+
609
+ This operation searches cached data from Airbyte syncs.
610
+ Only available in hosted execution mode.
611
+
612
+ Available filter fields (BasesSearchFilter):
613
+ - id: Unique identifier for the base
614
+ - name: Name of the base
615
+ - permission_level: Permission level for the base
616
+
617
+ Args:
618
+ query: Filter and sort conditions. Supports operators like eq, neq, gt, gte, lt, lte,
619
+ in, like, fuzzy, keyword, not, and, or. Example: {"filter": {"eq": {"status": "active"}}}
620
+ limit: Maximum results to return (default 1000)
621
+ cursor: Pagination cursor from previous response's next_cursor
622
+ fields: Field paths to include in results. Each path is a list of keys for nested access.
623
+ Example: [["id"], ["user", "name"]] returns id and user.name fields.
624
+
625
+ Returns:
626
+ BasesSearchResult with hits (list of AirbyteSearchHit[BasesSearchData]) and pagination info
627
+
628
+ Raises:
629
+ NotImplementedError: If called in local execution mode
630
+ """
631
+ params: dict[str, Any] = {"query": query}
632
+ if limit is not None:
633
+ params["limit"] = limit
634
+ if cursor is not None:
635
+ params["cursor"] = cursor
636
+ if fields is not None:
637
+ params["fields"] = fields
638
+
639
+ result = await self._connector.execute("bases", "search", params)
640
+
641
+ # Parse response into typed result
642
+ return BasesSearchResult(
643
+ hits=[
644
+ AirbyteSearchHit[BasesSearchData](
645
+ id=hit.get("id"),
646
+ score=hit.get("score"),
647
+ data=BasesSearchData(**hit.get("data", {}))
648
+ )
649
+ for hit in result.get("hits", [])
650
+ ],
651
+ next_cursor=result.get("next_cursor"),
652
+ took_ms=result.get("took_ms")
653
+ )
654
+
655
+ class TablesQuery:
656
+ """
657
+ Query class for Tables entity operations.
658
+ """
659
+
660
+ def __init__(self, connector: AirtableConnector):
661
+ """Initialize query with connector reference."""
662
+ self._connector = connector
663
+
664
+ async def list(
665
+ self,
666
+ base_id: str,
667
+ **kwargs
668
+ ) -> TablesListResult:
669
+ """
670
+ Returns a list of all tables in the specified base with their schema information
671
+
672
+ Args:
673
+ base_id: The ID of the base
674
+ **kwargs: Additional parameters
675
+
676
+ Returns:
677
+ TablesListResult
678
+ """
679
+ params = {k: v for k, v in {
680
+ "base_id": base_id,
681
+ **kwargs
682
+ }.items() if v is not None}
683
+
684
+ result = await self._connector.execute("tables", "list", params)
685
+ # Cast generic envelope to concrete typed result
686
+ return TablesListResult(
687
+ data=result.data
688
+ )
689
+
690
+
691
+
692
+ async def search(
693
+ self,
694
+ query: TablesSearchQuery,
695
+ limit: int | None = None,
696
+ cursor: str | None = None,
697
+ fields: list[list[str]] | None = None,
698
+ ) -> TablesSearchResult:
699
+ """
700
+ Search tables records from Airbyte cache.
701
+
702
+ This operation searches cached data from Airbyte syncs.
703
+ Only available in hosted execution mode.
704
+
705
+ Available filter fields (TablesSearchFilter):
706
+ - id: Unique identifier for the table
707
+ - name: Name of the table
708
+ - primary_field_id: ID of the primary field
709
+ - fields: List of fields in the table
710
+ - views: List of views in the table
711
+
712
+ Args:
713
+ query: Filter and sort conditions. Supports operators like eq, neq, gt, gte, lt, lte,
714
+ in, like, fuzzy, keyword, not, and, or. Example: {"filter": {"eq": {"status": "active"}}}
715
+ limit: Maximum results to return (default 1000)
716
+ cursor: Pagination cursor from previous response's next_cursor
717
+ fields: Field paths to include in results. Each path is a list of keys for nested access.
718
+ Example: [["id"], ["user", "name"]] returns id and user.name fields.
719
+
720
+ Returns:
721
+ TablesSearchResult with hits (list of AirbyteSearchHit[TablesSearchData]) and pagination info
722
+
723
+ Raises:
724
+ NotImplementedError: If called in local execution mode
725
+ """
726
+ params: dict[str, Any] = {"query": query}
727
+ if limit is not None:
728
+ params["limit"] = limit
729
+ if cursor is not None:
730
+ params["cursor"] = cursor
731
+ if fields is not None:
732
+ params["fields"] = fields
733
+
734
+ result = await self._connector.execute("tables", "search", params)
735
+
736
+ # Parse response into typed result
737
+ return TablesSearchResult(
738
+ hits=[
739
+ AirbyteSearchHit[TablesSearchData](
740
+ id=hit.get("id"),
741
+ score=hit.get("score"),
742
+ data=TablesSearchData(**hit.get("data", {}))
743
+ )
744
+ for hit in result.get("hits", [])
745
+ ],
746
+ next_cursor=result.get("next_cursor"),
747
+ took_ms=result.get("took_ms")
748
+ )
749
+
750
+ class RecordsQuery:
751
+ """
752
+ Query class for Records entity operations.
753
+ """
754
+
755
+ def __init__(self, connector: AirtableConnector):
756
+ """Initialize query with connector reference."""
757
+ self._connector = connector
758
+
759
+ async def list(
760
+ self,
761
+ base_id: str,
762
+ table_id_or_name: str,
763
+ offset: str | None = None,
764
+ page_size: int | None = None,
765
+ view: str | None = None,
766
+ filter_by_formula: str | None = None,
767
+ sort: str | None = None,
768
+ **kwargs
769
+ ) -> RecordsListResult:
770
+ """
771
+ Returns a paginated list of records from the specified table
772
+
773
+ Args:
774
+ base_id: The ID of the base
775
+ table_id_or_name: The ID or name of the table
776
+ offset: Pagination offset from previous response
777
+ page_size: Number of records per page (max 100)
778
+ view: Name or ID of a view to filter records
779
+ filter_by_formula: Airtable formula to filter records
780
+ sort: Sort configuration as JSON array
781
+ **kwargs: Additional parameters
782
+
783
+ Returns:
784
+ RecordsListResult
785
+ """
786
+ params = {k: v for k, v in {
787
+ "base_id": base_id,
788
+ "table_id_or_name": table_id_or_name,
789
+ "offset": offset,
790
+ "pageSize": page_size,
791
+ "view": view,
792
+ "filterByFormula": filter_by_formula,
793
+ "sort": sort,
794
+ **kwargs
795
+ }.items() if v is not None}
796
+
797
+ result = await self._connector.execute("records", "list", params)
798
+ # Cast generic envelope to concrete typed result
799
+ return RecordsListResult(
800
+ data=result.data
801
+ )
802
+
803
+
804
+
805
+ async def get(
806
+ self,
807
+ base_id: str,
808
+ table_id_or_name: str,
809
+ record_id: str,
810
+ **kwargs
811
+ ) -> Record:
812
+ """
813
+ Returns a single record by ID from the specified table
814
+
815
+ Args:
816
+ base_id: The ID of the base
817
+ table_id_or_name: The ID or name of the table
818
+ record_id: The ID of the record
819
+ **kwargs: Additional parameters
820
+
821
+ Returns:
822
+ Record
823
+ """
824
+ params = {k: v for k, v in {
825
+ "base_id": base_id,
826
+ "table_id_or_name": table_id_or_name,
827
+ "record_id": record_id,
828
+ **kwargs
829
+ }.items() if v is not None}
830
+
831
+ result = await self._connector.execute("records", "get", params)
832
+ return result
833
+
834
+