Skip to content

feat(cloud): add shared Cloud client auth foundation#1027

Closed
Aaron ("AJ") Steers (aaronsteers) wants to merge 16 commits into
mainfrom
devin/1779160376-cloud-auth-foundation
Closed

feat(cloud): add shared Cloud client auth foundation#1027
Aaron ("AJ") Steers (aaronsteers) wants to merge 16 commits into
mainfrom
devin/1779160376-cloud-auth-foundation

Conversation

@aaronsteers
Copy link
Copy Markdown
Member

@aaronsteers Aaron ("AJ") Steers (aaronsteers) commented May 19, 2026

Summary

AJ asked for this split so the non-CLI Cloud/auth foundation from #1010 can merge ahead of the CLI additions.

This PR is intentionally non-stacked against main and includes only shared PyAirbyte Cloud/auth code:

  • Adds airbyte.cloud.CloudClient as the shared authenticated Cloud/self-managed API facade in airbyte/cloud/client.py.
  • Adds airbyte.cloud.CloudOrganization in airbyte/cloud/organizations.py.
  • Keeps shared resolved credentials as an internal _AirbyteCredentials class in private airbyte.cloud._credentials, used internally by CloudClient, CloudWorkspace, and CloudOrganization.
  • Keeps public Cloud constructors/factories free of the private _AirbyteCredentials type; internal credential injection uses private factory methods.
  • Moves credential resolution and credentials-file read/write onto _AirbyteCredentials while keeping login/logout helpers private to airbyte.cloud._credentials.
  • Keeps private airbyte.cloud._credentials helpers for credential-file handling, explicit/env/secret-manager/credentials-file resolution, client-credentials login, logout, bearer-token auth, and optional organization_id support.
  • Updates CloudWorkspace to resolve credentials through shared Cloud credential logic while preserving keyword-only construction.
  • Keeps CloudWorkspace.deploy_source() scoped to deploying a Source; broader dict/string source deployment API changes are deferred.
  • Adds shared Cloud workspace/job info helpers needed by MCP/non-CLI consumers.
  • Routes Cloud MCP workspace/client/organization construction through the shared Cloud client/auth foundation.
  • Adds unit coverage for Cloud credential resolution and login behavior.

Included files:

  • airbyte/_util/api_imports.py
  • airbyte/cloud/__init__.py
  • airbyte/cloud/_credentials.py
  • airbyte/cloud/client.py
  • airbyte/cloud/organizations.py
  • airbyte/cloud/workspaces.py
  • airbyte/constants.py
  • airbyte/mcp/cloud.py
  • tests/unit_tests/test_cloud_credentials.py

Explicitly excluded from this split:

  • airbyte/cli/** Cyclopts CLI entrypoints and command modules.
  • airbyte-cloud / airbyte-local console-script changes in pyproject.toml.
  • CLI docs generation changes and generated CLI docs/tests.
  • CLI-only behavior changes and uv.lock dependency changes from PR 1010.
  • Deferred metadata helpers on CloudConnection, CloudConnector, and SyncResult.
  • Broader CloudWorkspace.deploy_source() API changes for dict/string source inputs.

Review & Testing Checklist for Human

  • Verify the CloudClient / CloudWorkspace / CloudOrganization construction shape matches what MCP and future CLI consumers should share.
  • Verify the internal _AirbyteCredentials approach is the preferred visibility boundary before PR 1010 rebases on this.
  • Verify the credentials-file/environment-variable/secret-manager precedence and local login/logout behavior are acceptable.
  • After this lands, rebase feat(cli): add airbyte-cloud and airbyte-local CLIs #1010 on main and remove any duplicate Cloud/auth foundation changes from that branch.

Notes

Validation run locally after latest review updates:

  • uv --directory /home/ubuntu/repos/PyAirbyte run ruff format --check airbyte/cloud/client.py airbyte/cloud/workspaces.py — passed.
  • uv --directory /home/ubuntu/repos/PyAirbyte run ruff check airbyte/cloud/client.py airbyte/cloud/workspaces.py airbyte/mcp/cloud.py tests/unit_tests/test_cloud_credentials.py tests/unit_tests/test_mcp_cloud.py — passed.
  • uv --directory /home/ubuntu/repos/PyAirbyte run pyrefly check — completed with 0 errors and 2 warnings about existing redundant casts in airbyte/mcp/cloud.py.
  • uv --directory /home/ubuntu/repos/PyAirbyte run pytest tests/unit_tests/test_cloud_credentials.py tests/unit_tests/test_mcp_cloud.py tests/unit_tests/test_cloud_api_util.py tests/unit_tests/test_cloud_api_roots.py tests/integration_tests/cloud/test_cloud_api_util.py tests/integration_tests/cloud/test_cloud_workspaces.py — passed: 47 passed, 1 skipped. The skipped test is an existing flaky Cloud workspace test linked to https://github.com/airbytehq/airbyte-internal-issues/issues/15502.

CI status after the latest push: pending.

Preview docs deployment: https://vercel.com/airbyte-growth/pyairbyte-api-docs/4uG5sch9xeGcHH9U4VYyw9Vzcnmk

Link to Devin session: https://app.devin.ai/sessions/b97ebb27aa72481d982ebb1b9131b687
Requested by: Aaron ("AJ") Steers (@aaronsteers)

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced CloudClient for authenticated Airbyte Cloud API access with improved credentials management
    • Added non-interactive login and logout functionality with local credential file persistence
    • Enabled workspace and organization listing, retrieval, and management capabilities with scoped access
    • Enhanced workspace configuration with improved credential resolution and API root handling
  • Tests

    • Added comprehensive cloud credentials test coverage for authentication flows and validation

Review Change Stack

@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@github-actions
Copy link
Copy Markdown

👋 Greetings, Airbyte Team Member!

Here are some helpful tips and reminders for your convenience.

💡 Show Tips and Tricks

Testing This PyAirbyte Version

You can test this version of PyAirbyte using the following:

# Run PyAirbyte CLI from this branch:
uvx --from 'git+https://github.com/airbytehq/PyAirbyte.git@devin/1779160376-cloud-auth-foundation' pyairbyte --help

# Install PyAirbyte from this branch for development:
pip install 'git+https://github.com/airbytehq/PyAirbyte.git@devin/1779160376-cloud-auth-foundation'

PR Slash Commands

Airbyte Maintainers can execute the following slash commands on your PR:

  • /fix-pr - Fixes most formatting and linting issues
  • /uv-lock - Updates uv.lock file
  • /test-pr - Runs tests with the updated PyAirbyte
  • /prerelease - Builds and publishes a prerelease version to PyPI
📚 Show Repo Guidance

Helpful Resources

Community Support

Questions? Join the #pyairbyte channel in our Slack workspace.

📝 Edit this welcome message.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds YAML-backed Cloud credential resolution, a CloudClient facade and CloudOrganization, refactors CloudWorkspace and MCP to use the new client/credentials, expands cloud package exports, and adds unit tests for credential flows.

Changes

Cloud Credential & Client Abstraction

Layer / File(s) Summary
Credential Management & File I/O
airbyte/constants.py, airbyte/cloud/_credentials.py
_AirbyteCredentials and CloudLoginResult implement YAML-backed credential resolution, non-interactive login, validation, to_file/from_file, and logout.
CloudOrganization Wrapper
airbyte/cloud/organizations.py
CloudOrganization stores organization_id, lazily fetches/caches org info, and exposes billing/payment-derived properties and account lock status.
CloudClient Authenticated Facade
airbyte/cloud/client.py
CloudClient centralizes credential resolution (factories from_env/from_auth), login/logout, get_workspace, list_workspaces (org-scoped overload), and get_organization.
CloudWorkspace Credential Integration & API
airbyte/cloud/workspaces.py
CloudWorkspace now builds credentials via _AirbyteCredentials.from_auth in an explicit __init__, provides credential-scoped get_organization(), simplifies from_env, and adds list_workspaces().
MCP Cloud Operations Integration
airbyte/mcp/cloud.py
Adds _get_cloud_client(ctx, organization_id) helper; refactors workspace/org lookup and workspace listing to use CloudClient instead of ad-hoc credential/roots handling.
Public API Type Exports
airbyte/_util/api_imports.py, airbyte/cloud/__init__.py
Imports/exports WorkspaceResponse, exposes CloudClient, CloudOrganization, and adds client and organizations to package exports.
Credential Login & Resolution Tests
tests/unit_tests/test_cloud_credentials.py
Tests covering login success path, YAML persistence and permissions, input validation errors, default cloud root behavior, and from_auth secret resolution.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • airbytehq/PyAirbyte#1024: Overlaps Cloud client/workspace listing changes and pagination/parameter handling for cloud workspace listing.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a shared Cloud client authentication foundation (CloudClient, CloudOrganization, _AirbyteCredentials) across multiple modules.
Docstring Coverage ✅ Passed Docstring coverage is 83.75% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1779160376-cloud-auth-foundation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
airbyte/mcp/cloud.py (1)

1326-1332: 💤 Low value

Both approaches are functionally equivalent, but consider using the factory method for consistency.

I checked the implementation: from_explicit_credentials() ultimately calls the direct constructor after wrapping credentials, and the direct constructor's __post_init__ does the same wrapping. So there's no behavioral difference between the two approaches.

However, _get_cloud_client at line 282 uses CloudClient.from_explicit_credentials(...) while this function uses CloudClient(...) directly. Since the factory method is already the pattern used elsewhere in the module, would it make sense to align them for consistency, wdyt?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@airbyte/mcp/cloud.py` around lines 1326 - 1332, The call creates a
CloudClient via its constructor but elsewhere (e.g. _get_cloud_client) the
module uses the factory CloudClient.from_explicit_credentials for consistency;
change the instantiation to use
CloudClient.from_explicit_credentials(client_id=client_id,
client_secret=client_secret, bearer_token=bearer_token,
public_api_root=api_root,
config_api_root=config_api_root).get_organization(organization_id=organization_id,
organization_name=organization_name) so the factory path is used (it ultimately
wraps credentials the same way) and maintains the module-wide pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@airbyte/mcp/cloud.py`:
- Around line 1326-1332: The call creates a CloudClient via its constructor but
elsewhere (e.g. _get_cloud_client) the module uses the factory
CloudClient.from_explicit_credentials for consistency; change the instantiation
to use CloudClient.from_explicit_credentials(client_id=client_id,
client_secret=client_secret, bearer_token=bearer_token,
public_api_root=api_root,
config_api_root=config_api_root).get_organization(organization_id=organization_id,
organization_name=organization_name) so the factory path is used (it ultimately
wraps credentials the same way) and maintains the module-wide pattern.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 20a6be19-03fb-4999-b67b-3be1dfe62dfd

📥 Commits

Reviewing files that changed from the base of the PR and between 70a7d82 and b8cfd71.

📒 Files selected for processing (10)
  • airbyte/_util/api_imports.py
  • airbyte/cloud/__init__.py
  • airbyte/cloud/connections.py
  • airbyte/cloud/connectors.py
  • airbyte/cloud/credentials.py
  • airbyte/cloud/sync_results.py
  • airbyte/cloud/workspaces.py
  • airbyte/constants.py
  • airbyte/mcp/cloud.py
  • tests/unit_tests/test_cloud_credentials.py

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 19, 2026

PyTest Results (Fast Tests Only, No Creds)

439 tests  +10   439 ✅ +10   5m 48s ⏱️ -13s
  1 suites ± 0     0 💤 ± 0 
  1 files   ± 0     0 ❌ ± 0 

Results for commit 3d1116e. ± Comparison against base commit 70a7d82.

♻️ This comment has been updated with latest results.

@aaronsteers Aaron ("AJ") Steers (aaronsteers) marked this pull request as ready for review May 19, 2026 03:38
Copilot AI review requested due to automatic review settings May 19, 2026 03:38
Comment thread airbyte/cloud/connections.py Outdated
Comment thread airbyte/cloud/connectors.py Outdated
Comment thread airbyte/cloud/sync_results.py Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extracts and introduces a shared, non-CLI “Cloud/auth foundation” for PyAirbyte Cloud usage, centered around a new authenticated CloudClient plus credential-file/env resolution utilities, and updates MCP + Cloud objects to route through the shared foundation.

Changes:

  • Added airbyte.cloud.CloudClient (co-located with CloudWorkspace) as the authenticated facade for Cloud/self-managed API interactions, including workspace/org discovery helpers.
  • Added airbyte.cloud.credentials for credentials-file handling, resolution precedence (explicit/env/file), client-credentials login, and logout.
  • Updated MCP Cloud tooling and Cloud entity wrappers to use shared construction paths and added get_info() helpers for metadata retrieval.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
airbyte/_util/api_imports.py Exposes WorkspaceResponse for typing/public use.
airbyte/cloud/__init__.py Exports CloudClient from the Cloud package surface.
airbyte/cloud/connections.py Adds CloudConnection.get_info() convenience wrapper.
airbyte/cloud/connectors.py Adds CloudConnector.get_info() convenience wrapper.
airbyte/cloud/credentials.py New credential resolution + credentials-file login/logout utilities.
airbyte/cloud/sync_results.py Adds SyncResult.get_info() convenience wrapper.
airbyte/cloud/workspaces.py Adds CloudClient; refactors CloudWorkspace to shared credential resolution and adds workspace/job helpers.
airbyte/constants.py Adds AIRBYTE_CLOUD_ORGANIZATION_ID env var constant.
airbyte/mcp/cloud.py Routes MCP Cloud client/workspace/org construction through CloudClient.
tests/unit_tests/test_cloud_credentials.py Adds unit coverage for non-interactive credentials login writing token file.
Comments suppressed due to low confidence (4)

airbyte/cloud/workspaces.py:311

  • CloudClient.get_organization() raises AirbyteMissingResourceError(resource_type="organization") without including the requested organization_id/organization_name in resource_name_or_id or context. This makes the error much less actionable (and loses detail compared to the previous implementation in airbyte/mcp/cloud.py). Consider including the identifier that was searched for (and possibly a guidance message) when raising.
        if organization_id:
            matching_organizations = [
                organization
                for organization in organizations
                if organization.organization_id == organization_id
            ]
        else:
            matching_organizations = [
                organization
                for organization in organizations
                if organization.organization_name == organization_name
            ]

        if not matching_organizations:
            raise AirbyteMissingResourceError(resource_type="organization")
        if len(matching_organizations) > 1:
            raise exc.PyAirbyteInputError(
                message="Organization name matches multiple organizations."
            )

airbyte/cloud/workspaces.py:243

  • CloudClient.list_workspaces() calls api_util.list_workspaces(workspace_id="", ...). While workspace_id is currently only used for error context in api_util, passing an empty string can lead to confusing error output. Consider updating the wrapper to pass a meaningful value (or adjusting api_util.list_workspaces to not require a workspace_id) so error context remains useful.
    def list_workspaces(
        self,
        name: str | None = None,
        *,
        name_filter: Callable[[str], bool] | None = None,
    ) -> list[api_imports.WorkspaceResponse]:
        """List workspaces available to this client."""
        return api_util.list_workspaces(
            workspace_id="",
            api_root=self.public_api_root,
            name=name,
            name_filter=name_filter,
            client_id=self.client_id,
            client_secret=self.client_secret,
            bearer_token=self.bearer_token,
        )

airbyte/cloud/workspaces.py:1070

  • CloudWorkspace.list_workspaces() types name_filter as plain Callable | None, while CloudClient.list_workspaces() uses Callable[[str], bool] | None. Tightening the annotation here to match (and to document the expected predicate signature) would improve usability and static checking.
    def list_workspaces(
        self,
        name: str | None = None,
        *,
        name_filter: Callable | None = None,
    ) -> list[WorkspaceResponse]:

airbyte/cloud/workspaces.py:605

  • The behavior of from_env() now depends on resolve_cloud_credentials() (via __init__), which raises PyAirbyteInputError when credentials are missing/partial. The docstring above still states it raises PyAirbyteSecretNotFoundError; please update the documented exception type(s) to match the new shared-resolution behavior so callers can catch the right error.
        return cls(
            workspace_id=workspace_id,
            api_root=api_root,
            config_api_root=config_api_root,
        )

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread airbyte/cloud/workspaces.py Outdated
Comment thread airbyte/cloud/_credentials.py
Comment thread airbyte/cloud/_credentials.py
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment thread airbyte/cloud/_credentials.py
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 19, 2026

PyTest Results (Full)

509 tests  +10   491 ✅ +10   25m 15s ⏱️ +15s
  1 suites ± 0    18 💤 ± 0 
  1 files   ± 0     0 ❌ ± 0 

Results for commit 3d1116e. ± Comparison against base commit 70a7d82.

♻️ This comment has been updated with latest results.

Comment thread airbyte/cloud/_credentials.py Outdated
Comment thread airbyte/cloud/_credentials.py Outdated
Comment thread airbyte/cloud/_credentials.py
Comment thread airbyte/cloud/client.py Outdated
Comment thread airbyte/cloud/client.py Outdated
Comment thread airbyte/cloud/client.py
Comment thread airbyte/cloud/workspaces.py Outdated
Comment thread airbyte/cloud/client.py Fixed
Comment thread airbyte/cloud/client.py Fixed
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
airbyte/cloud/_credentials.py (2)

144-150: 💤 Low value

Silent exception swallowing in read_file may hide real issues.

Catching OSError and yaml.YAMLError and returning an empty dict silently could mask legitimate problems (permission errors, corrupted YAML, etc.). Would it be worth at least logging a warning here so users have some visibility into credential file read failures, wdyt?

         try:
             content = credentials_file_path.read_text(encoding="utf-8").strip()
             parsed = yaml.safe_load(content) if content else {}
         except (OSError, yaml.YAMLError):
+            # Consider: logging.warning("Failed to read credentials file: %s", credentials_file_path)
             return {}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@airbyte/cloud/_credentials.py` around lines 144 - 150, The current exception
handling in the credentials file reader swallows OSError and yaml.YAMLError and
returns {} silently; modify the try/except in the read routine so that it
catches exceptions as e and emits a warning including the exception message and
the credentials file path (use the module logger or logging.warning) before
returning {} to preserve behavior; reference the credentials_file_path.read_text
call, yaml.safe_load usage, and the _as_string_mapping return so you only add
logging inside that except block without changing subsequent return logic.

28-35: 💤 Low value

Duplicate environment variable constants?

These constants duplicate those imported from airbyte.constants on lines 12-22. Are these intentionally different (e.g., AIRBYTE_CLIENT_ID vs AIRBYTE_CLOUD_CLIENT_ID), or could we consolidate them to avoid drift, wdyt?

Looking at the usage in _env_value calls (e.g., line 81), both sets are checked as fallbacks, which makes sense for backward compatibility. If that's the intent, maybe a brief comment explaining this dual-env-var strategy would help future readers?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@airbyte/cloud/_credentials.py` around lines 28 - 35, The file defines
duplicated env var constants (CREDENTIALS_FILE_PATH, CLIENT_ID_ENV_VAR,
CLIENT_SECRET_ENV_VAR, WORKSPACE_ID_ENV_VAR, ORGANIZATION_ID_ENV_VAR,
PUBLIC_API_ROOT_ENV_VAR, BEARER_TOKEN_ENV_VAR, CONFIG_API_ROOT_ENV_VAR) that
mirror those in airbyte.constants and are used as fallbacks via _env_value; add
a brief clarifying comment above these constants explaining that this file
intentionally defines legacy and new env var names (and is checked as a fallback
by _env_value) for backward compatibility, or alternatively consolidate to
import the canonical names from airbyte.constants and remove duplicates—update
references to use the canonical symbols (or keep both with the comment) to avoid
future drift.
airbyte/cloud/organizations.py (1)

65-67: 💤 Low value

Bare except Exception is quite broad.

This catches all exceptions including ones that might indicate real bugs (e.g., TypeError from bad API response parsing). Would it be worth narrowing this to specific expected exceptions (like AirbyteError or network-related exceptions), or at least logging the caught exception for debugging purposes, wdyt?

         try:
             self._organization_info = api_util.get_organization_info(
                 organization_id=self.organization_id,
                 api_root=self._credentials.public_api_root,
                 config_api_root=self._credentials.config_api_root,
                 client_id=self._credentials.client_id,
                 client_secret=self._credentials.client_secret,
                 bearer_token=self._credentials.bearer_token,
             )
-        except Exception:
+        except (AirbyteError, ConnectionError, TimeoutError):
             self._organization_info_fetch_failed = True
             return {}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@airbyte/cloud/organizations.py` around lines 65 - 67, Replace the bare except
in the organization info fetch block so it only handles expected errors (e.g.,
AirbyteError, requests.ConnectionError, requests.Timeout) instead of catching
all Exception; also log the caught exception before setting
self._organization_info_fetch_failed and returning {} to aid debugging. Locate
the try/except around the code that sets self._organization_info_fetch_failed
(the method that fetches organization info) and change the except Exception to a
narrower exception tuple and call the module/class logger to record the
exception details along with a clear message before returning the empty dict.
airbyte/cloud/client.py (2)

238-244: ⚡ Quick win

Redundant validation - resolved_organization_id will always be truthy here.

If we enter this block (line 238 condition is true), then either organization_id or self.organization_id is truthy. Since resolved_organization_id = organization_id or self.organization_id, it will always be truthy, making the check at lines 240-244 unreachable dead code. Could we simplify by removing the inner validation, wdyt?

         if organization_id is not None or self.organization_id is not None:
             resolved_organization_id = organization_id or self.organization_id
-            if not resolved_organization_id:
-                raise exc.PyAirbyteInputError(
-                    message="Organization ID is required.",
-                    guidance="Provide an organization ID.",
-                )
+            assert resolved_organization_id  # Guaranteed by outer condition
             return api_util.list_workspaces_in_organization(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@airbyte/cloud/client.py` around lines 238 - 244, The conditional inside the
block that checks resolved_organization_id is redundant because the outer if
already guarantees organization_id or self.organization_id is truthy; remove the
inner falsy check and the raise of exc.PyAirbyteInputError, and simply set
resolved_organization_id = organization_id or self.organization_id (keeping the
outer if condition as-is) to simplify the logic in the function that handles
organization_id resolution.

49-58: 💤 Low value

Minor: any([...]) creates an unnecessary list.

Using any(...) with a generator expression is slightly more efficient than any([...]) with a list. Small nit, but since you're here anyway, wdyt?

         if credentials and any(
-            [
-                client_id,
-                client_secret,
-                bearer_token,
-                public_api_root,
-                config_api_root,
-                organization_id,
-            ]
+            (
+                client_id,
+                client_secret,
+                bearer_token,
+                public_api_root,
+                config_api_root,
+                organization_id,
+            )
         ):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@airbyte/cloud/client.py` around lines 49 - 58, The conditional uses
any([...]) which builds an unnecessary list; change the check in client.py (the
if that references credentials and any([...]) containing client_id,
client_secret, bearer_token, public_api_root, config_api_root, organization_id)
to use a generator/tuple expression instead (e.g., any((client_id,
client_secret, bearer_token, public_api_root, config_api_root, organization_id))
or any(x for x in (...))) so no intermediate list is allocated.
airbyte/cloud/workspaces.py (1)

664-679: 💤 Low value

list_workspaces on CloudWorkspace duplicates CloudClient.list_workspaces.

This method lists workspaces available to the credentials, which isn't really workspace-specific behavior. It's also available on CloudClient. Is this intentional for backward compatibility or convenience? If so, a brief docstring note mentioning CloudClient.list_workspaces as the canonical location might help, wdyt?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@airbyte/cloud/workspaces.py` around lines 664 - 679, The
CloudWorkspace.list_workspaces method duplicates behavior provided by
CloudClient.list_workspaces; update the CloudWorkspace.list_workspaces docstring
to state that this is provided for convenience/backward-compatibility and point
callers to CloudClient.list_workspaces as the canonical API, e.g., mention
CloudClient.list_workspaces and when to prefer it, and keep the existing
implementation unchanged to preserve compatibility.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@airbyte/cloud/workspaces.py`:
- Around line 121-142: Remove the redundant early assignment to
self.config_api_root — it's overwritten by the value from
_AirbyteCredentials.from_auth. In the initializer that calls
_AirbyteCredentials.from_auth (the block creating credentials and then setting
self.workspace_id, self.client_id, etc.), delete the initial
self.config_api_root = config_api_root line and rely on
credentials.config_api_root when assigning self.config_api_root after
credentials are created.

---

Nitpick comments:
In `@airbyte/cloud/_credentials.py`:
- Around line 144-150: The current exception handling in the credentials file
reader swallows OSError and yaml.YAMLError and returns {} silently; modify the
try/except in the read routine so that it catches exceptions as e and emits a
warning including the exception message and the credentials file path (use the
module logger or logging.warning) before returning {} to preserve behavior;
reference the credentials_file_path.read_text call, yaml.safe_load usage, and
the _as_string_mapping return so you only add logging inside that except block
without changing subsequent return logic.
- Around line 28-35: The file defines duplicated env var constants
(CREDENTIALS_FILE_PATH, CLIENT_ID_ENV_VAR, CLIENT_SECRET_ENV_VAR,
WORKSPACE_ID_ENV_VAR, ORGANIZATION_ID_ENV_VAR, PUBLIC_API_ROOT_ENV_VAR,
BEARER_TOKEN_ENV_VAR, CONFIG_API_ROOT_ENV_VAR) that mirror those in
airbyte.constants and are used as fallbacks via _env_value; add a brief
clarifying comment above these constants explaining that this file intentionally
defines legacy and new env var names (and is checked as a fallback by
_env_value) for backward compatibility, or alternatively consolidate to import
the canonical names from airbyte.constants and remove duplicates—update
references to use the canonical symbols (or keep both with the comment) to avoid
future drift.

In `@airbyte/cloud/client.py`:
- Around line 238-244: The conditional inside the block that checks
resolved_organization_id is redundant because the outer if already guarantees
organization_id or self.organization_id is truthy; remove the inner falsy check
and the raise of exc.PyAirbyteInputError, and simply set
resolved_organization_id = organization_id or self.organization_id (keeping the
outer if condition as-is) to simplify the logic in the function that handles
organization_id resolution.
- Around line 49-58: The conditional uses any([...]) which builds an unnecessary
list; change the check in client.py (the if that references credentials and
any([...]) containing client_id, client_secret, bearer_token, public_api_root,
config_api_root, organization_id) to use a generator/tuple expression instead
(e.g., any((client_id, client_secret, bearer_token, public_api_root,
config_api_root, organization_id)) or any(x for x in (...))) so no intermediate
list is allocated.

In `@airbyte/cloud/organizations.py`:
- Around line 65-67: Replace the bare except in the organization info fetch
block so it only handles expected errors (e.g., AirbyteError,
requests.ConnectionError, requests.Timeout) instead of catching all Exception;
also log the caught exception before setting
self._organization_info_fetch_failed and returning {} to aid debugging. Locate
the try/except around the code that sets self._organization_info_fetch_failed
(the method that fetches organization info) and change the except Exception to a
narrower exception tuple and call the module/class logger to record the
exception details along with a clear message before returning the empty dict.

In `@airbyte/cloud/workspaces.py`:
- Around line 664-679: The CloudWorkspace.list_workspaces method duplicates
behavior provided by CloudClient.list_workspaces; update the
CloudWorkspace.list_workspaces docstring to state that this is provided for
convenience/backward-compatibility and point callers to
CloudClient.list_workspaces as the canonical API, e.g., mention
CloudClient.list_workspaces and when to prefer it, and keep the existing
implementation unchanged to preserve compatibility.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 070615ff-a9e9-4660-9993-b221eb997554

📥 Commits

Reviewing files that changed from the base of the PR and between 6038c5d and 6bcaf99.

📒 Files selected for processing (7)
  • airbyte/cloud/__init__.py
  • airbyte/cloud/_credentials.py
  • airbyte/cloud/client.py
  • airbyte/cloud/organizations.py
  • airbyte/cloud/workspaces.py
  • airbyte/mcp/cloud.py
  • tests/unit_tests/test_cloud_credentials.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • airbyte/mcp/cloud.py

Comment thread airbyte/cloud/workspaces.py Outdated
Comment thread airbyte/cloud/workspaces.py Outdated
Comment thread airbyte/cloud/_credentials.py Outdated
Comment thread airbyte/cloud/_credentials.py Outdated
Comment thread airbyte/cloud/_credentials.py Fixed
Comment thread airbyte/cloud/client.py Outdated
Comment thread airbyte/cloud/client.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/unit_tests/test_cloud_credentials.py (2)

47-49: ⚡ Quick win

Consider asserting the bearer token in the result as well?

The test verifies the bearer token was persisted to the YAML file (line 52), but doesn't check whether result itself exposes the bearer token. If CloudLoginResult has a bearer_token field, asserting result.bearer_token == "test-bearer-token" would provide more complete coverage of the login flow's return value. Wdyt?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit_tests/test_cloud_credentials.py` around lines 47 - 49, The test
currently asserts paths and API roots but doesn't verify the returned
CloudLoginResult includes the bearer token; update the unit test that
constructs/receives result (the CloudLoginResult instance) to also assert
result.bearer_token == "test-bearer-token" (or the expected token string) so the
login flow's return value is fully validated, and ensure the CloudLoginResult
type exposes the bearer_token attribute before adding the assertion.

161-168: ⚡ Quick win

Verify that the fake_try_get_secret signature matches the actual implementation?

The mocked function signature uses secret_name as positional-only, default as keyword, and **_ for remaining kwargs. To ensure this test accurately reflects real behavior, could you confirm this matches the actual try_get_secret signature from airbyte.secrets? Wdyt about running a quick check?

#!/bin/bash
# Description: Verify the signature of try_get_secret to ensure test mock matches reality

# Search for try_get_secret definition
ast-grep --pattern $'def try_get_secret($$$) $$$'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit_tests/test_cloud_credentials.py` around lines 161 - 168, The
test's fake_try_get_secret signature may not match the real try_get_secret from
airbyte.secrets; import try_get_secret and inspect.signature(try_get_secret) to
confirm the exact parameters, then update fake_try_get_secret to match (same
positional/keyword/varargs/kwonly layout) or simplify to accept identical
*args/**kwargs and the same parameter names so the mock accurately mirrors
try_get_secret's API (adjust the fake function in test_cloud_credentials.py
accordingly).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/unit_tests/test_cloud_credentials.py`:
- Around line 47-49: The test currently asserts paths and API roots but doesn't
verify the returned CloudLoginResult includes the bearer token; update the unit
test that constructs/receives result (the CloudLoginResult instance) to also
assert result.bearer_token == "test-bearer-token" (or the expected token string)
so the login flow's return value is fully validated, and ensure the
CloudLoginResult type exposes the bearer_token attribute before adding the
assertion.
- Around line 161-168: The test's fake_try_get_secret signature may not match
the real try_get_secret from airbyte.secrets; import try_get_secret and
inspect.signature(try_get_secret) to confirm the exact parameters, then update
fake_try_get_secret to match (same positional/keyword/varargs/kwonly layout) or
simplify to accept identical *args/**kwargs and the same parameter names so the
mock accurately mirrors try_get_secret's API (adjust the fake function in
test_cloud_credentials.py accordingly).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0fb26d45-7816-41c1-9cbb-d903a21022f0

📥 Commits

Reviewing files that changed from the base of the PR and between 6bcaf99 and 593dd0a.

📒 Files selected for processing (4)
  • airbyte/cloud/_credentials.py
  • airbyte/cloud/client.py
  • airbyte/cloud/workspaces.py
  • tests/unit_tests/test_cloud_credentials.py

Comment thread airbyte/cloud/workspaces.py Outdated
self,
name: str,
source: Source,
source: Source | dict[str, Any] | None = None,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that we want to make this change here in this PR. And an alternate approach, if we only need source_name would be allow source as a string or a Source object, which I think is cleaner.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 5244853: restored CloudWorkspace.deploy_source() to accept a Source object only and removed the dict / config input path from this split PR. I also called out the broader dict/string source-input API as deferred in the PR description.


Devin session

Comment thread airbyte/cloud/client.py Outdated
Comment on lines +58 to +78
@staticmethod
def _credentials_from_auth(
*,
organization_id: str | None = None,
client_id: str | SecretString | None = None,
client_secret: str | SecretString | None = None,
bearer_token: str | SecretString | None = None,
public_api_root: str | None = None,
config_api_root: str | None = None,
credentials_file_path: Path = CREDENTIALS_FILE_PATH,
) -> _AirbyteCredentials:
"""Create resolved Cloud credentials from explicit inputs."""
return _AirbyteCredentials.from_auth(
organization_id=organization_id,
client_id=client_id,
client_secret=client_secret,
bearer_token=bearer_token,
public_api_root=public_api_root,
config_api_root=config_api_root,
credentials_file_path=credentials_file_path,
)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look like it belongs on this class and doesn't add any value here. Suggest deletion.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 5244853: deleted the passthrough _credentials_from_auth() helper and now call _AirbyteCredentials.from_auth() directly from CloudClient.from_env() and CloudClient.from_auth().


Devin session

Comment thread airbyte/cloud/client.py Outdated
return cls._new_from_credentials(credentials)

@classmethod
def _new_from_credentials(cls, credentials: _AirbyteCredentials) -> CloudClient:
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The word 'new' is implied. You can simply rename to _from_credentials

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 5244853: renamed the internal constructor helper from _new_from_credentials() to _from_credentials().


Devin session

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@airbyte/cloud/client.py`:
- Around line 244-254: The non-organization branch that sets name =
name_contains and then calls api_util.list_workspaces does not pass through the
limit argument, so enforce the same limiting behavior by adding limit=limit to
the api_util.list_workspaces call; update the call site that currently passes
workspace_id, api_root, name, name_filter, client_id, client_secret,
bearer_token to also include limit=limit (ensure the surrounding method where
name_contains/name/name_filter are defined has access to the limit parameter).
- Around line 227-243: The organization branch currently returns
api_util.list_workspaces_in_organization directly so the name_filter (and the
name/name_contains logic) is ignored; update the organization-scoped path in the
method that references organization_id/self.organization_id to either pass the
appropriate name/name_contains argument into
api_util.list_workspaces_in_organization (e.g., use name_contains=name_filter or
name_contains=name or name_contains=name_filter or name) or call
api_util.list_workspaces_in_organization and then apply the existing name_filter
predicate to the returned list before returning, ensuring the same filtering
behavior as the non-organization branch; refer to organization_id,
api_util.list_workspaces_in_organization, name_contains, name, and name_filter
when making the change.

In `@airbyte/cloud/organizations.py`:
- Around line 58-75: The code currently sets
_organization_info_fetch_failed=True on any exception from
api_util.get_organization_info which permanently short-circuits future lookups;
instead, on exception in the method that calls api_util.get_organization_info
(the block that reads/sets self._organization_info), preserve and return the
last-known self._organization_info if it exists and avoid flipping the permanent
_organization_info_fetch_failed flag; only set the failure flag when there is no
prior cached self._organization_info (or implement a retry/timestamp-based
backoff) and ensure force_refresh still triggers a fetch attempt that can clear
the flag—update the exception handling around get_organization_info, referencing
_organization_info_fetch_failed, _organization_info, and get_organization_info
to implement this behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9ed12fd7-4845-48ec-99fa-137bf98e8291

📥 Commits

Reviewing files that changed from the base of the PR and between 593dd0a and 5244853.

📒 Files selected for processing (3)
  • airbyte/cloud/client.py
  • airbyte/cloud/organizations.py
  • airbyte/cloud/workspaces.py

Comment thread airbyte/cloud/client.py
Comment thread airbyte/cloud/client.py
Comment thread airbyte/cloud/organizations.py Outdated
@aaronsteers
Copy link
Copy Markdown
Member Author

Reverting to draft (on hold), as we are pivoting to first an internal CLI app launch.

@aaronsteers Aaron ("AJ") Steers (aaronsteers) marked this pull request as draft May 20, 2026 01:50
@aaronsteers
Copy link
Copy Markdown
Member Author

Closing. Stale. Migrated to ops repo CLI (internal).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants