airbyte-agent-orb 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. airbyte_agent_orb/__init__.py +139 -0
  2. airbyte_agent_orb/_vendored/__init__.py +1 -0
  3. airbyte_agent_orb/_vendored/connector_sdk/__init__.py +82 -0
  4. airbyte_agent_orb/_vendored/connector_sdk/auth_strategies.py +1171 -0
  5. airbyte_agent_orb/_vendored/connector_sdk/auth_template.py +135 -0
  6. airbyte_agent_orb/_vendored/connector_sdk/cloud_utils/__init__.py +5 -0
  7. airbyte_agent_orb/_vendored/connector_sdk/cloud_utils/client.py +338 -0
  8. airbyte_agent_orb/_vendored/connector_sdk/connector_model_loader.py +1121 -0
  9. airbyte_agent_orb/_vendored/connector_sdk/constants.py +78 -0
  10. airbyte_agent_orb/_vendored/connector_sdk/exceptions.py +23 -0
  11. airbyte_agent_orb/_vendored/connector_sdk/executor/__init__.py +31 -0
  12. airbyte_agent_orb/_vendored/connector_sdk/executor/hosted_executor.py +230 -0
  13. airbyte_agent_orb/_vendored/connector_sdk/executor/local_executor.py +1848 -0
  14. airbyte_agent_orb/_vendored/connector_sdk/executor/models.py +202 -0
  15. airbyte_agent_orb/_vendored/connector_sdk/extensions.py +693 -0
  16. airbyte_agent_orb/_vendored/connector_sdk/http/__init__.py +37 -0
  17. airbyte_agent_orb/_vendored/connector_sdk/http/adapters/__init__.py +9 -0
  18. airbyte_agent_orb/_vendored/connector_sdk/http/adapters/httpx_adapter.py +260 -0
  19. airbyte_agent_orb/_vendored/connector_sdk/http/config.py +98 -0
  20. airbyte_agent_orb/_vendored/connector_sdk/http/exceptions.py +119 -0
  21. airbyte_agent_orb/_vendored/connector_sdk/http/protocols.py +114 -0
  22. airbyte_agent_orb/_vendored/connector_sdk/http/response.py +104 -0
  23. airbyte_agent_orb/_vendored/connector_sdk/http_client.py +693 -0
  24. airbyte_agent_orb/_vendored/connector_sdk/introspection.py +481 -0
  25. airbyte_agent_orb/_vendored/connector_sdk/logging/__init__.py +11 -0
  26. airbyte_agent_orb/_vendored/connector_sdk/logging/logger.py +273 -0
  27. airbyte_agent_orb/_vendored/connector_sdk/logging/types.py +93 -0
  28. airbyte_agent_orb/_vendored/connector_sdk/observability/__init__.py +11 -0
  29. airbyte_agent_orb/_vendored/connector_sdk/observability/config.py +179 -0
  30. airbyte_agent_orb/_vendored/connector_sdk/observability/models.py +19 -0
  31. airbyte_agent_orb/_vendored/connector_sdk/observability/redactor.py +81 -0
  32. airbyte_agent_orb/_vendored/connector_sdk/observability/session.py +103 -0
  33. airbyte_agent_orb/_vendored/connector_sdk/performance/__init__.py +6 -0
  34. airbyte_agent_orb/_vendored/connector_sdk/performance/instrumentation.py +57 -0
  35. airbyte_agent_orb/_vendored/connector_sdk/performance/metrics.py +93 -0
  36. airbyte_agent_orb/_vendored/connector_sdk/schema/__init__.py +75 -0
  37. airbyte_agent_orb/_vendored/connector_sdk/schema/base.py +212 -0
  38. airbyte_agent_orb/_vendored/connector_sdk/schema/components.py +244 -0
  39. airbyte_agent_orb/_vendored/connector_sdk/schema/connector.py +120 -0
  40. airbyte_agent_orb/_vendored/connector_sdk/schema/extensions.py +301 -0
  41. airbyte_agent_orb/_vendored/connector_sdk/schema/operations.py +156 -0
  42. airbyte_agent_orb/_vendored/connector_sdk/schema/security.py +241 -0
  43. airbyte_agent_orb/_vendored/connector_sdk/secrets.py +182 -0
  44. airbyte_agent_orb/_vendored/connector_sdk/telemetry/__init__.py +10 -0
  45. airbyte_agent_orb/_vendored/connector_sdk/telemetry/config.py +32 -0
  46. airbyte_agent_orb/_vendored/connector_sdk/telemetry/events.py +59 -0
  47. airbyte_agent_orb/_vendored/connector_sdk/telemetry/tracker.py +155 -0
  48. airbyte_agent_orb/_vendored/connector_sdk/types.py +274 -0
  49. airbyte_agent_orb/_vendored/connector_sdk/utils.py +127 -0
  50. airbyte_agent_orb/_vendored/connector_sdk/validation.py +997 -0
  51. airbyte_agent_orb/_vendored/connector_sdk/validation_replication.py +970 -0
  52. airbyte_agent_orb/connector.py +1179 -0
  53. airbyte_agent_orb/connector_model.py +2163 -0
  54. airbyte_agent_orb/models.py +532 -0
  55. airbyte_agent_orb/types.py +1090 -0
  56. airbyte_agent_orb-0.1.6.dist-info/METADATA +153 -0
  57. airbyte_agent_orb-0.1.6.dist-info/RECORD +58 -0
  58. airbyte_agent_orb-0.1.6.dist-info/WHEEL +4 -0
@@ -0,0 +1,1179 @@
1
+ """
2
+ Orb 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 OrbConnectorModel
18
+ from ._vendored.connector_sdk.introspection import describe_entities, generate_tool_description
19
+ from .types import (
20
+ CustomersGetParams,
21
+ CustomersListParams,
22
+ InvoicesGetParams,
23
+ InvoicesListParams,
24
+ PlansGetParams,
25
+ PlansListParams,
26
+ SubscriptionsGetParams,
27
+ SubscriptionsListParams,
28
+ AirbyteSearchParams,
29
+ CustomersSearchFilter,
30
+ CustomersSearchQuery,
31
+ SubscriptionsSearchFilter,
32
+ SubscriptionsSearchQuery,
33
+ PlansSearchFilter,
34
+ PlansSearchQuery,
35
+ InvoicesSearchFilter,
36
+ InvoicesSearchQuery,
37
+ )
38
+ if TYPE_CHECKING:
39
+ from .models import OrbAuthConfig
40
+
41
+ # Import response models and envelope models at runtime
42
+ from .models import (
43
+ OrbCheckResult,
44
+ OrbExecuteResult,
45
+ OrbExecuteResultWithMeta,
46
+ CustomersListResult,
47
+ SubscriptionsListResult,
48
+ PlansListResult,
49
+ InvoicesListResult,
50
+ Customer,
51
+ Invoice,
52
+ Plan,
53
+ Subscription,
54
+ AirbyteSearchHit,
55
+ AirbyteSearchResult,
56
+ CustomersSearchData,
57
+ CustomersSearchResult,
58
+ SubscriptionsSearchData,
59
+ SubscriptionsSearchResult,
60
+ PlansSearchData,
61
+ PlansSearchResult,
62
+ InvoicesSearchData,
63
+ InvoicesSearchResult,
64
+ )
65
+
66
+ # TypeVar for decorator type preservation
67
+ _F = TypeVar("_F", bound=Callable[..., Any])
68
+
69
+ DEFAULT_MAX_OUTPUT_CHARS = 50_000 # ~50KB default, configurable per-tool
70
+
71
+
72
+ def _raise_output_too_large(message: str) -> None:
73
+ try:
74
+ from pydantic_ai import ModelRetry # type: ignore[import-not-found]
75
+ except Exception as exc:
76
+ raise RuntimeError(message) from exc
77
+ raise ModelRetry(message)
78
+
79
+
80
+ def _check_output_size(result: Any, max_chars: int | None, tool_name: str) -> Any:
81
+ if max_chars is None or max_chars <= 0:
82
+ return result
83
+
84
+ try:
85
+ serialized = json.dumps(result, default=str)
86
+ except (TypeError, ValueError):
87
+ return result
88
+
89
+ if len(serialized) > max_chars:
90
+ truncated_preview = serialized[:500] + "..." if len(serialized) > 500 else serialized
91
+ _raise_output_too_large(
92
+ f"Tool '{tool_name}' output too large ({len(serialized):,} chars, limit {max_chars:,}). "
93
+ "Please narrow your query by: using the 'fields' parameter to select only needed fields, "
94
+ "adding filters, or reducing the 'limit'. "
95
+ f"Preview: {truncated_preview}"
96
+ )
97
+
98
+ return result
99
+
100
+
101
+
102
+
103
+ class OrbConnector:
104
+ """
105
+ Type-safe Orb API connector.
106
+
107
+ Auto-generated from OpenAPI specification with full type safety.
108
+ """
109
+
110
+ connector_name = "orb"
111
+ connector_version = "0.1.1"
112
+ vendored_sdk_version = "0.1.0" # Version of vendored connector-sdk
113
+
114
+ # Map of (entity, action) -> needs_envelope for envelope wrapping decision
115
+ _ENVELOPE_MAP = {
116
+ ("customers", "list"): True,
117
+ ("customers", "get"): None,
118
+ ("subscriptions", "list"): True,
119
+ ("subscriptions", "get"): None,
120
+ ("plans", "list"): True,
121
+ ("plans", "get"): None,
122
+ ("invoices", "list"): True,
123
+ ("invoices", "get"): None,
124
+ }
125
+
126
+ # Map of (entity, action) -> {python_param_name: api_param_name}
127
+ # Used to convert snake_case TypedDict keys to API parameter names in execute()
128
+ _PARAM_MAP = {
129
+ ('customers', 'list'): {'limit': 'limit', 'cursor': 'cursor'},
130
+ ('customers', 'get'): {'customer_id': 'customer_id'},
131
+ ('subscriptions', 'list'): {'limit': 'limit', 'cursor': 'cursor', 'customer_id': 'customer_id', 'external_customer_id': 'external_customer_id', 'status': 'status'},
132
+ ('subscriptions', 'get'): {'subscription_id': 'subscription_id'},
133
+ ('plans', 'list'): {'limit': 'limit', 'cursor': 'cursor'},
134
+ ('plans', 'get'): {'plan_id': 'plan_id'},
135
+ ('invoices', 'list'): {'limit': 'limit', 'cursor': 'cursor', 'customer_id': 'customer_id', 'external_customer_id': 'external_customer_id', 'subscription_id': 'subscription_id', 'invoice_date_gt': 'invoice_date_gt', 'invoice_date_gte': 'invoice_date_gte', 'invoice_date_lt': 'invoice_date_lt', 'invoice_date_lte': 'invoice_date_lte', 'status': 'status'},
136
+ ('invoices', 'get'): {'invoice_id': 'invoice_id'},
137
+ }
138
+
139
+ def __init__(
140
+ self,
141
+ auth_config: OrbAuthConfig | None = None,
142
+ external_user_id: str | None = None,
143
+ airbyte_client_id: str | None = None,
144
+ airbyte_client_secret: str | None = None,
145
+ connector_id: str | None = None,
146
+ on_token_refresh: Any | None = None ):
147
+ """
148
+ Initialize a new orb connector instance.
149
+
150
+ Supports both local and hosted execution modes:
151
+ - Local mode: Provide `auth_config` for direct API calls
152
+ - Hosted mode: Provide Airbyte credentials with either `connector_id` or `external_user_id`
153
+
154
+ Args:
155
+ auth_config: Typed authentication configuration (required for local mode)
156
+ external_user_id: External user ID (for hosted mode lookup)
157
+ airbyte_client_id: Airbyte OAuth client ID (required for hosted mode)
158
+ airbyte_client_secret: Airbyte OAuth client secret (required for hosted mode)
159
+ connector_id: Specific connector/source ID (for hosted mode, skips lookup)
160
+ on_token_refresh: Optional callback for OAuth2 token refresh persistence.
161
+ Called with new_tokens dict when tokens are refreshed. Can be sync or async.
162
+ Example: lambda tokens: save_to_database(tokens)
163
+ Examples:
164
+ # Local mode (direct API calls)
165
+ connector = OrbConnector(auth_config=OrbAuthConfig(api_key="..."))
166
+ # Hosted mode with explicit connector_id (no lookup needed)
167
+ connector = OrbConnector(
168
+ airbyte_client_id="client_abc123",
169
+ airbyte_client_secret="secret_xyz789",
170
+ connector_id="existing-source-uuid"
171
+ )
172
+
173
+ # Hosted mode with lookup by external_user_id
174
+ connector = OrbConnector(
175
+ external_user_id="user-123",
176
+ airbyte_client_id="client_abc123",
177
+ airbyte_client_secret="secret_xyz789"
178
+ )
179
+
180
+ # Local mode with OAuth2 token refresh callback
181
+ def save_tokens(new_tokens: dict) -> None:
182
+ # Persist updated tokens to your storage (file, database, etc.)
183
+ with open("tokens.json", "w") as f:
184
+ json.dump(new_tokens, f)
185
+
186
+ connector = OrbConnector(
187
+ auth_config=OrbAuthConfig(access_token="...", refresh_token="..."),
188
+ on_token_refresh=save_tokens
189
+ )
190
+ """
191
+ # Hosted mode: Airbyte credentials + either connector_id OR external_user_id
192
+ is_hosted = airbyte_client_id and airbyte_client_secret and (connector_id or external_user_id)
193
+
194
+ if is_hosted:
195
+ from ._vendored.connector_sdk.executor import HostedExecutor
196
+ self._executor = HostedExecutor(
197
+ airbyte_client_id=airbyte_client_id,
198
+ airbyte_client_secret=airbyte_client_secret,
199
+ connector_id=connector_id,
200
+ external_user_id=external_user_id,
201
+ connector_definition_id=str(OrbConnectorModel.id) if not connector_id else None,
202
+ )
203
+ else:
204
+ # Local mode: auth_config required
205
+ if not auth_config:
206
+ raise ValueError(
207
+ "Either provide Airbyte credentials (airbyte_client_id, airbyte_client_secret) with "
208
+ "connector_id or external_user_id for hosted mode, or auth_config for local mode"
209
+ )
210
+
211
+ from ._vendored.connector_sdk.executor import LocalExecutor
212
+
213
+ # Build config_values dict from server variables
214
+ config_values = None
215
+
216
+ self._executor = LocalExecutor(
217
+ model=OrbConnectorModel,
218
+ auth_config=auth_config.model_dump() if auth_config else None,
219
+ config_values=config_values,
220
+ on_token_refresh=on_token_refresh
221
+ )
222
+
223
+ # Update base_url with server variables if provided
224
+
225
+ # Initialize entity query objects
226
+ self.customers = CustomersQuery(self)
227
+ self.subscriptions = SubscriptionsQuery(self)
228
+ self.plans = PlansQuery(self)
229
+ self.invoices = InvoicesQuery(self)
230
+
231
+ # ===== TYPED EXECUTE METHOD (Recommended Interface) =====
232
+
233
+ @overload
234
+ async def execute(
235
+ self,
236
+ entity: Literal["customers"],
237
+ action: Literal["list"],
238
+ params: "CustomersListParams"
239
+ ) -> "CustomersListResult": ...
240
+
241
+ @overload
242
+ async def execute(
243
+ self,
244
+ entity: Literal["customers"],
245
+ action: Literal["get"],
246
+ params: "CustomersGetParams"
247
+ ) -> "Customer": ...
248
+
249
+ @overload
250
+ async def execute(
251
+ self,
252
+ entity: Literal["subscriptions"],
253
+ action: Literal["list"],
254
+ params: "SubscriptionsListParams"
255
+ ) -> "SubscriptionsListResult": ...
256
+
257
+ @overload
258
+ async def execute(
259
+ self,
260
+ entity: Literal["subscriptions"],
261
+ action: Literal["get"],
262
+ params: "SubscriptionsGetParams"
263
+ ) -> "Subscription": ...
264
+
265
+ @overload
266
+ async def execute(
267
+ self,
268
+ entity: Literal["plans"],
269
+ action: Literal["list"],
270
+ params: "PlansListParams"
271
+ ) -> "PlansListResult": ...
272
+
273
+ @overload
274
+ async def execute(
275
+ self,
276
+ entity: Literal["plans"],
277
+ action: Literal["get"],
278
+ params: "PlansGetParams"
279
+ ) -> "Plan": ...
280
+
281
+ @overload
282
+ async def execute(
283
+ self,
284
+ entity: Literal["invoices"],
285
+ action: Literal["list"],
286
+ params: "InvoicesListParams"
287
+ ) -> "InvoicesListResult": ...
288
+
289
+ @overload
290
+ async def execute(
291
+ self,
292
+ entity: Literal["invoices"],
293
+ action: Literal["get"],
294
+ params: "InvoicesGetParams"
295
+ ) -> "Invoice": ...
296
+
297
+
298
+ @overload
299
+ async def execute(
300
+ self,
301
+ entity: str,
302
+ action: Literal["list", "get", "search"],
303
+ params: Mapping[str, Any]
304
+ ) -> OrbExecuteResult[Any] | OrbExecuteResultWithMeta[Any, Any] | Any: ...
305
+
306
+ async def execute(
307
+ self,
308
+ entity: str,
309
+ action: Literal["list", "get", "search"],
310
+ params: Mapping[str, Any] | None = None
311
+ ) -> Any:
312
+ """
313
+ Execute an entity operation with full type safety.
314
+
315
+ This is the recommended interface for blessed connectors as it:
316
+ - Uses the same signature as non-blessed connectors
317
+ - Provides full IDE autocomplete for entity/action/params
318
+ - Makes migration from generic to blessed connectors seamless
319
+
320
+ Args:
321
+ entity: Entity name (e.g., "customers")
322
+ action: Operation action (e.g., "create", "get", "list")
323
+ params: Operation parameters (typed based on entity+action)
324
+
325
+ Returns:
326
+ Typed response based on the operation
327
+
328
+ Example:
329
+ customer = await connector.execute(
330
+ entity="customers",
331
+ action="get",
332
+ params={"id": "cus_123"}
333
+ )
334
+ """
335
+ from ._vendored.connector_sdk.executor import ExecutionConfig
336
+
337
+ # Remap parameter names from snake_case (TypedDict keys) to API parameter names
338
+ resolved_params = dict(params) if params is not None else None
339
+ if resolved_params:
340
+ param_map = self._PARAM_MAP.get((entity, action), {})
341
+ if param_map:
342
+ resolved_params = {param_map.get(k, k): v for k, v in resolved_params.items()}
343
+
344
+ # Use ExecutionConfig for both local and hosted executors
345
+ config = ExecutionConfig(
346
+ entity=entity,
347
+ action=action,
348
+ params=resolved_params
349
+ )
350
+
351
+ result = await self._executor.execute(config)
352
+
353
+ if not result.success:
354
+ raise RuntimeError(f"Execution failed: {result.error}")
355
+
356
+ # Check if this operation has extractors configured
357
+ has_extractors = self._ENVELOPE_MAP.get((entity, action), False)
358
+
359
+ if has_extractors:
360
+ # With extractors - return Pydantic envelope with data and meta
361
+ if result.meta is not None:
362
+ return OrbExecuteResultWithMeta[Any, Any](
363
+ data=result.data,
364
+ meta=result.meta
365
+ )
366
+ else:
367
+ return OrbExecuteResult[Any](data=result.data)
368
+ else:
369
+ # No extractors - return raw response data
370
+ return result.data
371
+
372
+ # ===== HEALTH CHECK METHOD =====
373
+
374
+ async def check(self) -> OrbCheckResult:
375
+ """
376
+ Perform a health check to verify connectivity and credentials.
377
+
378
+ Executes a lightweight list operation (limit=1) to validate that
379
+ the connector can communicate with the API and credentials are valid.
380
+
381
+ Returns:
382
+ OrbCheckResult with status ("healthy" or "unhealthy") and optional error message
383
+
384
+ Example:
385
+ result = await connector.check()
386
+ if result.status == "healthy":
387
+ print("Connection verified!")
388
+ else:
389
+ print(f"Check failed: {result.error}")
390
+ """
391
+ result = await self._executor.check()
392
+
393
+ if result.success and isinstance(result.data, dict):
394
+ return OrbCheckResult(
395
+ status=result.data.get("status", "unhealthy"),
396
+ error=result.data.get("error"),
397
+ checked_entity=result.data.get("checked_entity"),
398
+ checked_action=result.data.get("checked_action"),
399
+ )
400
+ else:
401
+ return OrbCheckResult(
402
+ status="unhealthy",
403
+ error=result.error or "Unknown error during health check",
404
+ )
405
+
406
+ # ===== INTROSPECTION METHODS =====
407
+
408
+ @classmethod
409
+ def tool_utils(
410
+ cls,
411
+ func: _F | None = None,
412
+ *,
413
+ update_docstring: bool = True,
414
+ enable_hosted_mode_features: bool = True,
415
+ max_output_chars: int | None = DEFAULT_MAX_OUTPUT_CHARS,
416
+ ) -> _F | Callable[[_F], _F]:
417
+ """
418
+ Decorator that adds tool utilities like docstring augmentation and output limits.
419
+
420
+ Usage:
421
+ @mcp.tool()
422
+ @OrbConnector.tool_utils
423
+ async def execute(entity: str, action: str, params: dict):
424
+ ...
425
+
426
+ @mcp.tool()
427
+ @OrbConnector.tool_utils(update_docstring=False, max_output_chars=None)
428
+ async def execute(entity: str, action: str, params: dict):
429
+ ...
430
+
431
+ Args:
432
+ update_docstring: When True, append connector capabilities to __doc__.
433
+ enable_hosted_mode_features: When False, omit hosted-mode search sections from docstrings.
434
+ max_output_chars: Max serialized output size before raising. Use None to disable.
435
+ """
436
+
437
+ def decorate(inner: _F) -> _F:
438
+ if update_docstring:
439
+ description = generate_tool_description(
440
+ OrbConnectorModel,
441
+ enable_hosted_mode_features=enable_hosted_mode_features,
442
+ )
443
+ original_doc = inner.__doc__ or ""
444
+ if original_doc.strip():
445
+ full_doc = f"{original_doc.strip()}\n{description}"
446
+ else:
447
+ full_doc = description
448
+ else:
449
+ full_doc = ""
450
+
451
+ if inspect.iscoroutinefunction(inner):
452
+
453
+ @wraps(inner)
454
+ async def aw(*args: Any, **kwargs: Any) -> Any:
455
+ result = await inner(*args, **kwargs)
456
+ return _check_output_size(result, max_output_chars, inner.__name__)
457
+
458
+ wrapped = aw
459
+ else:
460
+
461
+ @wraps(inner)
462
+ def sw(*args: Any, **kwargs: Any) -> Any:
463
+ result = inner(*args, **kwargs)
464
+ return _check_output_size(result, max_output_chars, inner.__name__)
465
+
466
+ wrapped = sw
467
+
468
+ if update_docstring:
469
+ wrapped.__doc__ = full_doc
470
+ return wrapped # type: ignore[return-value]
471
+
472
+ if func is not None:
473
+ return decorate(func)
474
+ return decorate
475
+
476
+ def list_entities(self) -> list[dict[str, Any]]:
477
+ """
478
+ Get structured data about available entities, actions, and parameters.
479
+
480
+ Returns a list of entity descriptions with:
481
+ - entity_name: Name of the entity (e.g., "contacts", "deals")
482
+ - description: Entity description from the first endpoint
483
+ - available_actions: List of actions (e.g., ["list", "get", "create"])
484
+ - parameters: Dict mapping action -> list of parameter dicts
485
+
486
+ Example:
487
+ entities = connector.list_entities()
488
+ for entity in entities:
489
+ print(f"{entity['entity_name']}: {entity['available_actions']}")
490
+ """
491
+ return describe_entities(OrbConnectorModel)
492
+
493
+ def entity_schema(self, entity: str) -> dict[str, Any] | None:
494
+ """
495
+ Get the JSON schema for an entity.
496
+
497
+ Args:
498
+ entity: Entity name (e.g., "contacts", "companies")
499
+
500
+ Returns:
501
+ JSON schema dict describing the entity structure, or None if not found.
502
+
503
+ Example:
504
+ schema = connector.entity_schema("contacts")
505
+ if schema:
506
+ print(f"Contact properties: {list(schema.get('properties', {}).keys())}")
507
+ """
508
+ entity_def = next(
509
+ (e for e in OrbConnectorModel.entities if e.name == entity),
510
+ None
511
+ )
512
+ if entity_def is None:
513
+ logging.getLogger(__name__).warning(
514
+ f"Entity '{entity}' not found. Available entities: "
515
+ f"{[e.name for e in OrbConnectorModel.entities]}"
516
+ )
517
+ return entity_def.entity_schema if entity_def else None
518
+
519
+ @property
520
+ def connector_id(self) -> str | None:
521
+ """Get the connector/source ID (only available in hosted mode).
522
+
523
+ Returns:
524
+ The connector ID if in hosted mode, None if in local mode.
525
+
526
+ Example:
527
+ connector = await OrbConnector.create_hosted(...)
528
+ print(f"Created connector: {connector.connector_id}")
529
+ """
530
+ if hasattr(self, '_executor') and hasattr(self._executor, '_connector_id'):
531
+ return self._executor._connector_id
532
+ return None
533
+
534
+ # ===== HOSTED MODE FACTORY =====
535
+
536
+ @classmethod
537
+ async def create_hosted(
538
+ cls,
539
+ *,
540
+ external_user_id: str,
541
+ airbyte_client_id: str,
542
+ airbyte_client_secret: str,
543
+ auth_config: "OrbAuthConfig",
544
+ name: str | None = None,
545
+ replication_config: dict[str, Any] | None = None,
546
+ source_template_id: str | None = None,
547
+ ) -> "OrbConnector":
548
+ """
549
+ Create a new hosted connector on Airbyte Cloud.
550
+
551
+ This factory method:
552
+ 1. Creates a source on Airbyte Cloud with the provided credentials
553
+ 2. Returns a connector configured with the new connector_id
554
+
555
+ Args:
556
+ external_user_id: Workspace identifier in Airbyte Cloud
557
+ airbyte_client_id: Airbyte OAuth client ID
558
+ airbyte_client_secret: Airbyte OAuth client secret
559
+ auth_config: Typed auth config (same as local mode)
560
+ name: Optional source name (defaults to connector name + external_user_id)
561
+ replication_config: Optional replication settings dict.
562
+ Required for connectors with x-airbyte-replication-config (REPLICATION mode sources).
563
+ source_template_id: Source template ID. Required when organization has
564
+ multiple source templates for this connector type.
565
+
566
+ Returns:
567
+ A OrbConnector instance configured in hosted mode
568
+
569
+ Example:
570
+ # Create a new hosted connector with API key auth
571
+ connector = await OrbConnector.create_hosted(
572
+ external_user_id="my-workspace",
573
+ airbyte_client_id="client_abc",
574
+ airbyte_client_secret="secret_xyz",
575
+ auth_config=OrbAuthConfig(api_key="..."),
576
+ )
577
+
578
+ # Use the connector
579
+ result = await connector.execute("entity", "list", {})
580
+ """
581
+
582
+ from ._vendored.connector_sdk.cloud_utils import AirbyteCloudClient
583
+
584
+ client = AirbyteCloudClient(
585
+ client_id=airbyte_client_id,
586
+ client_secret=airbyte_client_secret,
587
+ )
588
+
589
+ try:
590
+ # Build credentials from auth_config (if provided)
591
+ credentials = auth_config.model_dump(exclude_none=True) if auth_config else None
592
+ replication_config_dict = replication_config.model_dump(exclude_none=True) if replication_config else None
593
+
594
+ # Create source on Airbyte Cloud
595
+ source_name = name or f"{cls.connector_name} - {external_user_id}"
596
+ source_id = await client.create_source(
597
+ name=source_name,
598
+ connector_definition_id=str(OrbConnectorModel.id),
599
+ external_user_id=external_user_id,
600
+ credentials=credentials,
601
+ replication_config=replication_config_dict,
602
+ source_template_id=source_template_id,
603
+ )
604
+ finally:
605
+ await client.close()
606
+
607
+ # Return connector configured with the new connector_id
608
+ return cls(
609
+ airbyte_client_id=airbyte_client_id,
610
+ airbyte_client_secret=airbyte_client_secret,
611
+ connector_id=source_id,
612
+ )
613
+
614
+
615
+
616
+
617
+ class CustomersQuery:
618
+ """
619
+ Query class for Customers entity operations.
620
+ """
621
+
622
+ def __init__(self, connector: OrbConnector):
623
+ """Initialize query with connector reference."""
624
+ self._connector = connector
625
+
626
+ async def list(
627
+ self,
628
+ limit: int | None = None,
629
+ cursor: str | None = None,
630
+ **kwargs
631
+ ) -> CustomersListResult:
632
+ """
633
+ Returns a paginated list of customers
634
+
635
+ Args:
636
+ limit: Number of items to return per page
637
+ cursor: Cursor for pagination
638
+ **kwargs: Additional parameters
639
+
640
+ Returns:
641
+ CustomersListResult
642
+ """
643
+ params = {k: v for k, v in {
644
+ "limit": limit,
645
+ "cursor": cursor,
646
+ **kwargs
647
+ }.items() if v is not None}
648
+
649
+ result = await self._connector.execute("customers", "list", params)
650
+ # Cast generic envelope to concrete typed result
651
+ return CustomersListResult(
652
+ data=result.data,
653
+ meta=result.meta
654
+ )
655
+
656
+
657
+
658
+ async def get(
659
+ self,
660
+ customer_id: str,
661
+ **kwargs
662
+ ) -> Customer:
663
+ """
664
+ Get a single customer by ID
665
+
666
+ Args:
667
+ customer_id: Customer ID
668
+ **kwargs: Additional parameters
669
+
670
+ Returns:
671
+ Customer
672
+ """
673
+ params = {k: v for k, v in {
674
+ "customer_id": customer_id,
675
+ **kwargs
676
+ }.items() if v is not None}
677
+
678
+ result = await self._connector.execute("customers", "get", params)
679
+ return result
680
+
681
+
682
+
683
+ async def search(
684
+ self,
685
+ query: CustomersSearchQuery,
686
+ limit: int | None = None,
687
+ cursor: str | None = None,
688
+ fields: list[list[str]] | None = None,
689
+ ) -> CustomersSearchResult:
690
+ """
691
+ Search customers records from Airbyte cache.
692
+
693
+ This operation searches cached data from Airbyte syncs.
694
+ Only available in hosted execution mode.
695
+
696
+ Available filter fields (CustomersSearchFilter):
697
+ - id: The unique identifier of the customer
698
+ - external_customer_id: The ID of the customer in an external system
699
+ - name: The name of the customer
700
+ - email: The email address of the customer
701
+ - created_at: The date and time when the customer was created
702
+ - payment_provider: The payment provider used by the customer
703
+ - payment_provider_id: The ID of the customer in the payment provider's system
704
+ - timezone: The timezone setting of the customer
705
+ - shipping_address: The shipping address of the customer
706
+ - billing_address: The billing address of the customer
707
+ - balance: The current balance of the customer
708
+ - currency: The currency of the customer
709
+ - auto_collection: Whether auto collection is enabled
710
+ - metadata: Additional metadata for the customer
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
+ CustomersSearchResult with hits (list of AirbyteSearchHit[CustomersSearchData]) 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("customers", "search", params)
735
+
736
+ # Parse response into typed result
737
+ return CustomersSearchResult(
738
+ hits=[
739
+ AirbyteSearchHit[CustomersSearchData](
740
+ id=hit.get("id"),
741
+ score=hit.get("score"),
742
+ data=CustomersSearchData(**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 SubscriptionsQuery:
751
+ """
752
+ Query class for Subscriptions entity operations.
753
+ """
754
+
755
+ def __init__(self, connector: OrbConnector):
756
+ """Initialize query with connector reference."""
757
+ self._connector = connector
758
+
759
+ async def list(
760
+ self,
761
+ limit: int | None = None,
762
+ cursor: str | None = None,
763
+ customer_id: str | None = None,
764
+ external_customer_id: str | None = None,
765
+ status: str | None = None,
766
+ **kwargs
767
+ ) -> SubscriptionsListResult:
768
+ """
769
+ Returns a paginated list of subscriptions
770
+
771
+ Args:
772
+ limit: Number of items to return per page
773
+ cursor: Cursor for pagination
774
+ customer_id: Filter subscriptions by customer ID
775
+ external_customer_id: Filter subscriptions by external customer ID
776
+ status: Filter subscriptions by status
777
+ **kwargs: Additional parameters
778
+
779
+ Returns:
780
+ SubscriptionsListResult
781
+ """
782
+ params = {k: v for k, v in {
783
+ "limit": limit,
784
+ "cursor": cursor,
785
+ "customer_id": customer_id,
786
+ "external_customer_id": external_customer_id,
787
+ "status": status,
788
+ **kwargs
789
+ }.items() if v is not None}
790
+
791
+ result = await self._connector.execute("subscriptions", "list", params)
792
+ # Cast generic envelope to concrete typed result
793
+ return SubscriptionsListResult(
794
+ data=result.data,
795
+ meta=result.meta
796
+ )
797
+
798
+
799
+
800
+ async def get(
801
+ self,
802
+ subscription_id: str,
803
+ **kwargs
804
+ ) -> Subscription:
805
+ """
806
+ Get a single subscription by ID
807
+
808
+ Args:
809
+ subscription_id: Subscription ID
810
+ **kwargs: Additional parameters
811
+
812
+ Returns:
813
+ Subscription
814
+ """
815
+ params = {k: v for k, v in {
816
+ "subscription_id": subscription_id,
817
+ **kwargs
818
+ }.items() if v is not None}
819
+
820
+ result = await self._connector.execute("subscriptions", "get", params)
821
+ return result
822
+
823
+
824
+
825
+ async def search(
826
+ self,
827
+ query: SubscriptionsSearchQuery,
828
+ limit: int | None = None,
829
+ cursor: str | None = None,
830
+ fields: list[list[str]] | None = None,
831
+ ) -> SubscriptionsSearchResult:
832
+ """
833
+ Search subscriptions records from Airbyte cache.
834
+
835
+ This operation searches cached data from Airbyte syncs.
836
+ Only available in hosted execution mode.
837
+
838
+ Available filter fields (SubscriptionsSearchFilter):
839
+ - id: The unique identifier of the subscription
840
+ - created_at: The date and time when the subscription was created
841
+ - start_date: The date and time when the subscription starts
842
+ - end_date: The date and time when the subscription ends
843
+ - status: The current status of the subscription
844
+ - customer: The customer associated with the subscription
845
+ - plan: The plan associated with the subscription
846
+ - current_billing_period_start_date: The start date of the current billing period
847
+ - current_billing_period_end_date: The end date of the current billing period
848
+ - auto_collection: Whether auto collection is enabled
849
+ - net_terms: The net terms for the subscription
850
+ - metadata: Additional metadata for the subscription
851
+
852
+ Args:
853
+ query: Filter and sort conditions. Supports operators like eq, neq, gt, gte, lt, lte,
854
+ in, like, fuzzy, keyword, not, and, or. Example: {"filter": {"eq": {"status": "active"}}}
855
+ limit: Maximum results to return (default 1000)
856
+ cursor: Pagination cursor from previous response's next_cursor
857
+ fields: Field paths to include in results. Each path is a list of keys for nested access.
858
+ Example: [["id"], ["user", "name"]] returns id and user.name fields.
859
+
860
+ Returns:
861
+ SubscriptionsSearchResult with hits (list of AirbyteSearchHit[SubscriptionsSearchData]) and pagination info
862
+
863
+ Raises:
864
+ NotImplementedError: If called in local execution mode
865
+ """
866
+ params: dict[str, Any] = {"query": query}
867
+ if limit is not None:
868
+ params["limit"] = limit
869
+ if cursor is not None:
870
+ params["cursor"] = cursor
871
+ if fields is not None:
872
+ params["fields"] = fields
873
+
874
+ result = await self._connector.execute("subscriptions", "search", params)
875
+
876
+ # Parse response into typed result
877
+ return SubscriptionsSearchResult(
878
+ hits=[
879
+ AirbyteSearchHit[SubscriptionsSearchData](
880
+ id=hit.get("id"),
881
+ score=hit.get("score"),
882
+ data=SubscriptionsSearchData(**hit.get("data", {}))
883
+ )
884
+ for hit in result.get("hits", [])
885
+ ],
886
+ next_cursor=result.get("next_cursor"),
887
+ took_ms=result.get("took_ms")
888
+ )
889
+
890
+ class PlansQuery:
891
+ """
892
+ Query class for Plans entity operations.
893
+ """
894
+
895
+ def __init__(self, connector: OrbConnector):
896
+ """Initialize query with connector reference."""
897
+ self._connector = connector
898
+
899
+ async def list(
900
+ self,
901
+ limit: int | None = None,
902
+ cursor: str | None = None,
903
+ **kwargs
904
+ ) -> PlansListResult:
905
+ """
906
+ Returns a paginated list of plans
907
+
908
+ Args:
909
+ limit: Number of items to return per page
910
+ cursor: Cursor for pagination
911
+ **kwargs: Additional parameters
912
+
913
+ Returns:
914
+ PlansListResult
915
+ """
916
+ params = {k: v for k, v in {
917
+ "limit": limit,
918
+ "cursor": cursor,
919
+ **kwargs
920
+ }.items() if v is not None}
921
+
922
+ result = await self._connector.execute("plans", "list", params)
923
+ # Cast generic envelope to concrete typed result
924
+ return PlansListResult(
925
+ data=result.data,
926
+ meta=result.meta
927
+ )
928
+
929
+
930
+
931
+ async def get(
932
+ self,
933
+ plan_id: str,
934
+ **kwargs
935
+ ) -> Plan:
936
+ """
937
+ Get a single plan by ID
938
+
939
+ Args:
940
+ plan_id: Plan ID
941
+ **kwargs: Additional parameters
942
+
943
+ Returns:
944
+ Plan
945
+ """
946
+ params = {k: v for k, v in {
947
+ "plan_id": plan_id,
948
+ **kwargs
949
+ }.items() if v is not None}
950
+
951
+ result = await self._connector.execute("plans", "get", params)
952
+ return result
953
+
954
+
955
+
956
+ async def search(
957
+ self,
958
+ query: PlansSearchQuery,
959
+ limit: int | None = None,
960
+ cursor: str | None = None,
961
+ fields: list[list[str]] | None = None,
962
+ ) -> PlansSearchResult:
963
+ """
964
+ Search plans records from Airbyte cache.
965
+
966
+ This operation searches cached data from Airbyte syncs.
967
+ Only available in hosted execution mode.
968
+
969
+ Available filter fields (PlansSearchFilter):
970
+ - id: The unique identifier of the plan
971
+ - created_at: The date and time when the plan was created
972
+ - name: The name of the plan
973
+ - description: A description of the plan
974
+ - status: The status of the plan
975
+ - currency: The currency of the plan
976
+ - prices: The pricing options for the plan
977
+ - product: The product associated with the plan
978
+ - external_plan_id: The external plan ID
979
+ - metadata: Additional metadata for the plan
980
+
981
+ Args:
982
+ query: Filter and sort conditions. Supports operators like eq, neq, gt, gte, lt, lte,
983
+ in, like, fuzzy, keyword, not, and, or. Example: {"filter": {"eq": {"status": "active"}}}
984
+ limit: Maximum results to return (default 1000)
985
+ cursor: Pagination cursor from previous response's next_cursor
986
+ fields: Field paths to include in results. Each path is a list of keys for nested access.
987
+ Example: [["id"], ["user", "name"]] returns id and user.name fields.
988
+
989
+ Returns:
990
+ PlansSearchResult with hits (list of AirbyteSearchHit[PlansSearchData]) and pagination info
991
+
992
+ Raises:
993
+ NotImplementedError: If called in local execution mode
994
+ """
995
+ params: dict[str, Any] = {"query": query}
996
+ if limit is not None:
997
+ params["limit"] = limit
998
+ if cursor is not None:
999
+ params["cursor"] = cursor
1000
+ if fields is not None:
1001
+ params["fields"] = fields
1002
+
1003
+ result = await self._connector.execute("plans", "search", params)
1004
+
1005
+ # Parse response into typed result
1006
+ return PlansSearchResult(
1007
+ hits=[
1008
+ AirbyteSearchHit[PlansSearchData](
1009
+ id=hit.get("id"),
1010
+ score=hit.get("score"),
1011
+ data=PlansSearchData(**hit.get("data", {}))
1012
+ )
1013
+ for hit in result.get("hits", [])
1014
+ ],
1015
+ next_cursor=result.get("next_cursor"),
1016
+ took_ms=result.get("took_ms")
1017
+ )
1018
+
1019
+ class InvoicesQuery:
1020
+ """
1021
+ Query class for Invoices entity operations.
1022
+ """
1023
+
1024
+ def __init__(self, connector: OrbConnector):
1025
+ """Initialize query with connector reference."""
1026
+ self._connector = connector
1027
+
1028
+ async def list(
1029
+ self,
1030
+ limit: int | None = None,
1031
+ cursor: str | None = None,
1032
+ customer_id: str | None = None,
1033
+ external_customer_id: str | None = None,
1034
+ subscription_id: str | None = None,
1035
+ invoice_date_gt: str | None = None,
1036
+ invoice_date_gte: str | None = None,
1037
+ invoice_date_lt: str | None = None,
1038
+ invoice_date_lte: str | None = None,
1039
+ status: str | None = None,
1040
+ **kwargs
1041
+ ) -> InvoicesListResult:
1042
+ """
1043
+ Returns a paginated list of invoices
1044
+
1045
+ Args:
1046
+ limit: Number of items to return per page
1047
+ cursor: Cursor for pagination
1048
+ customer_id: Filter invoices by customer ID
1049
+ external_customer_id: Filter invoices by external customer ID
1050
+ subscription_id: Filter invoices by subscription ID
1051
+ invoice_date_gt: Filter invoices with invoice date greater than this value (ISO 8601 format)
1052
+ invoice_date_gte: Filter invoices with invoice date greater than or equal to this value (ISO 8601 format)
1053
+ invoice_date_lt: Filter invoices with invoice date less than this value (ISO 8601 format)
1054
+ invoice_date_lte: Filter invoices with invoice date less than or equal to this value (ISO 8601 format)
1055
+ status: Filter invoices by status
1056
+ **kwargs: Additional parameters
1057
+
1058
+ Returns:
1059
+ InvoicesListResult
1060
+ """
1061
+ params = {k: v for k, v in {
1062
+ "limit": limit,
1063
+ "cursor": cursor,
1064
+ "customer_id": customer_id,
1065
+ "external_customer_id": external_customer_id,
1066
+ "subscription_id": subscription_id,
1067
+ "invoice_date_gt": invoice_date_gt,
1068
+ "invoice_date_gte": invoice_date_gte,
1069
+ "invoice_date_lt": invoice_date_lt,
1070
+ "invoice_date_lte": invoice_date_lte,
1071
+ "status": status,
1072
+ **kwargs
1073
+ }.items() if v is not None}
1074
+
1075
+ result = await self._connector.execute("invoices", "list", params)
1076
+ # Cast generic envelope to concrete typed result
1077
+ return InvoicesListResult(
1078
+ data=result.data,
1079
+ meta=result.meta
1080
+ )
1081
+
1082
+
1083
+
1084
+ async def get(
1085
+ self,
1086
+ invoice_id: str,
1087
+ **kwargs
1088
+ ) -> Invoice:
1089
+ """
1090
+ Get a single invoice by ID
1091
+
1092
+ Args:
1093
+ invoice_id: Invoice ID
1094
+ **kwargs: Additional parameters
1095
+
1096
+ Returns:
1097
+ Invoice
1098
+ """
1099
+ params = {k: v for k, v in {
1100
+ "invoice_id": invoice_id,
1101
+ **kwargs
1102
+ }.items() if v is not None}
1103
+
1104
+ result = await self._connector.execute("invoices", "get", params)
1105
+ return result
1106
+
1107
+
1108
+
1109
+ async def search(
1110
+ self,
1111
+ query: InvoicesSearchQuery,
1112
+ limit: int | None = None,
1113
+ cursor: str | None = None,
1114
+ fields: list[list[str]] | None = None,
1115
+ ) -> InvoicesSearchResult:
1116
+ """
1117
+ Search invoices records from Airbyte cache.
1118
+
1119
+ This operation searches cached data from Airbyte syncs.
1120
+ Only available in hosted execution mode.
1121
+
1122
+ Available filter fields (InvoicesSearchFilter):
1123
+ - id: The unique identifier of the invoice
1124
+ - created_at: The date and time when the invoice was created
1125
+ - invoice_date: The date of the invoice
1126
+ - due_date: The due date for the invoice
1127
+ - invoice_pdf: The URL to download the PDF version of the invoice
1128
+ - subtotal: The subtotal amount of the invoice
1129
+ - total: The total amount of the invoice
1130
+ - amount_due: The amount due on the invoice
1131
+ - status: The current status of the invoice
1132
+ - memo: Any additional notes or comments on the invoice
1133
+ - paid_at: The date and time when the invoice was paid
1134
+ - issued_at: The date and time when the invoice was issued
1135
+ - hosted_invoice_url: The URL to view the hosted invoice
1136
+ - line_items: The line items on the invoice
1137
+ - subscription: The subscription associated with the invoice
1138
+ - customer: The customer associated with the invoice
1139
+ - currency: The currency of the invoice
1140
+ - invoice_number: The invoice number
1141
+ - metadata: Additional metadata for the invoice
1142
+
1143
+ Args:
1144
+ query: Filter and sort conditions. Supports operators like eq, neq, gt, gte, lt, lte,
1145
+ in, like, fuzzy, keyword, not, and, or. Example: {"filter": {"eq": {"status": "active"}}}
1146
+ limit: Maximum results to return (default 1000)
1147
+ cursor: Pagination cursor from previous response's next_cursor
1148
+ fields: Field paths to include in results. Each path is a list of keys for nested access.
1149
+ Example: [["id"], ["user", "name"]] returns id and user.name fields.
1150
+
1151
+ Returns:
1152
+ InvoicesSearchResult with hits (list of AirbyteSearchHit[InvoicesSearchData]) and pagination info
1153
+
1154
+ Raises:
1155
+ NotImplementedError: If called in local execution mode
1156
+ """
1157
+ params: dict[str, Any] = {"query": query}
1158
+ if limit is not None:
1159
+ params["limit"] = limit
1160
+ if cursor is not None:
1161
+ params["cursor"] = cursor
1162
+ if fields is not None:
1163
+ params["fields"] = fields
1164
+
1165
+ result = await self._connector.execute("invoices", "search", params)
1166
+
1167
+ # Parse response into typed result
1168
+ return InvoicesSearchResult(
1169
+ hits=[
1170
+ AirbyteSearchHit[InvoicesSearchData](
1171
+ id=hit.get("id"),
1172
+ score=hit.get("score"),
1173
+ data=InvoicesSearchData(**hit.get("data", {}))
1174
+ )
1175
+ for hit in result.get("hits", [])
1176
+ ],
1177
+ next_cursor=result.get("next_cursor"),
1178
+ took_ms=result.get("took_ms")
1179
+ )