Skip to content

feat(cloud): add CloudClient, move CloudOrganization to dedicated cloud.organizations module #1030

Merged
Aaron ("AJ") Steers (aaronsteers) merged 4 commits into
mainfrom
devin/1779226376-cloud-core-refactor
May 20, 2026
Merged

feat(cloud): add CloudClient, move CloudOrganization to dedicated cloud.organizations module #1030
Aaron ("AJ") Steers (aaronsteers) merged 4 commits into
mainfrom
devin/1779226376-cloud-core-refactor

Conversation

@aaronsteers
Copy link
Copy Markdown
Member

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

Summary

Extracts the core Cloud client/organization refactor from PR #1027, excluding login/logout and credentials-file persistence. This is the first of a two-PR split requested by AJ.

What's included:

  • New CloudClient class in airbyte/cloud/client.py with from_auth(*, env_vars=False, ...) factory (merges previous from_env/from_auth into one method)
  • CloudOrganization moved from workspaces.py to airbyte/cloud/organizations.py
  • Internal _AirbyteCredentials in airbyte/cloud/_credentials.py for shared credential resolution (env-var fallback, no file I/O)
  • CloudWorkspace.__init__ now accepts str | SecretString | None for credentials; workspace_id is optional at signature level
  • New CloudWorkspace.list_workspaces() method
  • CloudClient and CloudOrganization exported from airbyte.cloud.__all__
  • WorkspaceResponse added to airbyte._util.api_imports
  • CLOUD_ORGANIZATION_ID_ENV_VAR added to airbyte.constants
  • MCP cloud module refactored to use CloudClient internally

What's NOT included (deferred to stacked PR):

  • CloudClient.login() / CloudClient.logout()
  • CloudLoginResult dataclass
  • Credentials-file read/write (from_file, to_file, CREDENTIALS_FILE_PATH)
  • All login/logout/creds-file tests

Context: Split requested by AJ from #1027. See also #1010 for the full CLI PR.

Review & Testing Checklist for Human

  • Verify CloudClient.from_auth(env_vars=True) correctly resolves from env vars
  • Verify CloudClient.from_auth() (default env_vars=False) requires explicit credentials
  • Verify CloudOrganization import path works from both airbyte.cloud and airbyte.cloud.organizations
  • Run uv run pytest tests/unit_tests/ -x — all 302 tests should pass

Notes

Local validation:

  • uv run ruff format + uv run ruff check: clean
  • uv run pyrefly check: 0 errors
  • uv run pytest tests/unit_tests/: 302 passed, 1 skipped

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

Summary by CodeRabbit

  • New Features
    • Unified Cloud authentication with env-var fallback, scoped credential objects, and clearer validation.
    • Added CloudClient and CloudOrganization classes plus improved CloudWorkspace flows for consistent org/workspace access.
    • Workspace listing: name filtering and consistent limit behavior; environment variable support for selecting an organization.
  • Tests
    • Expanded unit tests covering credential resolution, client/workspace listing, organization resolution, and caching behaviors.

Review Change Stack

Important

Auto-merge enabled.

This PR is set to merge automatically when all requirements are met.

- Add CloudClient class with from_auth(env_vars=False) factory
- Move CloudOrganization from workspaces.py to organizations.py
- Add internal _AirbyteCredentials for shared credential resolution
- Add CloudWorkspace.list_workspaces() method
- Refactor MCP cloud module to use CloudClient internally
- Export CloudClient and CloudOrganization from airbyte.cloud
- Add CLOUD_ORGANIZATION_ID_ENV_VAR constant
- Add WorkspaceResponse to api_imports

Does not include login/logout or credentials-file persistence;
those will follow in a stacked PR.
@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/1779226376-cloud-core-refactor' pyairbyte --help

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

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

📝 Walkthrough

Walkthrough

Centralizes Airbyte Cloud credential resolution into _AirbyteCredentials, adds CloudClient as the unified entry point for workspace and organization operations, introduces CloudOrganization with lazy caching, refactors CloudWorkspace and MCP tooling to use the new client/credentials, updates package exports, and adds unit tests for auth/listing/caching behaviors.

Changes

Cloud Client & Credential Unification

Layer / File(s) Summary
Credential Resolution Infrastructure
airbyte/cloud/_credentials.py
New module with env-var name constants and frozen _AirbyteCredentials; from_auth() selects explicit or env secrets, enforces auth invariants, wraps secrets as SecretString, and provides with_workspace_id()/with_organization_id() plus _first_value/_env_value` helpers.
CloudClient & Workspace Resolution
airbyte/cloud/client.py
New CloudClient class: constructors (__init__, from_auth, _from_credentials), read-only credential/API-root properties, get_workspace() that validates/resolves workspace ID and returns a CloudWorkspace, list_workspaces() with organization/global overloads and filtering/limit semantics, and get_organization() that selects a single org by id or exact name and returns CloudOrganization.
CloudOrganization with Lazy Caching
airbyte/cloud/organizations.py
New CloudOrganization class stores org id and optional metadata, wraps credentials, implements _fetch_organization_info(force_refresh) with caching and failure handling, and exposes organization_name, email, payment_status, subscription_status, and is_account_locked properties.
CloudWorkspace Credential Refactoring
airbyte/cloud/workspaces.py
CloudWorkspace.__init__ now uses _AirbyteCredentials.from_auth(), enforces workspace_id presence, stores resolved auth/API roots, simplifies from_env(), scopes credentials for get_organization(), and adds list_workspaces() returning WorkspaceResponse.
MCP Cloud Tools Refactoring
airbyte/mcp/cloud.py
MCP helpers create a single CloudClient via new _get_cloud_client(...), _get_cloud_workspace() delegates to CloudClient.get_workspace(), organization resolution delegates to CloudClient.get_organization(), and listing/describe tools call the client methods.
Public API Surface & Constants
airbyte/cloud/__init__.py, airbyte/_util/api_imports.py, airbyte/constants.py
Exports updated: airbyte.cloud.__all__ adds client, organizations, CloudClient, and CloudOrganization; WorkspaceResponse added to airbyte._util.api_imports.__all__; new CLOUD_ORGANIZATION_ID_ENV_VAR constant added.
Unit Tests
tests/unit_tests/test_cloud_credentials.py
Expanded tests covering _AirbyteCredentials.from_auth env-var resolution and guidance messages, mixed-auth rejection, CloudClient.list_workspaces/CloudWorkspace.list_workspaces limit forwarding and name-filter behaviors, explicit-credentials non-resolution, CloudClient.get_organization error context, and CloudOrganization caching on refresh failure.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CloudClient
  participant api_util
  participant CloudWorkspace
  participant CloudOrganization

  User->>CloudClient: list_workspaces(name_contains, organization_id?)
  alt organization scope present
    CloudClient->>api_util: list_workspaces_in_organization(organization_id, ...)
    api_util-->>CloudClient: workspace list (unfiltered)
    CloudClient->>CloudClient: apply name_filter / limit
    CloudClient-->>User: filtered/truncated list
  else global scope
    CloudClient->>api_util: list_workspaces(workspace_id="")
    api_util-->>CloudClient: workspace list
    CloudClient->>CloudClient: apply name_contains -> name_filter
    CloudClient-->>User: filtered list
  end

  User->>CloudClient: get_organization(organization_id or organization_name)
  CloudClient->>api_util: list_organizations_for_user()
  api_util-->>CloudClient: organizations
  CloudClient->>CloudOrganization: construct scoped CloudOrganization
  CloudOrganization-->>CloudClient: org object
  CloudClient-->>User: CloudOrganization
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • airbytehq/PyAirbyte#1024: Related changes adding limit support and pagination to api_util.list_workspaces*, which the new list_workspaces implementations call.
  • airbytehq/PyAirbyte#1023: Related work on propagating config_api_root and client/workspace credential plumbing that intersects the new _AirbyteCredentials and CloudClient changes.

Would you like me to highlight specific lines for the highest-risk validation logic (e.g., _AirbyteCredentials.from_auth invariants or CloudClient.get_organization matching) to speed a focused review? wdyt?

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main changes: adding CloudClient and moving CloudOrganization to a dedicated module, which are the core objectives.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1779226376-cloud-core-refactor

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 (4)
airbyte/mcp/cloud.py (1)

1409-1425: 💤 Low value

Consider simplifying by passing the client to _resolve_organization_id?

The current flow creates a CloudClient, extracts its credentials to pass to _resolve_organization_id, which then constructs another CloudClient internally. This means two clients are instantiated for this operation. Would it be cleaner to have _resolve_organization_id accept a CloudClient directly, or have client.list_workspaces() handle the organization resolution internally when given a name? Just a thought for a future iteration, 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 1409 - 1425, Change
_resolve_organization_id to accept an already-instantiated CloudClient instead
of separate credential params and stop creating a new CloudClient inside it;
update the caller in this diff to call _resolve_organization_id(client=client,
organization_id=organization_id, organization_name=organization_name) (remove
api_root/client_id/client_secret/bearer_token/config_api_root args), and inside
_resolve_organization_id use the passed CloudClient methods/properties to
resolve org id; ensure all other call sites of _resolve_organization_id are
updated to pass a CloudClient and adjust type hints/tests accordingly, or
alternatively implement org-name resolution inside CloudClient.list_workspaces
and call that from here.
airbyte/cloud/workspaces.py (1)

642-657: 💤 Low value

Consider adding a limit parameter for consistency?

The other list methods (list_connections, list_sources, list_destinations) all include a limit parameter, but list_workspaces doesn't. For API consistency, would it make sense to add one here too, wdyt?

Also, I notice the method passes workspace_id="" to api_util.list_workspaces. Just wanted to confirm this empty string is the intended way to signal "list all workspaces" rather than None?

🤖 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 642 - 657, Add a limit parameter to
list_workspaces (e.g., def list_workspaces(self, name: str | None = None, *,
name_filter: Callable | None = None, limit: int | None = None) ->
list[WorkspaceResponse]) and pass it through to api_util.list_workspaces as
limit=limit for API consistency with
list_connections/list_sources/list_destinations; keep the workspace_id argument
as-is only if api_util.list_workspaces expects an empty string to mean “all”
else change workspace_id="" to workspace_id=None so the call signature to
api_util.list_workspaces (the symbol to update) matches the utility’s expected
sentinel for listing all workspaces.
airbyte/cloud/organizations.py (1)

73-76: ⚡ Quick win

Bare except Exception may silently hide important errors.

The broad exception handler catches and suppresses all errors without logging. While the resilient fallback to cached data is nice, this could make debugging auth failures or network issues difficult. Would you consider logging the exception (at debug or warning level) before returning the fallback, wdyt?

         except Exception:
+            # TODO: Consider logging the exception for debugging
             if self._organization_info is None:
                 self._organization_info_fetch_failed = True
             return self._organization_info or {}
🤖 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 73 - 76, The bare except in the
method that returns organization info swallows all errors; update the except
block in the function/method that accesses self._organization_info (the block
using self._organization_info_fetch_failed) to catch Exception as e and log the
exception before returning the fallback. Specifically, change the handler to
"except Exception as e:" and call the module/class logger (e.g., self._logger or
a module-level logger) to log e at debug or warning level with context like
"failed to fetch organization info", then preserve the current behavior of
setting self._organization_info_fetch_failed and returning
self._organization_info or {}.
airbyte/cloud/client.py (1)

30-50: ⚡ Quick win

Should __init__ validate credentials like from_auth does?

The __init__ method bypasses the credential validation in _AirbyteCredentials.from_auth. Users calling CloudClient() without any credentials will succeed here but fail later at runtime when making API calls. Would it make sense to either:

  1. Add validation here to fail fast, or
  2. Mark __init__ as internal (e.g., rename to _init pattern or document that from_auth is the preferred entry point)?

This could prevent confusing deferred errors, 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/client.py` around lines 30 - 50, The __init__ of CloudClient
currently constructs _AirbyteCredentials directly and skips the validation logic
in _AirbyteCredentials.from_auth, allowing callers to instantiate CloudClient
without credentials and only fail later; update CloudClient.__init__ to perform
the same validation by calling _AirbyteCredentials.from_auth (or delegating
credential validation to that method) using the incoming client_id,
client_secret, bearer_token, public_api_root, config_api_root, workspace_id, and
organization_id, and assign the returned validated credentials to
self._credentials (or raise a clear ValueError if validation fails) so
construction fails fast.
🤖 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/cloud/client.py`:
- Around line 30-50: The __init__ of CloudClient currently constructs
_AirbyteCredentials directly and skips the validation logic in
_AirbyteCredentials.from_auth, allowing callers to instantiate CloudClient
without credentials and only fail later; update CloudClient.__init__ to perform
the same validation by calling _AirbyteCredentials.from_auth (or delegating
credential validation to that method) using the incoming client_id,
client_secret, bearer_token, public_api_root, config_api_root, workspace_id, and
organization_id, and assign the returned validated credentials to
self._credentials (or raise a clear ValueError if validation fails) so
construction fails fast.

In `@airbyte/cloud/organizations.py`:
- Around line 73-76: The bare except in the method that returns organization
info swallows all errors; update the except block in the function/method that
accesses self._organization_info (the block using
self._organization_info_fetch_failed) to catch Exception as e and log the
exception before returning the fallback. Specifically, change the handler to
"except Exception as e:" and call the module/class logger (e.g., self._logger or
a module-level logger) to log e at debug or warning level with context like
"failed to fetch organization info", then preserve the current behavior of
setting self._organization_info_fetch_failed and returning
self._organization_info or {}.

In `@airbyte/cloud/workspaces.py`:
- Around line 642-657: Add a limit parameter to list_workspaces (e.g., def
list_workspaces(self, name: str | None = None, *, name_filter: Callable | None =
None, limit: int | None = None) -> list[WorkspaceResponse]) and pass it through
to api_util.list_workspaces as limit=limit for API consistency with
list_connections/list_sources/list_destinations; keep the workspace_id argument
as-is only if api_util.list_workspaces expects an empty string to mean “all”
else change workspace_id="" to workspace_id=None so the call signature to
api_util.list_workspaces (the symbol to update) matches the utility’s expected
sentinel for listing all workspaces.

In `@airbyte/mcp/cloud.py`:
- Around line 1409-1425: Change _resolve_organization_id to accept an
already-instantiated CloudClient instead of separate credential params and stop
creating a new CloudClient inside it; update the caller in this diff to call
_resolve_organization_id(client=client, organization_id=organization_id,
organization_name=organization_name) (remove
api_root/client_id/client_secret/bearer_token/config_api_root args), and inside
_resolve_organization_id use the passed CloudClient methods/properties to
resolve org id; ensure all other call sites of _resolve_organization_id are
updated to pass a CloudClient and adjust type hints/tests accordingly, or
alternatively implement org-name resolution inside CloudClient.list_workspaces
and call that from here.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 028f3584-3a40-4bde-983e-86ff29af35f8

📥 Commits

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

📒 Files selected for processing (9)
  • 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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 19, 2026

PyTest Results (Fast Tests Only, No Creds)

445 tests  +16   445 ✅ +16   5m 58s ⏱️ -3s
  1 suites ± 0     0 💤 ± 0 
  1 files   ± 0     0 ❌ ± 0 

Results for commit 0b8df08. ± Comparison against base commit 70a7d82.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 19, 2026

PyTest Results (Full)

515 tests  +16   497 ✅ +16   25m 8s ⏱️ +8s
  1 suites ± 0    18 💤 ± 0 
  1 files   ± 0     0 ❌ ± 0 

Results for commit 0b8df08. ± 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 20, 2026 00:47
Copilot AI review requested due to automatic review settings May 20, 2026 00:47
@aaronsteers Aaron ("AJ") Steers (aaronsteers) changed the title feat(cloud): add CloudClient and refactor CloudOrganization (core only) feat(cloud): add CloudClient, move CloudOrganization to dedicated cloud.organizations module May 20, 2026
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 the “core” Airbyte Cloud client/auth refactor into a standalone change: it introduces a unified CloudClient facade, centralizes credential resolution in an internal _AirbyteCredentials helper, and moves CloudOrganization into its own module, with MCP updated to use the new client.

Changes:

  • Added CloudClient (airbyte/cloud/client.py) with from_auth(env_vars=...), workspace listing, workspace construction, and organization resolution.
  • Introduced internal _AirbyteCredentials (airbyte/cloud/_credentials.py) for shared auth/env-var resolution and added CLOUD_ORGANIZATION_ID_ENV_VAR.
  • Moved CloudOrganization into airbyte/cloud/organizations.py, updated exports/imports, and refactored MCP cloud tools to use CloudClient.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
airbyte/cloud/client.py New CloudClient facade: auth factory, workspace/org helpers, workspace listing.
airbyte/cloud/_credentials.py New internal credential/env-var resolution shared by Cloud client/workspace/org.
airbyte/cloud/organizations.py New CloudOrganization module with lazy org info fetching/caching.
airbyte/cloud/workspaces.py Refactors workspace auth initialization to use _AirbyteCredentials; adds list_workspaces().
airbyte/cloud/__init__.py Exports CloudClient / CloudOrganization and exposes new submodules.
airbyte/mcp/cloud.py Refactors MCP cloud tooling to construct/auth via CloudClient.
airbyte/constants.py Adds CLOUD_ORGANIZATION_ID_ENV_VAR.
airbyte/_util/api_imports.py Exposes WorkspaceResponse for typing usage.
tests/unit_tests/test_cloud_credentials.py Adds unit coverage for credential resolution, list_workspaces forwarding, and org caching behavior.

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

Comment thread airbyte/cloud/_credentials.py Outdated
Comment thread airbyte/cloud/_credentials.py Outdated
Comment thread airbyte/cloud/client.py
Comment thread airbyte/cloud/client.py
Comment thread airbyte/cloud/workspaces.py
Comment thread airbyte/cloud/workspaces.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 2 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment thread airbyte/cloud/workspaces.py
Comment thread airbyte/cloud/workspaces.py
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 (3)
airbyte/mcp/cloud.py (2)

1343-1346: 💤 Low value

Stale docstring after refactoring?

The docstring says "This is a convenience wrapper around _resolve_organization" but the function now directly calls client.get_organization() instead. Would you like to update this to reflect the new implementation, wdyt?

 def _resolve_organization_id(
     organization_id: str | None,
     organization_name: str | None,
     *,
     client: CloudClient,
 ) -> str:
-    """Resolve organization ID from either ID or exact name match.
-
-    This is a convenience wrapper around _resolve_organization that returns just the ID.
-    """
+    """Resolve organization ID from either ID or exact name match."""
     org = client.get_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/mcp/cloud.py` around lines 1343 - 1346, The docstring for
resolve_organization_id is stale — it mentions being a “convenience wrapper
around _resolve_organization” while the implementation now calls
client.get_organization(); update the docstring to accurately describe the
current behavior: state that resolve_organization_id resolves and returns the
organization ID by calling client.get_organization() to fetch an organization by
ID or exact name (and mention any parameters/return value expectations), and
remove the reference to _resolve_organization so the text matches the code.

1290-1335: ⚡ Quick win

Consider removing _resolve_organization as it's no longer used.

The function at line 1290 is dead code after the refactoring. While _resolve_organization_id mentions it in the docstring as a "convenience wrapper," the actual implementation calls client.get_organization() directly instead. No callers of _resolve_organization exist anywhere in the codebase. Removing it would clean up unnecessary code and reduce maintenance burden—should we take care of this?

🤖 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 1290 - 1335, The function
_resolve_organization is dead code and should be removed; delete the entire
_resolve_organization function and any leftover references to it, and update the
docstring of _resolve_organization_id to remove the "convenience wrapper"
mention (or point it to CloudClient.get_organization) so documentation stays
accurate; ensure no callers remain and that behavior continues to use
CloudClient.get_organization for organization resolution.
airbyte/cloud/client.py (1)

30-50: ⚡ Quick win

Should __init__ validate credentials like from_auth does?

The __init__ constructor directly creates _AirbyteCredentials without calling from_auth, which means it bypasses the validation for mixed auth methods (bearer + client creds) and partial client credentials. A user could create CloudClient(bearer_token="x", client_id="y") without an immediate error.

Was this intentional to allow more flexibility for direct construction, or would you want to add validation here too, 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/client.py` around lines 30 - 50, The CloudClient.__init__
currently instantiates _AirbyteCredentials directly and therefore skips the
validation logic in _AirbyteCredentials.from_auth that prevents mixed auth
(bearer + client creds) and incomplete client credentials; update __init__ to
perform the same validation path as from_auth (e.g., call
_AirbyteCredentials.from_auth or replicate its checks) so invalid combinations
like supplying both bearer_token and client_id or partial client credentials
raise immediately, ensuring consistent behavior between CloudClient.__init__ and
CloudClient.from_auth.
🤖 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/cloud/client.py`:
- Around line 30-50: The CloudClient.__init__ currently instantiates
_AirbyteCredentials directly and therefore skips the validation logic in
_AirbyteCredentials.from_auth that prevents mixed auth (bearer + client creds)
and incomplete client credentials; update __init__ to perform the same
validation path as from_auth (e.g., call _AirbyteCredentials.from_auth or
replicate its checks) so invalid combinations like supplying both bearer_token
and client_id or partial client credentials raise immediately, ensuring
consistent behavior between CloudClient.__init__ and CloudClient.from_auth.

In `@airbyte/mcp/cloud.py`:
- Around line 1343-1346: The docstring for resolve_organization_id is stale — it
mentions being a “convenience wrapper around _resolve_organization” while the
implementation now calls client.get_organization(); update the docstring to
accurately describe the current behavior: state that resolve_organization_id
resolves and returns the organization ID by calling client.get_organization() to
fetch an organization by ID or exact name (and mention any parameters/return
value expectations), and remove the reference to _resolve_organization so the
text matches the code.
- Around line 1290-1335: The function _resolve_organization is dead code and
should be removed; delete the entire _resolve_organization function and any
leftover references to it, and update the docstring of _resolve_organization_id
to remove the "convenience wrapper" mention (or point it to
CloudClient.get_organization) so documentation stays accurate; ensure no callers
remain and that behavior continues to use CloudClient.get_organization for
organization resolution.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c784ebe3-bb34-41ea-ba38-ae2bf848aa24

📥 Commits

Reviewing files that changed from the base of the PR and between 9c715cc and 25d4d21.

📒 Files selected for processing (6)
  • 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

@devin-ai-integration
Copy link
Copy Markdown
Contributor

Addressed the three latest CodeRabbit follow-ups in ab4a890:

  • Removed the stale _resolve_organization_id() docstring reference.
  • Removed the confirmed-dead _resolve_organization() helper and unused imports.
  • Updated CloudClient.__init__() to use _AirbyteCredentials.from_auth(..., env_vars=False) so direct construction validates mixed and partial credential inputs consistently with from_auth().
  • Added focused constructor-validation coverage.

Validation:

  • uv run ruff format airbyte/cloud/client.py airbyte/mcp/cloud.py tests/unit_tests/test_cloud_credentials.py
  • uv run ruff check airbyte/cloud/client.py airbyte/mcp/cloud.py tests/unit_tests/test_cloud_credentials.py
  • uv run pytest tests/unit_tests/test_cloud_credentials.py — 14 passed
  • uv run pyrefly check — 0 errors

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
airbyte/cloud/client.py (1)

224-239: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Could get_organization() fall back to the client's default organization_id here?

Right now CloudClient(bearer_token="...", organization_id="org-id").get_organization() still raises PyAirbyteInputError because this method only looks at its parameters. get_workspace() and list_workspaces() already honor defaults stored on the client, so this makes the new public API inconsistent on the happy path. wdyt?

Possible fix
     def get_organization(
         self,
         organization_id: str | None = None,
         *,
         organization_name: str | None = None,
     ) -> CloudOrganization:
         """Resolve an organization by ID or exact name."""
-        if organization_id and organization_name:
+        resolved_organization_id = organization_id or self.organization_id
+
+        if resolved_organization_id and organization_name:
             raise exc.PyAirbyteInputError(
                 message="Provide either organization ID or organization name."
             )
-        if not organization_id and not organization_name:
+        if not resolved_organization_id and not organization_name:
             raise exc.PyAirbyteInputError(
                 message="Organization ID or organization name is required."
             )
@@
-        if organization_id:
+        if resolved_organization_id:
             matching_organizations = [
                 organization
                 for organization in organizations
-                if organization.organization_id == organization_id
+                if organization.organization_id == resolved_organization_id
             ]
         else:
             matching_organizations = [
                 organization
                 for organization in organizations
@@
         if not matching_organizations:
             raise AirbyteMissingResourceError(
                 resource_type="organization",
-                resource_name_or_id=organization_id or organization_name,
+                resource_name_or_id=resolved_organization_id or organization_name,
             )
🤖 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 224 - 239, get_organization currently
only checks its parameters and raises PyAirbyteInputError if neither
organization_id nor organization_name is passed; update it to fall back to the
client's default organization_id (e.g. self.organization_id) when
organization_id is None so it behaves like get_workspace()/list_workspaces.
Concretely: in CloudClient.get_organization, when organization_id is falsy and
organization_name is not provided, use self.organization_id before raising an
error; keep the mutual-exclusion check (if both provided) and ensure the
validation messages remain correct and types handle Optional[str] for
organization_id and organization_name.
🧹 Nitpick comments (1)
airbyte/mcp/cloud.py (1)

1295-1298: ⚡ Quick win

Could this return the explicit organization_id directly when it's already provided?

Even ID-based calls now go through client.get_organization(...), so list_cloud_workspaces(..., organization_id=...) pays for an extra organization lookup before doing the real work. That adds avoidable latency and another network/permission failure surface to the direct-ID path. wdyt?

Possible simplification
 def _resolve_organization_id(
     organization_id: str | None,
     organization_name: str | None,
     *,
     client: CloudClient,
 ) -> str:
     """Resolve organization ID from either ID or exact name match."""
+    if organization_id is not None:
+        return organization_id
+
     org = client.get_organization(
         organization_id=organization_id,
         organization_name=organization_name,
     )
     return org.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/mcp/cloud.py` around lines 1295 - 1298, When organization_id is
supplied to list_cloud_workspaces, avoid calling client.get_organization
unnecessarily: if organization_id is not None, use it directly instead of
invoking client.get_organization(organization_id=organization_id,
organization_name=organization_name); only call client.get_organization when
organization_id is None (or when you need to resolve organization_name to an
id). Update the logic in list_cloud_workspaces to short-circuit on a provided
organization_id, returning or using that id for subsequent work to eliminate the
extra lookup and network/permission surface.
🤖 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.

Outside diff comments:
In `@airbyte/cloud/client.py`:
- Around line 224-239: get_organization currently only checks its parameters and
raises PyAirbyteInputError if neither organization_id nor organization_name is
passed; update it to fall back to the client's default organization_id (e.g.
self.organization_id) when organization_id is None so it behaves like
get_workspace()/list_workspaces. Concretely: in CloudClient.get_organization,
when organization_id is falsy and organization_name is not provided, use
self.organization_id before raising an error; keep the mutual-exclusion check
(if both provided) and ensure the validation messages remain correct and types
handle Optional[str] for organization_id and organization_name.

---

Nitpick comments:
In `@airbyte/mcp/cloud.py`:
- Around line 1295-1298: When organization_id is supplied to
list_cloud_workspaces, avoid calling client.get_organization unnecessarily: if
organization_id is not None, use it directly instead of invoking
client.get_organization(organization_id=organization_id,
organization_name=organization_name); only call client.get_organization when
organization_id is None (or when you need to resolve organization_name to an
id). Update the logic in list_cloud_workspaces to short-circuit on a provided
organization_id, returning or using that id for subsequent work to eliminate the
extra lookup and network/permission surface.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 72eee534-3bd9-48b8-9317-aefaf6a561e8

📥 Commits

Reviewing files that changed from the base of the PR and between 25d4d21 and ab4a890.

📒 Files selected for processing (3)
  • airbyte/cloud/client.py
  • airbyte/mcp/cloud.py
  • tests/unit_tests/test_cloud_credentials.py

@devin-ai-integration
Copy link
Copy Markdown
Contributor

Confirmed this was a real consistency gap. Addressed in 0b8df08: CloudClient.get_organization() now falls back to the client’s stored organization_id when no explicit organization_id or organization_name is provided, and the missing-resource context uses the resolved ID.

Also added focused coverage for CloudClient(bearer_token="...", organization_id="default-org").get_organization().

Validation:

  • uv run ruff format airbyte/cloud/client.py airbyte/mcp/cloud.py tests/unit_tests/test_cloud_credentials.py
  • uv run ruff check airbyte/cloud/client.py airbyte/mcp/cloud.py tests/unit_tests/test_cloud_credentials.py
  • uv run pytest tests/unit_tests/test_cloud_credentials.py — 16 passed
  • uv run pyrefly check — 0 errors

Devin session

@aaronsteers Aaron ("AJ") Steers (aaronsteers) merged commit 8a2b207 into main May 20, 2026
21 checks passed
@aaronsteers Aaron ("AJ") Steers (aaronsteers) deleted the devin/1779226376-cloud-core-refactor branch May 20, 2026 01:46
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