Skip to content

Commit 8a2b207

Browse files
feat(cloud): add CloudClient, move CloudOrganization to dedicated cloud.organizations module (#1030)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 70a7d82 commit 8a2b207

9 files changed

Lines changed: 1007 additions & 340 deletions

File tree

airbyte/_util/api_imports.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
ConnectionResponse,
2626
DestinationResponse,
2727
JobResponse,
28+
WorkspaceResponse,
2829
)
2930

3031
# Public-Use Classes
@@ -39,4 +40,5 @@
3940
"DestinationResponse",
4041
"JobResponse",
4142
"JobStatusEnum",
43+
"WorkspaceResponse",
4244
]

airbyte/cloud/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,11 @@
8484

8585
from typing import TYPE_CHECKING
8686

87+
from airbyte.cloud.client import CloudClient
8788
from airbyte.cloud.client_config import CloudClientConfig
8889
from airbyte.cloud.connections import CloudConnection
8990
from airbyte.cloud.constants import JobStatusEnum
91+
from airbyte.cloud.organizations import CloudOrganization
9092
from airbyte.cloud.sync_results import SyncResult
9193
from airbyte.cloud.workspaces import CloudWorkspace
9294

@@ -95,9 +97,11 @@
9597
if TYPE_CHECKING:
9698
# ruff: noqa: TC004
9799
from airbyte.cloud import (
100+
client,
98101
client_config,
99102
connections,
100103
constants,
104+
organizations,
101105
sync_results,
102106
workspaces,
103107
)
@@ -106,11 +110,15 @@
106110
__all__ = [
107111
# Submodules
108112
"workspaces",
113+
"client",
114+
"organizations",
109115
"connections",
110116
"constants",
111117
"client_config",
112118
"sync_results",
113119
# Classes
120+
"CloudClient",
121+
"CloudOrganization",
114122
"CloudWorkspace",
115123
"CloudConnection",
116124
"CloudClientConfig",

airbyte/cloud/_credentials.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2+
"""Internal credential resolution for Airbyte Cloud authentication."""
3+
4+
from __future__ import annotations
5+
6+
from dataclasses import dataclass, replace
7+
8+
from airbyte.constants import (
9+
CLOUD_API_ROOT,
10+
CLOUD_API_ROOT_ENV_VAR,
11+
CLOUD_BEARER_TOKEN_ENV_VAR,
12+
CLOUD_CLIENT_ID_ENV_VAR,
13+
CLOUD_CLIENT_SECRET_ENV_VAR,
14+
CLOUD_CONFIG_API_ROOT_ENV_VAR,
15+
CLOUD_ORGANIZATION_ID_ENV_VAR,
16+
CLOUD_WORKSPACE_ID_ENV_VAR,
17+
)
18+
from airbyte.exceptions import PyAirbyteInputError
19+
from airbyte.secrets.base import SecretString
20+
from airbyte.secrets.util import try_get_secret
21+
22+
23+
CLIENT_ID_ENV_VAR = "AIRBYTE_CLIENT_ID"
24+
CLIENT_SECRET_ENV_VAR = "AIRBYTE_CLIENT_SECRET"
25+
WORKSPACE_ID_ENV_VAR = "AIRBYTE_WORKSPACE_ID"
26+
ORGANIZATION_ID_ENV_VAR = "AIRBYTE_ORGANIZATION_ID"
27+
PUBLIC_API_ROOT_ENV_VAR = "AIRBYTE_API_ROOT"
28+
BEARER_TOKEN_ENV_VAR = "AIRBYTE_BEARER_TOKEN"
29+
CONFIG_API_ROOT_ENV_VAR = "AIRBYTE_CONFIG_API_ROOT"
30+
31+
32+
@dataclass(frozen=True)
33+
class _AirbyteCredentials:
34+
"""Resolved credentials and API roots for Airbyte control-plane APIs."""
35+
36+
client_id: SecretString | None
37+
client_secret: SecretString | None
38+
bearer_token: SecretString | None
39+
public_api_root: str
40+
config_api_root: str | None
41+
workspace_id: str | None = None
42+
organization_id: str | None = None
43+
44+
@classmethod
45+
def from_auth(
46+
cls,
47+
*,
48+
workspace_id: str | None = None,
49+
organization_id: str | None = None,
50+
client_id: str | SecretString | None = None,
51+
client_secret: str | SecretString | None = None,
52+
bearer_token: str | SecretString | None = None,
53+
public_api_root: str | None = None,
54+
config_api_root: str | None = None,
55+
env_vars: bool = True,
56+
) -> _AirbyteCredentials:
57+
"""Resolve Airbyte Cloud credentials from inputs and optionally env vars.
58+
59+
When `env_vars` is True (default), environment variables are checked as a
60+
fallback after explicit inputs.
61+
"""
62+
resolved_bearer_token = _first_value(
63+
str(bearer_token) if bearer_token is not None else None,
64+
_env_value(BEARER_TOKEN_ENV_VAR, CLOUD_BEARER_TOKEN_ENV_VAR) if env_vars else None,
65+
)
66+
resolved_client_id = _first_value(
67+
str(client_id) if client_id is not None else None,
68+
_env_value(CLIENT_ID_ENV_VAR, CLOUD_CLIENT_ID_ENV_VAR) if env_vars else None,
69+
)
70+
resolved_client_secret = _first_value(
71+
str(client_secret) if client_secret is not None else None,
72+
_env_value(CLIENT_SECRET_ENV_VAR, CLOUD_CLIENT_SECRET_ENV_VAR) if env_vars else None,
73+
)
74+
75+
if resolved_bearer_token and (resolved_client_id or resolved_client_secret):
76+
raise PyAirbyteInputError(
77+
message="Cannot use both client credentials and bearer token authentication.",
78+
guidance=(
79+
"Provide either client_id and client_secret together, "
80+
"or bearer_token alone, but not both."
81+
),
82+
)
83+
if bool(resolved_client_id) != bool(resolved_client_secret):
84+
raise PyAirbyteInputError(
85+
message="Client ID and client secret are both required.",
86+
guidance="Provide both client ID and client secret, or use a bearer token.",
87+
)
88+
if not resolved_bearer_token and not resolved_client_id:
89+
guidance = (
90+
"Set Airbyte Cloud credentials in environment variables."
91+
if env_vars
92+
else "Provide either bearer_token or both client_id and client_secret."
93+
)
94+
raise PyAirbyteInputError(
95+
message="No Airbyte credentials found.",
96+
guidance=guidance,
97+
)
98+
99+
return cls(
100+
client_id=SecretString(resolved_client_id) if resolved_client_id else None,
101+
client_secret=SecretString(resolved_client_secret) if resolved_client_secret else None,
102+
bearer_token=SecretString(resolved_bearer_token) if resolved_bearer_token else None,
103+
public_api_root=_first_value(
104+
public_api_root,
105+
_env_value(PUBLIC_API_ROOT_ENV_VAR, CLOUD_API_ROOT_ENV_VAR) if env_vars else None,
106+
)
107+
or CLOUD_API_ROOT,
108+
config_api_root=_first_value(
109+
config_api_root,
110+
_env_value(CONFIG_API_ROOT_ENV_VAR, CLOUD_CONFIG_API_ROOT_ENV_VAR)
111+
if env_vars
112+
else None,
113+
),
114+
workspace_id=_first_value(
115+
workspace_id,
116+
_env_value(WORKSPACE_ID_ENV_VAR, CLOUD_WORKSPACE_ID_ENV_VAR) if env_vars else None,
117+
),
118+
organization_id=_first_value(
119+
organization_id,
120+
_env_value(ORGANIZATION_ID_ENV_VAR, CLOUD_ORGANIZATION_ID_ENV_VAR)
121+
if env_vars
122+
else None,
123+
),
124+
)
125+
126+
def with_workspace_id(self, workspace_id: str | None) -> _AirbyteCredentials:
127+
"""Return credentials scoped to a workspace."""
128+
return replace(self, workspace_id=workspace_id)
129+
130+
def with_organization_id(self, organization_id: str | None) -> _AirbyteCredentials:
131+
"""Return credentials scoped to an organization."""
132+
return replace(self, organization_id=organization_id)
133+
134+
135+
def _first_value(*values: str | None) -> str | None:
136+
"""Return the first non-empty string value."""
137+
for value in values:
138+
if value:
139+
return value
140+
return None
141+
142+
143+
def _env_value(*names: str) -> str | None:
144+
"""Return the first available environment variable value."""
145+
for name in names:
146+
value = try_get_secret(name, default=None)
147+
if value:
148+
return str(value)
149+
return None

0 commit comments

Comments
 (0)