Skip to content

feat: add optional extras for cloud storage and notification clients#2163

Open
devin-ai-integration[bot] wants to merge 6 commits intomasterfrom
devin/1774117812-optional-extras
Open

feat: add optional extras for cloud storage and notification clients#2163
devin-ai-integration[bot] wants to merge 6 commits intomasterfrom
devin/1774117812-optional-extras

Conversation

@devin-ai-integration
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot commented Mar 21, 2026

Summary

Phase 1 of splitting cloud storage and notification clients into optional extras (#2155). This is a safe, backwards-compatible step — no existing installs break.

What changed (3 files)

  1. pyproject.toml: Cloud/notification deps (boto3, google-cloud-storage, slack-sdk, azure-storage-blob, pymsteams) remain required but are grouped with a comment explaining they will become optional in Phase 2. Empty extras markers (s3, gcs, azure, slack, teams) are defined so that pip install elementary-data[s3] is valid today (no-op) and will pull the real dep once Phase 2 flips them to optional.

  2. config.py: google.auth is now imported inline inside has_gcloud instead of at module top-level. This prevents unconditional loading of the module (addresses khoaanguyenn's comment). The except DefaultCredentialsError was broadened to except Exception to avoid importing the exception class at the top level.

  3. data_monitoring_report.py: Cloud storage and notification clients (S3, GCS, Azure, Slack) are now only imported and initialized when the config indicates they are needed (has_s3, has_gcs, has_blob, has_slack). Each is wrapped in a try/except ImportError with a warning that tells the user which extra to install.

Migration plan

  • Phase 1 (this PR): Extras exist but deps are still required. Zero user impact.
  • Phase 2 (future release): Announce to users they should add the relevant extras.
  • Phase 3 (subsequent release): Flip deps to optional = true and populate the extras.

Review & Testing Checklist for Human

  • Broad except Exception in config.py:has_gcloud (line ~272): The original code caught only DefaultCredentialsError. Now catches ImportError (new) plus a blanket Exception. Verify this doesn't silently mask real credential-configuration errors that users should see.
  • Downstream None client handling: s3_client, gcs_client, azure_client, and slack_client now default to None when the config flag is false. Verify that send_report and other downstream methods handle None gracefully (check the _send_to_* methods).
  • Empty extras are valid: Confirm pip install 'elementary-data[s3]' succeeds as a no-op on a clean install. Poetry accepts s3 = [] but verify pip/build-backend behavior.
  • Warning messages reference future extras: The ImportError warnings say e.g. pip install 'elementary-data[s3]' — in Phase 1 that extra is empty, so the instruction wouldn't actually help. This is benign (deps are required, so ImportError can't fire today) but worth noting.

Suggested test plan: Install from this branch with pip install -e . (no extras). Verify all clients initialize correctly when config flags are set. Then simulate Phase 2 by manually removing e.g. boto3 from the venv and confirm the warning fires and the report still generates without the S3 client.

Notes

  • All mypy errors in pre-commit are pre-existing on master (missing type stubs for tabulate, ratelimit, dateutil, requests). No new mypy errors introduced.
  • The earlier revision of this PR touched 14 files with a deps.py helper — that was simplified per reviewer feedback to only touch the orchestration layer.

Link to Devin session: https://app.devin.ai/sessions/dbb0946ed052408ba49bb689d39632a2
Requested by: @haritamar


Open with Devin

Summary by CodeRabbit

  • Refactor
    • Cloud service clients now load lazily and initialize on demand, reducing startup overhead and avoiding failures when optional cloud packages are absent.
    • Cloud integration dependencies reorganized in preparation for optional-extras installation.
    • Slack message formatting now relies on internal length limits for safer truncation and consistent messages.
    • Google auth detection made optional at runtime, improving resilience when cloud auth libraries are not installed.

Add pip extras (s3, gcs, azure, slack, teams) for optional dependencies.
Make all cloud/notification imports lazy to prevent unconditional loading.
Dependencies remain in default install for backwards compatibility (Phase 1).

Closes #2155

Co-Authored-By: Itamar Hartstein <haritamar@gmail.com>
@devin-ai-integration
Copy link
Contributor Author

🤖 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
Contributor

👋 @devin-ai-integration[bot]
Thank you for raising your pull request.
Please make sure to add tests and document all user-facing changes.
You can do this by editing the docs files in this pull request.

@coderabbitai
Copy link

coderabbitai bot commented Mar 21, 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

Module-level imports for optional cloud and messaging integrations were removed and replaced with lazy/runtime imports; Slack text-length constants were internalized; pyproject updated to prepare optional extras. Config.has_gcloud now imports google.auth at runtime and handles ImportError separately.

Changes

Cohort / File(s) Summary
Config change
elementary/config/config.py
Config.has_gcloud now performs a runtime import of google.auth, catches ImportError (returns False when missing) and then handles google.auth.exceptions.DefaultCredentialsError during google.auth.default() invocation.
Report client lazy loading
elementary/monitor/data_monitoring/report/data_monitoring_report.py
Removed module-level S3/GCS/Azure/Slack client imports. Clients are initialized lazily in __init__ based on config flags with try/except ImportError and logged warnings; anonymous_warehouse_id condensed to a conditional expression.
Integration imports delayed
elementary/monitor/data_monitoring/alerts/integrations/integrations.py
Integration classes moved from module scope into get_integration and get_destination so imports occur only when config.has_slack/config.has_teams or specific destinations are evaluated.
Slack-related message limits localized
elementary/clients/slack/slack_message_builder.py, elementary/messages/formats/block_kit.py, elementary/monitor/data_monitoring/alerts/integrations/slack/slack.py
Removed reliance on slack_sdk.models.blocks for text max lengths; replaced with local constants (_HEADER_TEXT_MAX_LENGTH, _SECTION_TEXT_MAX_LENGTH) and updated truncation logic to use these constants.
Dependency metadata / extras
pyproject.toml
Cloud/messaging deps (boto3, google-cloud-storage, slack-sdk, azure-storage-blob, pymsteams) repositioned with comments about future optional-extras migration; new empty extras defined for s3, gcs, azure, slack, and teams (Phase 1).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble imports in the night,
Lazy-loading keeps things light,
Text limits tucked in my burrow tight,
Extras waiting for spring's bright light,
Hop—no crash—deploys feel right ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: introducing optional extras for cloud storage and notification clients as part of Phase 1 refactoring.

✏️ 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/1774117812-optional-extras

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

- Revert individual client file changes (no more lazy imports in clients)
- Remove deps.py helper (not needed)
- Only init clients in data_monitoring_report.py if config says they're needed
- Inline imports with ImportError handling and actionable warnings
- Move cloud/notification deps to optional=true in pyproject.toml
- Add blank lines between dbt adapter extras and integration extras
- Add integration extras to 'all' meta-extra
- Keep google.auth lazy import in config.py has_gcloud property

Co-Authored-By: Itamar Hartstein <haritamar@gmail.com>
Copy link

@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: 2

🧹 Nitpick comments (3)
elementary/clients/gcs/client.py (1)

92-103: Consider restoring return type annotation via TYPE_CHECKING.

The get_credentials method lost its return type annotation (previously -> Credentials). For type safety and IDE support, consider restoring it using a TYPE_CHECKING guard:

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from google.auth.credentials import Credentials

`@staticmethod`
def get_credentials(config: Config) -> "Credentials":

The lazy import pattern itself is correctly implemented with consistent use of the "gcs" extra.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@elementary/clients/gcs/client.py` around lines 92 - 103, Restore the return
type annotation for get_credentials by adding a TYPE_CHECKING guard import for
Credentials and annotating the static method as returning "Credentials";
specifically, add from typing import TYPE_CHECKING and inside an if
TYPE_CHECKING: block import Credentials from google.auth.credentials, then
change def get_credentials(config: Config) to def get_credentials(config:
Config) -> "Credentials" so the lazy import pattern (import_optional_dependency)
remains unchanged.
elementary/messages/formats/block_kit.py (1)

107-109: Consider consolidating duplicated constants.

These constants duplicate the values in elementary/clients/slack/slack_message_builder.py (lines 8-10). While acceptable for Phase 1, consider extracting them to a shared location (e.g., a slack_constants.py module) to ensure consistency and simplify future maintenance if Slack ever changes these limits.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@elementary/messages/formats/block_kit.py` around lines 107 - 109, The two
Slack Block Kit limits (_SECTION_TEXT_MAX_LENGTH and _HEADER_TEXT_MAX_LENGTH)
are duplicated; extract them into a shared constants module (e.g., create
slack_constants with those constant names) and replace the local definitions in
both block_kit.py and slack_message_builder.py by importing from that module;
update references to use the imported names and run tests to ensure no import
cycles.
elementary/monitor/data_monitoring/alerts/integrations/slack/slack.py (1)

8-11: Avoid depending on another module's private Slack size limit.

SlackAlertMessageBuilder is local to this package, but the guard now imports _SECTION_TEXT_MAX_LENGTH from elementary.clients.slack.slack_message_builder. That makes this path depend on a private symbol in a different builder module. Please move the limit to a shared public constant/helper and import that from both places.

Also applies to: 242-242

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@elementary/monitor/data_monitoring/alerts/integrations/slack/slack.py` around
lines 8 - 11, The code imports the private constant _SECTION_TEXT_MAX_LENGTH
from elementary.clients.slack.slack_message_builder, creating an improper
dependency; extract that size limit into a shared public constant (e.g.,
SECTION_TEXT_MAX_LENGTH) in a common module or a public utils file and update
both SlackAlertMessageBuilder and the original slack_message_builder to import
the new public constant instead of the private one; ensure references to
_SECTION_TEXT_MAX_LENGTH are replaced with the new SECTION_TEXT_MAX_LENGTH
symbol throughout the package.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@elementary/clients/slack/slack_message_builder.py`:
- Around line 8-10: The two Slack Block Kit limit constants are defined with
leading underscores but are imported externally (violates private naming);
rename _HEADER_TEXT_MAX_LENGTH and _SECTION_TEXT_MAX_LENGTH to public
HEADER_TEXT_MAX_LENGTH and SECTION_TEXT_MAX_LENGTH and update all
references/imports (e.g., the import in slack.py under
elementar/monitor/data_monitoring/alerts/integrations/slack/slack.py) to use the
new symbol names, or alternatively stop importing them and duplicate the
constants or expose a getter function—prefer renaming to make them publicly
accessible.

In `@elementary/config/config.py`:
- Around line 266-275: Narrow the exception handling in has_gcloud: remove the
broad except ImportError/Exception blocks so
import_optional_dependency("google.auth", "gcs") can raise its install-time
ImportError, then only catch google_auth.exceptions.DefaultCredentialsError from
google_auth.default() (e.g., except
google_auth.exceptions.DefaultCredentialsError: return False) so credential
errors are handled but other unexpected errors propagate.

---

Nitpick comments:
In `@elementary/clients/gcs/client.py`:
- Around line 92-103: Restore the return type annotation for get_credentials by
adding a TYPE_CHECKING guard import for Credentials and annotating the static
method as returning "Credentials"; specifically, add from typing import
TYPE_CHECKING and inside an if TYPE_CHECKING: block import Credentials from
google.auth.credentials, then change def get_credentials(config: Config) to def
get_credentials(config: Config) -> "Credentials" so the lazy import pattern
(import_optional_dependency) remains unchanged.

In `@elementary/messages/formats/block_kit.py`:
- Around line 107-109: The two Slack Block Kit limits (_SECTION_TEXT_MAX_LENGTH
and _HEADER_TEXT_MAX_LENGTH) are duplicated; extract them into a shared
constants module (e.g., create slack_constants with those constant names) and
replace the local definitions in both block_kit.py and slack_message_builder.py
by importing from that module; update references to use the imported names and
run tests to ensure no import cycles.

In `@elementary/monitor/data_monitoring/alerts/integrations/slack/slack.py`:
- Around line 8-11: The code imports the private constant
_SECTION_TEXT_MAX_LENGTH from elementary.clients.slack.slack_message_builder,
creating an improper dependency; extract that size limit into a shared public
constant (e.g., SECTION_TEXT_MAX_LENGTH) in a common module or a public utils
file and update both SlackAlertMessageBuilder and the original
slack_message_builder to import the new public constant instead of the private
one; ensure references to _SECTION_TEXT_MAX_LENGTH are replaced with the new
SECTION_TEXT_MAX_LENGTH symbol throughout the package.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: deab9bd7-205c-45b4-8956-53ebca216431

📥 Commits

Reviewing files that changed from the base of the PR and between ed170b7 and 54bdef6.

📒 Files selected for processing (14)
  • elementary/clients/azure/client.py
  • elementary/clients/gcs/client.py
  • elementary/clients/s3/client.py
  • elementary/clients/slack/client.py
  • elementary/clients/slack/slack_message_builder.py
  • elementary/config/config.py
  • elementary/messages/formats/block_kit.py
  • elementary/messages/messaging_integrations/slack_web.py
  • elementary/messages/messaging_integrations/slack_webhook.py
  • elementary/monitor/data_monitoring/alerts/integrations/integrations.py
  • elementary/monitor/data_monitoring/alerts/integrations/slack/slack.py
  • elementary/monitor/data_monitoring/report/data_monitoring_report.py
  • elementary/utils/deps.py
  • pyproject.toml

Comment on lines 266 to 275
from elementary.utils.deps import import_optional_dependency

google_auth = import_optional_dependency("google.auth", "gcs")
google_auth.default()
return True
except DefaultCredentialsError:
except ImportError:
return False
except Exception:
# google.auth.exceptions.DefaultCredentialsError or similar
return False
Copy link

@coderabbitai coderabbitai bot Mar 21, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

According to the public google-auth docs/API, what exception does google.auth.default()raise when application default credentials are unavailable, and isgoogle.auth.exceptions.DefaultCredentialsError the correct type to catch across supported versions?

💡 Result:

google.auth.default() raises google.auth.exceptions.DefaultCredentialsError when it cannot find usable Application Default Credentials (ADC) (i.e., “no credentials were found” / credentials are invalid). This is explicitly documented in the default() docstring/reference docs and is the exception type the implementation raises in that situation. [1], [2]

Yes—google.auth.exceptions.DefaultCredentialsError is the correct, public exception type to catch for “ADC unavailable” across supported google-auth versions (it’s a documented public exception in google.auth.exceptions, and default() documents/uses it). If you want a broader catch-all for auth-library failures, catch its base class google.auth.exceptions.GoogleAuthError. [1], [3]

Sources:
[1] google-auth implementation docs for google.auth._default.default() (Raises DefaultCredentialsError if no credentials found/invalid) (google-auth.readthedocs.io)
[2] google-auth “stable” PDF docs (ADC: “If no credentials are found, DefaultCredentialsError will be raised.”) (google-auth.readthedocs.io)
[3] google.auth.exceptions reference docs (DefaultCredentialsError is a public exception, used when acquiring default credentials failed; base is GoogleAuthError) (google-auth.readthedocs.io)

Citations:


🏁 Script executed:

# First, find and examine import_optional_dependency
find . -type f -name "*.py" | xargs grep -l "def import_optional_dependency" | head -1

Repository: elementary-data/elementary

Length of output: 96


🏁 Script executed:

# Also find the GCS client usage
find . -type f -name "*.py" -path "*/gcs/*" | head -10

Repository: elementary-data/elementary

Length of output: 141


🏁 Script executed:

# Check the config.py context around the changed lines
cat -n elementary/config/config.py | sed -n '250,290p'

Repository: elementary-data/elementary

Length of output: 1519


🏁 Script executed:

# Examine import_optional_dependency implementation
cat -n elementary/utils/deps.py

Repository: elementary-data/elementary

Length of output: 1086


🏁 Script executed:

# Check how has_gcs is used in the GCS client
cat -n elementary/clients/gcs/client.py | head -50

Repository: elementary-data/elementary

Length of output: 2317


🏁 Script executed:

# Let's verify the broader context and ensure no other exception types
# from google-auth are raised by default() under other conditions
cat -n elementary/utils/deps.py | tail -10

Repository: elementary-data/elementary

Length of output: 444


🏁 Script executed:

# Check if google.auth.exceptions is importable via the same mechanism
grep -r "google.auth.exceptions" elementary/ --include="*.py" | head -5

Repository: elementary-data/elementary

Length of output: 169


🏁 Script executed:

# Check the complete context and see if there are any other considerations
# around exception handling in this method or related code
grep -B 5 -A 15 "def has_gcloud" elementary/config/config.py

Repository: elementary-data/elementary

Length of output: 669


Narrow exception handling in has_gcloud to avoid masking errors.

The except ImportError at line 271 suppresses the actionable install message from import_optional_dependency("google.auth", "gcs"). The except Exception at line 273 masks the real google.auth.exceptions.DefaultCredentialsError from google.auth.default() when credentials are misconfigured. This makes a configured GCS bucket appear unavailable instead of failing with the actual root cause.

Catch only google.auth.exceptions.DefaultCredentialsError from default(), and let import_optional_dependency raise its own error if the module is missing:

Suggested fix
-        try:
-            from elementary.utils.deps import import_optional_dependency
-
-            google_auth = import_optional_dependency("google.auth", "gcs")
-            google_auth.default()
-            return True
-        except ImportError:
-            return False
-        except Exception:
-            # google.auth.exceptions.DefaultCredentialsError or similar
-            return False
+        from elementary.utils.deps import import_optional_dependency
+
+        google_auth = import_optional_dependency("google.auth", "gcs")
+        google_auth_exceptions = import_optional_dependency(
+            "google.auth.exceptions", "gcs"
+        )
+        try:
+            google_auth.default()
+            return True
+        except google_auth_exceptions.DefaultCredentialsError:
+            return False
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from elementary.utils.deps import import_optional_dependency
google_auth = import_optional_dependency("google.auth", "gcs")
google_auth.default()
return True
except DefaultCredentialsError:
except ImportError:
return False
except Exception:
# google.auth.exceptions.DefaultCredentialsError or similar
return False
from elementary.utils.deps import import_optional_dependency
google_auth = import_optional_dependency("google.auth", "gcs")
google_auth_exceptions = import_optional_dependency(
"google.auth.exceptions", "gcs"
)
try:
google_auth.default()
return True
except google_auth_exceptions.DefaultCredentialsError:
return False
🧰 Tools
🪛 Ruff (0.15.6)

[warning] 273-273: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@elementary/config/config.py` around lines 266 - 275, Narrow the exception
handling in has_gcloud: remove the broad except ImportError/Exception blocks so
import_optional_dependency("google.auth", "gcs") can raise its install-time
ImportError, then only catch google_auth.exceptions.DefaultCredentialsError from
google_auth.default() (e.g., except
google_auth.exceptions.DefaultCredentialsError: return False) so credential
errors are handled but other unexpected errors propagate.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this still relevant?

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

Let me check the current state of the file on the PR branch.


🏁 Script executed:

#!/bin/bash
grep -n -A 20 "def has_gcloud" elementary/config/config.py

Length of output: 697


@haritamar — Partially. The code was changed to drop import_optional_dependency and revert to a plain import google.auth, so the first concern (suppressing the install-hint ImportError from import_optional_dependency) is no longer applicable.

However, the second concern still stands: the broad except Exception on line 272 still masks google.auth.exceptions.DefaultCredentialsError and any other unexpected errors when google.auth.default() fails (e.g. a misconfigured service account). The Ruff BLE001 warning also still fires on that line.

A narrower fix that matches the current code (no import_optional_dependency) would be:

`@property`
def has_gcloud(self):
    if self.google_service_account_path:
        return True
    try:
        import google.auth  # type: ignore[import]
        import google.auth.exceptions  # type: ignore[import]

        google.auth.default()
        return True
    except ImportError:
        return False
    except google.auth.exceptions.DefaultCredentialsError:
        return False

This keeps the ImportError guard for when google-auth isn't installed, but replaces the catch-all Exception with the precise public exception type, letting unexpected errors propagate instead of silently returning False.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, still relevant — I'll narrow the except Exception back to except google.auth.exceptions.DefaultCredentialsError (with inline import) to match the original behavior. Fixing in the same batch as the transitive import changes.

Copy link

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

Copy link

@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

♻️ Duplicate comments (1)
elementary/config/config.py (1)

270-274: ⚠️ Potential issue | 🟠 Major

Broad exception handling masks actionable errors.

The except Exception on line 272 catches all exceptions from google.auth.default(), including DefaultCredentialsError, but also unexpected errors like misconfiguration or bugs. This causes the property to silently return False when users may have intended to use GCS but have misconfigured credentials.

Consider narrowing to catch only google.auth.exceptions.DefaultCredentialsError so that other errors propagate and provide actionable feedback.

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@elementary/config/config.py` around lines 270 - 274, Change the broad
fallback that swallows all exceptions when calling google.auth.default():
instead of catching Exception, import and catch only
google.auth.exceptions.DefaultCredentialsError (keep the existing ImportError
handler). Update the except block around the google.auth.default() call to catch
DefaultCredentialsError so unexpected errors propagate and remain visible for
debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@elementary/monitor/data_monitoring/report/data_monitoring_report.py`:
- Around line 57-69: The code currently skips the GCS dependency warning when
google.auth is missing because self.config.has_gcs relies on has_gcloud; update
the initialization in data_monitoring_report.py to also check the explicit GCS
configuration (e.g., self.config.gcs_bucket_name or another flag indicating GCS
is configured) so the warning is shown whenever GCS is configured; locate the
block that sets self.gcs_client and change the condition from if
self.config.has_gcs to if self.config.has_gcs or self.config.gcs_bucket_name (or
equivalent), then keep the try/except ImportError around importing GCSClient and
calling GCSClient.create_client(self.config, tracking=self.tracking) so the
create_client call only happens when the import succeeds while the warning is
emitted whenever GCS is configured but dependencies are missing.

---

Duplicate comments:
In `@elementary/config/config.py`:
- Around line 270-274: Change the broad fallback that swallows all exceptions
when calling google.auth.default(): instead of catching Exception, import and
catch only google.auth.exceptions.DefaultCredentialsError (keep the existing
ImportError handler). Update the except block around the google.auth.default()
call to catch DefaultCredentialsError so unexpected errors propagate and remain
visible for debugging.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0947fe13-39d0-43ce-96e6-979a7bc3b3a5

📥 Commits

Reviewing files that changed from the base of the PR and between 54bdef6 and a4c320c.

📒 Files selected for processing (3)
  • elementary/config/config.py
  • elementary/monitor/data_monitoring/report/data_monitoring_report.py
  • pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml

devin-ai-integration bot and others added 3 commits March 21, 2026 19:03
…ntegrations extra

Co-Authored-By: Itamar Hartstein <haritamar@gmail.com>
…ompatibility

Co-Authored-By: Itamar Hartstein <haritamar@gmail.com>
…hase 1)

Co-Authored-By: Itamar Hartstein <haritamar@gmail.com>
Copy link
Contributor Author

@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 4 additional findings in Devin Review.

Open in Devin Review


# Cloud storage and notification integrations.
# These will move to optional extras in a future release (Phase 2).
# For now they remain required so existing installs are not broken.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🔴 slack_sdk still required at import time despite being marked optional in pyproject.toml

The PR moves slack-sdk to an optional dependency (pyproject.toml:45), but data_monitoring_report.py still has a top-level import of SlackReportSummaryMessageBuilder (line 14-16), which transitively requires slack_sdk through SlackMessageBuilderfrom slack_sdk.models.blocks import HeaderBlock, SectionBlock (elementary/clients/slack/slack_message_builder.py:4). The import chain cli/cli.py:9monitor/cli.py:9data_monitoring_report.py:14slack_report_summary_message_builder.py:4slack_message_builder.py:4slack_sdk means the entire edr CLI crashes with an ImportError when slack_sdk is not installed (i.e., pip install elementary-data without the [slack] or [integrations] extra). This defeats the purpose of making the dependency optional. The lazy import pattern was applied to SlackClient (lines 85-97) but not to SlackReportSummaryMessageBuilder. The same issue exists through DataMonitoringAlertsintegrations.pySlackIntegrationslack_sdk.

Prompt for agents
The slack-sdk dependency at pyproject.toml:45 is marked optional, but the codebase still has many top-level imports that transitively require it. The key paths that need fixing:

1. elementary/monitor/data_monitoring/report/data_monitoring_report.py lines 14-16: The top-level import of SlackReportSummaryMessageBuilder must be made lazy (moved inside send_test_results_summary method at line 347 where it's actually used), guarded by a try/except ImportError.

2. elementary/clients/slack/slack_message_builder.py line 4: from slack_sdk.models.blocks import HeaderBlock, SectionBlock - this is the root cause of the transitive dependency. Consider making this a lazy import or restructuring so that the constants (HeaderBlock.text_max_length, SectionBlock.text_max_length) are accessed without requiring slack_sdk at import time.

3. elementary/monitor/data_monitoring/alerts/integrations/integrations.py lines 9-13, 21-23: Top-level imports of SlackWebMessagingIntegration, SlackWebhookMessagingIntegration, and SlackIntegration all transitively depend on slack_sdk. These should be lazily imported inside the get_integration method (line 46).

4. elementary/monitor/data_monitoring/alerts/integrations/slack/slack.py line 6: from slack_sdk.models.blocks import SectionBlock is a top-level import.

All of these need to be made lazy imports to truly make slack-sdk optional.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a non-issue for Phase 1. slack-sdk (and all other cloud/notification deps) are required dependencies in this PR — they are NOT optional. The extras (s3 = [], gcs = [], azure = [], slack = [], teams = []) are empty markers that will be populated in Phase 2 when the deps actually become optional. Since slack-sdk is always installed today, the transitive import chain works fine.

The lazy import patterns in data_monitoring_report.py are forward-looking — they'll become meaningful in Phase 2 when the deps become truly optional.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think actually we should fix this regardless - as the original issue also mentions the ability to exclude dependencies manually via uv, so if we can support that for them it's meaningful.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point about uv manual exclusion — I'll make all transitive imports lazy so users who exclude these deps won't hit ImportErrors. Working on it now.

Comment on lines 272 to 274
except Exception:
# google.auth.exceptions.DefaultCredentialsError or similar
return False
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🟡 has_gcloud catches overly broad Exception, silently swallowing credential errors

The original has_gcloud only caught DefaultCredentialsError ("no credentials found"). The new code catches except Exception at line 272, which silently swallows legitimate errors like TransportError, RefreshError, or PermissionError during credential loading. When a user has configured GCS credentials but encounters a transient or fixable credential issue, has_gcloud will return False instead of propagating the error, causing GCS uploads to be silently skipped. The user will get no indication that their credential configuration is broken — the system will just behave as if GCS isn't configured.

Suggested change
except Exception:
# google.auth.exceptions.DefaultCredentialsError or similar
return False
except ImportError:
return False
except Exception:
# google.auth.exceptions.DefaultCredentialsError or similar
# Log the error so users can diagnose credential issues
logger.debug("Google Cloud credentials check failed", exc_info=True)
return False
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good observation about the broad except Exception. The original code caught only DefaultCredentialsError, but we broadened it to avoid importing the exception class at the top level. Adding a logger.debug here would be a reasonable improvement to avoid silently swallowing credential errors — will defer to the maintainer on whether to include that in this PR or handle separately.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Just making sure - we are no longer shadowing exceptions we didn't before? I prefer not to change behavior that we don't have to

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch — the original code only caught DefaultCredentialsError, so the except Exception broadens the behavior. I'll narrow it back to catch DefaultCredentialsError specifically (with an ImportError guard for when google.auth itself is missing). This way we don't change existing behavior.

…ion via uv

- Replace top-level slack_sdk.models.blocks imports with hardcoded constants
  in slack_message_builder.py, block_kit.py, and slack/slack.py
- Move Slack/Teams integration imports inside methods in integrations.py
- Move SlackReportSummaryMessageBuilder import inside method in data_monitoring_report.py
- Narrow config.py exception handling to catch DefaultCredentialsError specifically
- Use gcs_bucket_name instead of has_gcs for GCS client init check

Co-Authored-By: Itamar Hartstein <haritamar@gmail.com>
Copy link

@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

Caution

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

⚠️ Outside diff range comments (1)
elementary/clients/slack/slack_message_builder.py (1)

61-71: ⚠️ Potential issue | 🟡 Minor

Avoid truncating messages that are already at the limit.

At Line 61, using < causes a 3000-char message to be rewritten unnecessarily. Use <= so exact-limit payloads are left intact.

Proposed fix
-        if len(section_msg) < _SECTION_TEXT_MAX_LENGTH:
+        if len(section_msg) <= _SECTION_TEXT_MAX_LENGTH:
             return section_msg
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@elementary/clients/slack/slack_message_builder.py` around lines 61 - 71, The
condition in SlackMessageBuilder that checks section_msg length uses a strict
'<' which causes messages exactly equal to _SECTION_TEXT_MAX_LENGTH to be
truncated; update the check in the method handling section_msg so it uses '<='
instead of '<' to return the original section_msg when its length is exactly
_SECTION_TEXT_MAX_LENGTH (leave the truncation logic using _CONTINUATION_SYMBOL
and _LONGEST_MARKDOWN_SUFFIX_LEN unchanged).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@elementary/monitor/data_monitoring/alerts/integrations/integrations.py`:
- Around line 77-85: The top-level imports of SlackWebMessagingIntegration,
SlackWebhookMessagingIntegration, and TeamsWebhookMessagingIntegration cause
optional dependency ImportError; move those imports into the get_destination
function (or the branch handling each integration) so they are imported lazily
only when needed (e.g., inside the branch that returns
SlackWebMessagingIntegration, SlackWebhookMessagingIntegration, or
TeamsWebhookMessagingIntegration). Update get_destination to catch ImportError
for each local import and raise or handle a clear error message, keeping all
references to the exact class names (SlackWebMessagingIntegration,
SlackWebhookMessagingIntegration, TeamsWebhookMessagingIntegration,
get_destination) so callers still receive the correct integration when its
optional dependency is present.

---

Outside diff comments:
In `@elementary/clients/slack/slack_message_builder.py`:
- Around line 61-71: The condition in SlackMessageBuilder that checks
section_msg length uses a strict '<' which causes messages exactly equal to
_SECTION_TEXT_MAX_LENGTH to be truncated; update the check in the method
handling section_msg so it uses '<=' instead of '<' to return the original
section_msg when its length is exactly _SECTION_TEXT_MAX_LENGTH (leave the
truncation logic using _CONTINUATION_SYMBOL and _LONGEST_MARKDOWN_SUFFIX_LEN
unchanged).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 685e3cc5-c045-473c-8e48-bfb1f0fd4de2

📥 Commits

Reviewing files that changed from the base of the PR and between 870175a and cbdcace.

📒 Files selected for processing (6)
  • elementary/clients/slack/slack_message_builder.py
  • elementary/config/config.py
  • elementary/messages/formats/block_kit.py
  • elementary/monitor/data_monitoring/alerts/integrations/integrations.py
  • elementary/monitor/data_monitoring/alerts/integrations/slack/slack.py
  • elementary/monitor/data_monitoring/report/data_monitoring_report.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • elementary/config/config.py
  • elementary/monitor/data_monitoring/report/data_monitoring_report.py

Comment on lines +77 to +85
from elementary.messages.messaging_integrations.slack_web import (
SlackWebMessagingIntegration,
)
from elementary.messages.messaging_integrations.slack_webhook import (
SlackWebhookMessagingIntegration,
)
from elementary.messages.messaging_integrations.teams_webhook import (
TeamsWebhookMessagingIntegration,
)
Copy link

@coderabbitai coderabbitai bot Mar 21, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

get_destination still imports optional integrations eagerly.

At Lines 77-85, importing Slack and Teams classes up front can raise ImportError before type checks when only one extra is installed. This weakens Phase-2 optional-dependency readiness.

Suggested refactor (branch-local imports)
     def get_destination(
         integration: BaseMessagingIntegration[DestinationType, Any],
         config: Config,
         metadata: dict,
         override_config_defaults: bool = False,
     ) -> DestinationType:
-        from elementary.messages.messaging_integrations.slack_web import (
-            SlackWebMessagingIntegration,
-        )
-        from elementary.messages.messaging_integrations.slack_webhook import (
-            SlackWebhookMessagingIntegration,
-        )
-        from elementary.messages.messaging_integrations.teams_webhook import (
-            TeamsWebhookMessagingIntegration,
-        )
-
-        if (
-            isinstance(integration, TeamsWebhookMessagingIntegration)
-            and config.has_teams
-            and config.teams_webhook
-        ):
-            return cast(DestinationType, None)
-        elif isinstance(integration, SlackWebMessagingIntegration):
+        if config.has_teams and config.teams_webhook:
+            from elementary.messages.messaging_integrations.teams_webhook import (
+                TeamsWebhookMessagingIntegration,
+            )
+            if isinstance(integration, TeamsWebhookMessagingIntegration):
+                return cast(DestinationType, None)
+
+        if config.has_slack:
+            from elementary.messages.messaging_integrations.slack_web import (
+                SlackWebMessagingIntegration,
+            )
+            from elementary.messages.messaging_integrations.slack_webhook import (
+                SlackWebhookMessagingIntegration,
+            )
+        else:
+            SlackWebMessagingIntegration = tuple()  # type: ignore[assignment]
+            SlackWebhookMessagingIntegration = tuple()  # type: ignore[assignment]
+
+        if isinstance(integration, SlackWebMessagingIntegration):
             if override_config_defaults:
                 if "channel" in metadata:
                     logger.info(
                         f"ignoring channel from metadata: {metadata['channel']}"
                     )
                 return config.slack_channel_name
             return metadata.get("channel", config.slack_channel_name)
         elif isinstance(integration, SlackWebhookMessagingIntegration):
             return cast(DestinationType, None)
         raise UnsupportedAlertIntegrationError
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@elementary/monitor/data_monitoring/alerts/integrations/integrations.py`
around lines 77 - 85, The top-level imports of SlackWebMessagingIntegration,
SlackWebhookMessagingIntegration, and TeamsWebhookMessagingIntegration cause
optional dependency ImportError; move those imports into the get_destination
function (or the branch handling each integration) so they are imported lazily
only when needed (e.g., inside the branch that returns
SlackWebMessagingIntegration, SlackWebhookMessagingIntegration, or
TeamsWebhookMessagingIntegration). Update get_destination to catch ImportError
for each local import and raise or handle a clear error message, keeping all
references to the exact class names (SlackWebMessagingIntegration,
SlackWebhookMessagingIntegration, TeamsWebhookMessagingIntegration,
get_destination) so callers still receive the correct integration when its
optional dependency is present.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These imports are already lazy — they were moved from module top-level into this method body. They won't execute until get_destination is actually called, which only happens when the integration is configured.

The suggested refactoring (branch-local imports with tuple() fallback) adds complexity without a real benefit for Phase 1, since the deps are still required. We can revisit this pattern in Phase 2 when deps become truly optional.

Copy link

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

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.

1 participant