Skip to content

Conversation

@chandrasekharan-zipstack
Copy link
Contributor

@chandrasekharan-zipstack chandrasekharan-zipstack commented Oct 30, 2025

What

  • Introduce a generic, framework-agnostic plugin system for Django, Flask, and Workers
  • Add core plugin infrastructure with discovery, validation, and loading capabilities
  • Support for both singleton and non-singleton plugin patterns
  • Redis client enhancement to support REDIS_USERNAME environment variable
  • Support to add plugins to unstract-core as well
  • MINOR: Added debupgy dependency to worker to allow debugging

Why

  • UN-2930 requires a flexible plugin architecture to separate OSS and cloud-specific functionality
  • Need a standardized way to load and manage plugins across different frameworks (Django, Flask, Workers)
  • Enable dynamic feature loading without hardcoding cloud-specific logic in OSS codebase

How

Core Plugin Infrastructure:

  • unstract/core/src/unstract/core/plugins/plugin_manager.py - Generic PluginManager with:
    • Plugin discovery from directories
    • Metadata validation (version, dependencies)
    • Support for .so compiled extensions
    • Framework-specific registration callbacks

Framework-Specific Wrappers:

  • unstract/core/src/unstract/core/django/plugin.py - DjangoPluginManager wrapper

    • Singleton pattern for Django apps
    • Django AppConfig integration
    • Convenience plugin_loader() function
  • unstract/core/src/unstract/core/flask/plugin.py - FlaskPluginManager wrapper

    • Flask blueprint registration support
    • Flask app.logger integration
    • Singleton pattern for Flask apps

Supporting Changes:

  • Updated .gitignore to exclude plugin directories that will contain cloud-specific code
  • Added debugpy>=1.8.14 to workers dev dependencies for debugging support
  • Enhanced Redis client to accept REDIS_USERNAME env var (alternative to REDIS_USER)

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

No, this PR will not break any existing features.

Reasoning:

  1. Pure additive changes - Only adds new plugin infrastructure, doesn't modify existing code paths
  2. No existing code depends on this - This is net-new functionality
  3. Backward compatible - All changes are additions, no deletions or modifications to existing APIs
  4. Isolated changes - Plugin system is opt-in and doesn't affect services that don't use it
  5. Redis client change is backward compatible - Adds alternative env var, doesn't remove existing REDIS_USER support

Env Config

Optional (backward compatible):

  • REDIS_USERNAME - Alternative to REDIS_USER for Redis authentication (not required)

Related Issues or PRs

  • JIRA: UN-2930

Dependencies Versions

Added:

  • debugpy>=1.8.14 (dev dependency in workers/pyproject.toml)

No changes to runtime dependencies.

Notes on Testing

Pre-commit Validation

  • ✅ All pre-commit hooks passed
  • ✅ ruff, ruff-format, pycln, pyupgrade all passed
  • ✅ No linting or formatting issues

Screenshots

N/A - Infrastructure changes only, no UI changes

Checklist

I have read and understood the Contribution Guidelines.


🤖 Generated with Claude Code

Co-Authored-By: Claude [email protected]

Introduce a generic, framework-agnostic plugin system that supports
Django, Flask, and Workers. This foundation enables dynamic loading
of plugins with metadata validation, singleton/non-singleton patterns,
and support for compiled extensions.

Key components:
- Generic PluginManager with plugin discovery and validation
- DjangoPluginManager wrapper for Django apps
- FlaskPluginManager wrapper for Flask apps
- Redis client enhancement (REDIS_USERNAME support)
- Plugin directory exclusions in .gitignore
- Development dependency: debugpy for debugging

This is the first PR in a series for UN-2930, establishing the
infrastructure needed for subscription usage tracking and other
plugin-based features.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 30, 2025

Summary by CodeRabbit

  • New Features

    • Introduced a dynamic plugin system enabling discovery, loading, and management of extensible components.
    • Added framework-specific plugin registration support for Django and Flask applications.
  • Chores

    • Updated backend plugin directory configuration.
    • Added development debugging tools to dependencies.

Walkthrough

Adds a framework-agnostic PluginManager, Flask and Django wrappers and helpers, backend AppConfig and module accessors to load plugins at startup, plugin docs and per-directory .gitignore adjustments, REDIS_USERNAME fallback in Redis client env parsing, and debugpy added to worker dev dependencies. (50 words)

Changes

Cohort / File(s) Summary
Core plugin system
unstract/core/src/unstract/core/plugins/plugin_manager.py, unstract/core/src/unstract/core/plugins/__init__.py, unstract/core/src/unstract/core/plugins/.gitignore, unstract/core/src/unstract/core/plugins/README.md
New framework-agnostic PluginManager (discovery, import, metadata validation, optional singleton, registration callback, reload), package initializer exporting PluginManager, docs describing metadata and usage, and .gitignore to ignore enterprise plugin implementations while keeping core infra files.
Django integration
unstract/core/src/unstract/core/django/plugin.py, unstract/core/src/unstract/core/django/__init__.py, backend/plugins/__init__.py, backend/plugins/apps.py, backend/plugins/README.md, backend/backend/settings/base.py
Adds DjangoPluginManager wrapper and plugin_loader helper, re-exports in package __init__; backend module exposes get_plugin_manager and get_plugin; PluginsConfig(AppConfig) loads plugins in ready() (logs errors); backend README now references core plugin docs; settings updated to use plugins.apps.PluginsConfig.
Flask integration
unstract/core/src/unstract/core/flask/plugin.py, unstract/core/src/unstract/core/flask/__init__.py
Adds FlaskPluginManager wrapper (accepts app, wires registration callback to register blueprints on the Flask app), PluginManager alias, and plugin_loader helper; re-exports in package __init__.
Redis client
unstract/core/src/unstract/core/cache/redis_client.py
from_env now reads username from REDIS_USER with fallback to REDIS_USERNAME; docstring updated to mention both env vars.
Repository ignores
.gitignore, backend/plugins/.gitignore
Updated ignore rules: removed several previous ignore entries for backend plugin categories and added more targeted plugin .gitignore patterns to ignore implementations while allowing core infra files.
Backend plugin package
backend/plugins/__init__.py, backend/plugins/apps.py, backend/plugins/README.md
Exposes plugin manager accessor functions, adds AppConfig that triggers plugin loading on ready(), README now points to core plugin documentation.
Dev tooling
workers/pyproject.toml
Adds debugpy>=1.8.14 to development dependencies.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application (Django/Flask)
    participant Wrapper as Framework PluginManager
    participant Generic as Generic PluginManager
    participant FS as Filesystem (plugins dir)

    App->>Wrapper: instantiate (app?/plugins_dir, plugins_pkg, logger)
    Note right of Wrapper #e6f7ff: lazy init / singleton storage
    App->>Wrapper: load_plugins()
    Wrapper->>Generic: init manager (plugins_dir, plugins_pkg, logger, registration_callback)
    Generic->>FS: scan plugins directory
    FS-->>Generic: plugin entries
    loop per plugin
        Generic->>Generic: import plugin module
        Generic->>Generic: validate metadata (name, version, is_active, disable)
        alt valid & active
            Generic->>Wrapper: call registration_callback(plugin_data)
            Wrapper->>App: register artifacts (e.g., Flask blueprint)
            Generic-->>Wrapper: record plugin loaded
        else skip / invalid
            Generic-->>Wrapper: skip / log
        end
    end
    Generic-->>Wrapper: finished
    App->>Wrapper: get_plugin(name) / has_plugin(name)
    Wrapper-->>App: metadata / boolean
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing extra attention:
    • unstract/core/.../plugins/plugin_manager.py: discovery/import logic, .so handling, metadata validation, and error/logging branches.
    • Singleton and lifecycle semantics in FlaskPluginManager and DjangoPluginManager to avoid cross-context state leakage.
    • Flask blueprint registration callback: resolving blueprint objects, idempotence, and error handling during registration.
    • backend/plugins/apps.py ready(): timing of plugin loading, whether exceptions are appropriately logged vs. propagated.
    • .gitignore changes: ensure patterns do not unintentionally expose enterprise or sensitive files.

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'UN-2930 [MISC] Add plugin infrastructure to unstract-core' accurately reflects the main change in the PR, which is the introduction of a comprehensive plugin system infrastructure across multiple frameworks (Django, Flask, Workers). The title directly and clearly summarizes the primary objective and is sufficiently specific to convey what the changeset accomplishes without being overly verbose.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all key template sections: What (objectives), Why (reasoning), How (implementation details), breaking changes assessment, environment configuration, dependencies, testing notes, and proper attribution. The author has thoroughly documented the plugin infrastructure additions, framework-specific wrappers, supporting changes, and explicitly addressed backward compatibility concerns and JIRA linkage.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/UN-2930-plugin-infrastructure

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
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 (3)
workers/pyproject.toml (1)

49-50: Minor: Consider pinning upper version bound for reproducibility.

The addition of debugpy>=1.8.14 follows the existing project pattern of lower-bound-only versioning. However, for dev tools that may introduce breaking changes in future major versions, consider adding an upper bound constraint (e.g., debugpy>=1.8.14,<2.0.0) to ensure reproducible development environments and prevent unexpected breakage in CI pipelines.

This is an optional improvement and applies broadly to the dev dependencies section; the change itself is sound and appropriate.

unstract/core/src/unstract/core/flask/__init__.py (1)

3-3: Consider removing the unused noqa directive.

Modern linters understand re-export patterns, making the noqa: F401 directive unnecessary here.

Apply this diff if you'd like to clean it up:

-from .plugin import PluginManager, plugin_loader  # noqa: F401
+from .plugin import PluginManager, plugin_loader
unstract/core/src/unstract/core/django/__init__.py (1)

1-1: Consider removing the unused noqa directive.

Modern linters understand re-export patterns, making the noqa: F401 directive unnecessary here.

Apply this diff if you'd like to clean it up:

-from .plugin import DjangoPluginManager, PluginManager, plugin_loader  # noqa: F401
+from .plugin import DjangoPluginManager, PluginManager, plugin_loader
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between e8e3f64 and b385984.

⛔ Files ignored due to path filters (1)
  • workers/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • .gitignore (1 hunks)
  • unstract/core/src/unstract/core/cache/redis_client.py (2 hunks)
  • unstract/core/src/unstract/core/django/__init__.py (1 hunks)
  • unstract/core/src/unstract/core/django/plugin.py (1 hunks)
  • unstract/core/src/unstract/core/flask/__init__.py (1 hunks)
  • unstract/core/src/unstract/core/flask/plugin.py (1 hunks)
  • unstract/core/src/unstract/core/plugins/.gitignore (1 hunks)
  • unstract/core/src/unstract/core/plugins/__init__.py (1 hunks)
  • unstract/core/src/unstract/core/plugins/plugin_manager.py (1 hunks)
  • workers/pyproject.toml (1 hunks)
🧰 Additional context used
🪛 Ruff (0.14.2)
unstract/core/src/unstract/core/flask/__init__.py

3-3: Unused noqa directive (non-enabled: F401)

Remove unused noqa directive

(RUF100)

unstract/core/src/unstract/core/flask/plugin.py

87-90: Avoid specifying long messages outside the exception class

(TRY003)

unstract/core/src/unstract/core/django/plugin.py

76-79: Avoid specifying long messages outside the exception class

(TRY003)

unstract/core/src/unstract/core/plugins/plugin_manager.py

62-62: Unused static method argument: args

(ARG004)


113-113: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


113-113: Use explicit conversion flag

Replace with conversion flag

(RUF010)


156-156: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


156-156: Use explicit conversion flag

Replace with conversion flag

(RUF010)

unstract/core/src/unstract/core/django/__init__.py

1-1: Unused noqa directive (non-enabled: F401)

Remove unused noqa directive

(RUF100)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (18)
unstract/core/src/unstract/core/cache/redis_client.py (2)

255-255: LGTM! Documentation clearly explains the alternative variable.

The documentation update appropriately describes REDIS_USERNAME as an optional alternative to REDIS_USER, providing clarity for configuration flexibility.


267-267: No changes required—the implementation is correct and intentional.

The fallback logic at line 267 is documented (see the docstring at line 255) and serves as a backward-compatibility mechanism. The behavior where empty REDIS_USER falls back to REDIS_USERNAME is by design, allowing flexible configuration. Verification shows:

  • REDIS_USERNAME is actively used (e.g., platform-service) as an alternative to REDIS_USER
  • Sample configurations consistently use REDIS_USER=default (never empty), with no issues found
  • The implementation correctly handles all cases: missing variables return None, and the fallback provides configuration flexibility
.gitignore (1)

646-647: LGTM! Plugin directory exclusion is consistent.

The addition properly excludes the subscription usage plugin directory, aligning with the plugin infrastructure strategy.

unstract/core/src/unstract/core/plugins/.gitignore (1)

1-7: LGTM! Effective OSS/enterprise separation strategy.

The ignore pattern properly isolates enterprise plugins while preserving core infrastructure files.

unstract/core/src/unstract/core/plugins/plugin_manager.py (5)

1-25: LGTM! Well-documented framework-agnostic plugin manager.

The module docstring and class documentation clearly communicate the purpose and capabilities.


30-61: LGTM! Comprehensive initialization with good defaults.

The parameter documentation is thorough, and the path normalization handles both string and Path inputs appropriately.


62-72: LGTM! Singleton pattern correctly implemented.

The conditional singleton behavior based on use_singleton is properly implemented. The static analysis warning about unused args is a false positive—the parameter is necessary for the __new__ signature even though it's not directly used in this method.


74-161: LGTM! Robust plugin loading with comprehensive error handling.

The plugin loading logic handles multiple scenarios well:

  • Missing plugin directory with graceful degradation
  • Support for both directory and .so modules
  • Metadata validation with disabled plugin filtering
  • Proper error isolation per plugin

The static analysis suggestions to use logging.exception (lines 113, 156) are style preferences—using logging.error in a loop context is acceptable since you want to continue processing other plugins.


162-199: LGTM! Clean accessor API with safe defaults.

The plugin accessor methods provide a consistent interface with appropriate return values for missing plugins.

unstract/core/src/unstract/core/plugins/__init__.py (1)

1-12: LGTM! Clean package initialization with clear documentation.

The module docstring effectively explains the plugin system's purpose and framework integration options.

unstract/core/src/unstract/core/flask/plugin.py (4)

1-14: LGTM! Well-documented Flask integration.

The module docstring clearly explains the Flask-specific functionality.


25-60: LGTM! Flexible singleton pattern with parameter updates.

The __new__ implementation properly handles the singleton pattern while allowing parameter updates when plugins directory or package changes. The conditional reinitialization ensures the manager stays synchronized with configuration changes.


74-91: LGTM! Appropriate initialization and loading with error checking.

The manager initialization with Flask-specific parameters (app.logger, blueprint callback, src submodule) is correct, and the RuntimeError in load_plugins provides clear guidance when used incorrectly.


93-155: LGTM! Consistent accessor API with safe defaults.

The accessor methods and convenience function provide a clean, user-friendly API with appropriate safety checks.

unstract/core/src/unstract/core/django/plugin.py (4)

1-14: LGTM! Well-documented Django integration.

The module docstring clearly explains Django-specific functionality and differences from the Flask wrapper.


24-71: LGTM! Clean Django-specific implementation.

The singleton pattern and manager initialization are simpler than the Flask version, appropriately reflecting Django's different lifecycle. The comment on Line 69 helpfully explains why no registration callback is needed.


73-122: LGTM! Consistent accessor API with safe defaults.

The methods mirror the Flask wrapper's interface, providing consistency across framework integrations.


128-163: LGTM! Excellent documentation with practical example.

The docstring example demonstrates the complete usage pattern clearly, making it easy for developers to integrate the plugin system into Django apps.

@chandrasekharan-zipstack chandrasekharan-zipstack changed the title UN-2930 [FEAT] Add core plugin infrastructure (PR #1 of 6) UN-2930 [MISC] Add plugin infrastructure to unstract-core Oct 30, 2025
Copy link
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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between b385984 and aa77782.

📒 Files selected for processing (2)
  • .gitignore (0 hunks)
  • backend/plugins/.gitignore (1 hunks)
💤 Files with no reviewable changes (1)
  • .gitignore
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

NOTE: Existing mechanism is left untouched to avoid breaking anything
Copy link
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: 0

🧹 Nitpick comments (2)
backend/plugins/__init__.py (1)

37-60: Convenience function simplifies plugin access.

The get_plugin() wrapper provides a clean single-line interface for retrieving plugin metadata, which is well-documented with clear examples.

Optionally, you can sort __all__ alphabetically to address the static analysis hint (RUF022), though this is a minor formatting preference.

Apply this diff if you want to address the linting hint:

-__all__ = ["get_plugin_manager", "get_plugin"]
+__all__ = ["get_plugin", "get_plugin_manager"]
backend/plugins/apps.py (1)

20-34: Plugin loading implementation is appropriate for opt-in plugin system.

The ready() method correctly:

  • Uses local import to avoid AppRegistryNotReady errors
  • Loads plugins after Django initialization
  • Implements graceful error handling that allows Django to continue even if plugins fail

This is the right approach for an additive, backward-compatible plugin system.

Optionally, consider using logger.debug() instead of logger.info() on line 31 to reduce log noise in production, since successful plugin loading is expected behavior rather than noteworthy information.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between aa77782 and fce7a0b.

📒 Files selected for processing (3)
  • backend/backend/settings/base.py (1 hunks)
  • backend/plugins/__init__.py (1 hunks)
  • backend/plugins/apps.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.14.2)
backend/plugins/__init__.py

60-60: __all__ is not sorted

Apply an isort-style sorting to __all__

(RUF022)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (4)
backend/plugins/__init__.py (2)

1-11: LGTM!

The module initialization is clean and follows Python best practices with proper docstring, imports, and logger configuration.


17-34: Lazy initialization pattern is appropriate for Django AppConfig integration.

The singleton pattern correctly defers plugin loading until requested, and the docstring clearly documents that loading happens separately in PluginsConfig.ready(). The implementation is clean and follows the module-level singleton pattern.

Note: The pattern is not thread-safe (race condition possible if multiple threads call this concurrently), but since Django's ready() hook is called once during startup and the PR describes this as opt-in, this should be acceptable for the current use case.

backend/backend/settings/base.py (1)

289-289: LGTM!

Explicitly referencing the AppConfig class (plugins.apps.PluginsConfig) follows Django best practices and ensures the PluginsConfig.ready() hook is properly invoked during Django startup to load plugins.

backend/plugins/apps.py (1)

1-18: LGTM!

The AppConfig class is properly configured with appropriate name and verbose_name attributes. The docstring clearly explains the purpose of deferred loading to avoid Django initialization issues.

Copy link
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: 0

🧹 Nitpick comments (1)
backend/plugins/.gitignore (1)

1-15: Good—critical fix has been properly applied.

The past review flagged a critical issue where the broad */ pattern (line 3) would prevent negations on child paths because git doesn't descend into ignored parent directories. The current code correctly addresses this by un-ignoring parent directories (!authentication/, !api/) before their child paths, allowing git to descend and apply the negation rules.

The structure is now semantically correct: each child path negation is now reachable because its parent has been un-ignored first.

Minor note: Line 15 uses !workflow_manager while other directories use the trailing slash form (!authentication/, !api/, !api/dto/). For consistency and explicitness, consider changing it to !workflow_manager/ if it's a directory.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between fce7a0b and 77cbba1.

📒 Files selected for processing (1)
  • backend/plugins/.gitignore (1 hunks)

Copy link
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: 0

🧹 Nitpick comments (3)
unstract/core/src/unstract/core/plugins/README.md (1)

101-106: Add language specifier to fenced code block.

The directory structure example should specify a language identifier for the code fence (e.g., text or plaintext) to improve rendering and satisfy markdown linting rules.

Apply this diff:

-```
+```text
 unstract/core/src/unstract/core/plugins/
 ├── README.md                    # This file
 ├── __init__.py                  # Package initialization
 ├── plugin_manager.py            # Core plugin discovery and loading

</blockquote></details>
<details>
<summary>unstract/core/src/unstract/core/flask/plugin.py (2)</summary><blockquote>

`25-60`: **Consider documenting the three-parameter initialization requirement.**

The singleton initialization logic requires all three parameters (`app`, `plugins_dir`, `plugins_pkg`) to be present before initializing the manager (line 50). This requirement isn't immediately obvious from the method signature, which marks all parameters as optional. Consider either:
1. Adding a note in the docstring explaining that initialization only occurs when all three parameters are provided, or
2. Validating at instantiation time and providing a clear error if only some parameters are provided

This would prevent confusing usage patterns where a user provides only some parameters and expects initialization to occur.

---

`84-91`: **Error message could be extracted to a constant.**

Ruff suggests extracting long error messages to a constant or defining them in a custom exception class. This is a minor style improvement that can enhance maintainability.



If desired, apply this refactor:

```diff
+_NOT_INITIALIZED_ERROR = (
+    "FlaskPluginManager not initialized. "
+    "Call with app, plugins_dir, and plugins_pkg first."
+)
+
 def load_plugins(self) -> None:
     """Load plugins using the generic manager."""
     if not self._initialized:
-        raise RuntimeError(
-            "FlaskPluginManager not initialized. "
-            "Call with app, plugins_dir, and plugins_pkg first."
-        )
+        raise RuntimeError(_NOT_INITIALIZED_ERROR)
     self._manager.load_plugins()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 77cbba1 and 61089e2.

📒 Files selected for processing (5)
  • backend/plugins/.gitignore (1 hunks)
  • backend/plugins/README.md (1 hunks)
  • unstract/core/src/unstract/core/flask/plugin.py (1 hunks)
  • unstract/core/src/unstract/core/plugins/.gitignore (1 hunks)
  • unstract/core/src/unstract/core/plugins/README.md (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • backend/plugins/README.md
  • backend/plugins/.gitignore
🚧 Files skipped from review as they are similar to previous changes (1)
  • unstract/core/src/unstract/core/plugins/.gitignore
🧰 Additional context used
🪛 LanguageTool
unstract/core/src/unstract/core/plugins/README.md

[grammar] ~1-~1: Ensure spelling is correct
Context: # Unstract Plugin System ## Plugin Metadata Forma...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
unstract/core/src/unstract/core/plugins/README.md

101-101: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 Ruff (0.14.2)
unstract/core/src/unstract/core/flask/plugin.py

87-90: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (3)
unstract/core/src/unstract/core/flask/plugin.py (3)

65-72: LGTM! Blueprint registration correctly uses metadata.

The blueprint registration callback correctly retrieves the plugin name from metadata.get('name', 'unknown'), addressing the previous review comment. The implementation properly registers Flask blueprints and logs the activity.


93-124: LGTM! Safe default pattern for uninitialized access.

The implementation correctly returns safe defaults (empty dict or False) when the manager is not yet initialized, preventing errors while maintaining a clean API. This is good defensive programming.


131-155: LGTM! Clean convenience function with helpful documentation.

The plugin_loader convenience function provides a clean, straightforward API for the common use case of creating a plugin manager and loading plugins in one call. The docstring example is clear and helpful.

host=os.getenv("REDIS_HOST", "localhost"),
port=int(os.getenv("REDIS_PORT", "6379")),
username=os.getenv("REDIS_USER"),
username=os.getenv("REDIS_USER") or os.getenv("REDIS_USERNAME"),
Copy link
Contributor

Choose a reason for hiding this comment

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

@chandrasekharan-zipstack why we need to introduce a new variable instead of using existing for redis user?.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jaseemjaskp platform-service alone differs by checking for this REDIS_USERNAME env. I figured that changing it might mean updating the secrets and to ensure everything is backward compatible I did this. Later when we choose to standardize some of the envs across services, we could do away with this line

Copy link
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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 61089e2 and 6783d1f.

📒 Files selected for processing (2)
  • unstract/core/src/unstract/core/flask/plugin.py (1 hunks)
  • unstract/core/src/unstract/core/plugins/plugin_manager.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.14.2)
unstract/core/src/unstract/core/flask/plugin.py

86-89: Avoid specifying long messages outside the exception class

(TRY003)

unstract/core/src/unstract/core/plugins/plugin_manager.py

61-61: Unused static method argument: args

(ARG004)


133-133: Consider (*rel_parts, pkg_name) instead of concatenation

Replace with (*rel_parts, pkg_name)

(RUF005)


142-142: Consider (*rel_parts, item.name) instead of concatenation

Replace with (*rel_parts, item.name)

(RUF005)


179-179: Loop control variable plugin_path not used within loop body

Rename unused plugin_path to _plugin_path

(B007)


191-191: Use explicit conversion flag

Replace with conversion flag

(RUF010)


237-239: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


238-238: Use explicit conversion flag

Replace with conversion flag

(RUF010)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

- Update plugin_loader docstring to clarify plugins_dir and plugins_pkg are required
- Add parameter validation with helpful ValueError message
- Include practical example showing correct usage pattern
- Improves developer experience with clear setup requirements

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Copy link
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 (1)
unstract/core/src/unstract/core/flask/plugin.py (1)

83-123: Consider consistent error handling across methods.

There's an inconsistency in how methods handle the uninitialized state:

  • load_plugins() raises RuntimeError with a helpful message
  • get_plugin(), has_plugin(), and plugins return safe defaults ({} or False)

While this may be intentional (mutations vs. queries), it could surprise callers who expect consistent behavior. Consider either:

  1. Having all methods raise when uninitialized (fail-fast approach), or
  2. Having all methods return safe defaults (defensive approach)

The current approach is acceptable if documented, but consistency improves predictability.


Note: Static analysis (Ruff TRY003) suggests extracting the long error messages on lines 86-89 into custom exception classes, though this is a style preference.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 952e30a and 5bfaa3d.

📒 Files selected for processing (1)
  • unstract/core/src/unstract/core/flask/plugin.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.14.2)
unstract/core/src/unstract/core/flask/plugin.py

86-89: Avoid specifying long messages outside the exception class

(TRY003)


158-164: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (4)
unstract/core/src/unstract/core/flask/plugin.py (4)

1-14: LGTM! Clear module documentation and appropriate imports.

The module docstring clearly describes the Flask-specific wrapper purpose, and all imports are necessary for the implementation.


62-81: LGTM! Blueprint registration correctly implemented.

The Flask registration callback properly retrieves the plugin name from metadata (line 71), and the generic manager is correctly initialized with Flask-specific parameters. The past review issue regarding incorrect name source has been properly addressed.


126-127: LGTM! Backward compatibility maintained.

The alias ensures existing code using PluginManager continues to work.


130-167: LGTM! Parameter validation addresses previous concerns.

The function now properly validates that both plugins_dir and plugins_pkg are provided before creating the manager (lines 157-164), which addresses the previous review comment about failing with default usage. The clear error message with example usage helps developers understand the requirements.


Note: Static analysis (Ruff TRY003) suggests extracting the long error message on lines 158-164 into a custom exception class, though this is a style preference and the current inline message with example is quite helpful.

- Add threading.Lock at class level to protect singleton instance
- Guard instance creation and state mutations with lock
- Prevent race conditions in multi-threaded Flask environments
- Ensure thread-safe initialization of app, plugins_dir, and plugins_pkg

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Copy link
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: 0

🧹 Nitpick comments (1)
unstract/core/src/unstract/core/flask/plugin.py (1)

87-94: Consider extracting exception messages to exception classes.

The error message is clear and helpful. However, static analysis flags TRY003 for long messages defined inline. This is a minor style consideration—the current implementation is functional.

If you prefer to align with Ruff's recommendation, you could define custom exception classes:

class PluginManagerNotInitializedError(RuntimeError):
    """Raised when FlaskPluginManager is used before initialization."""
    def __init__(self):
        super().__init__(
            "FlaskPluginManager not initialized. "
            "Call with app, plugins_dir, and plugins_pkg first."
        )

Then raise with raise PluginManagerNotInitializedError(). The same pattern applies to line 162-168. However, this adds complexity for minimal benefit.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 5bfaa3d and c5e98b7.

📒 Files selected for processing (1)
  • unstract/core/src/unstract/core/flask/plugin.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.14.2)
unstract/core/src/unstract/core/flask/plugin.py

90-93: Avoid specifying long messages outside the exception class

(TRY003)


162-168: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (6)
unstract/core/src/unstract/core/flask/plugin.py (6)

1-15: LGTM: Clean imports and clear documentation.

The module docstring effectively describes the Flask integration purpose, and all imports are necessary and properly organized.


24-64: LGTM: Thread-safe singleton pattern correctly implemented.

The previous thread-safety concern has been properly addressed. The lock guards the entire instance creation and initialization logic, preventing race conditions in multi-threaded Flask environments. The short-circuit evaluation on line 56 ensures _plugins_dir and _plugins_pkg are only checked after they've been set.


66-85: LGTM: Flask integration callback correctly implemented.

The previous issue with blueprint registration logging has been fixed—line 75 now correctly retrieves the plugin name from metadata rather than plugin_data. The registration callback properly integrates Flask blueprints and logging.


96-127: LGTM: Consistent defensive query API.

The query methods (get_plugin, has_plugin, and plugins property) all follow a consistent pattern of returning safe defaults when the manager is not initialized, preventing crashes while signaling the uninitialized state. This is a good defensive design that maintains usability.


130-131: LGTM: Clean backward compatibility alias.

The alias maintains compatibility with existing code while the new FlaskPluginManager name better reflects the class's purpose.


134-171: LGTM: Well-designed convenience function with proper validation.

The previous issue where plugin_loader could fail with default parameters has been properly fixed. The validation on lines 161-168 now ensures both required parameters are provided before initialization, and the error message includes helpful usage examples. The function signature and documentation clearly indicate the requirements.

…dling

- Merge nested if statements in FlaskPluginManager.getInstance() into single compound condition
- Remove redundant PermissionError from plugin_manager (OSError parent class covers it)

🤖 Generated with Claude Code

Co-Authored-By: Claude <[email protected]>
@sonarqubecloud
Copy link

sonarqubecloud bot commented Nov 3, 2025

@github-actions
Copy link
Contributor

github-actions bot commented Nov 3, 2025

Test Results

Summary
  • Runner Tests: 11 passed, 0 failed (11 total)
  • SDK1 Tests: 66 passed, 0 failed (66 total)

Runner Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_logs}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup\_skip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_client\_init}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config\_without\_mount}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_run\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_for\_sidecar}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_sidecar\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{11}}$$ $$\textcolor{#23d18b}{\tt{11}}$$
SDK1 Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_non\_retryable\_http\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retryable\_http\_errors}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_post\_method\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_logging}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_retry\_on\_errors}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_wrapper\_methods\_retry}}$$ $$\textcolor{#23d18b}{\tt{4}}$$ $$\textcolor{#23d18b}{\tt{4}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_connection\_error\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_timeout\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_non\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_without\_response}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_non\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_other\_exception\_not\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_without\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_successful\_call\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_after\_transient\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_max\_retries\_exceeded}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_max\_time\_exceeded}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_with\_custom\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_no\_retry\_with\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_exception\_not\_in\_tuple\_not\_retried}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_delay\_would\_exceed\_max\_time}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_default\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_environment\_variable\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_max\_retries}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_max\_time}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_base\_delay}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_multiplier}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_jitter\_values}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_exceptions\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_predicate\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_both\_exceptions\_and\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_exceptions\_match\_but\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_platform\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_prompt\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_platform\_service\_decorator\_retries\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_prompt\_service\_decorator\_retries\_on\_timeout}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_warning\_logged\_on\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_info\_logged\_on\_success\_after\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_exception\_logged\_on\_giving\_up}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{66}}$$ $$\textcolor{#23d18b}{\tt{66}}$$

Copy link
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: 0

🧹 Nitpick comments (3)
unstract/core/src/unstract/core/plugins/plugin_manager.py (3)

133-133: Consider using tuple unpacking for concatenation.

The tuple concatenations can be simplified using unpacking syntax for slightly better readability and performance.

Apply this diff:

-                    module_path = ".".join(rel_parts + (pkg_name,))
+                    module_path = ".".join((*rel_parts, pkg_name))
-                new_rel_parts = rel_parts + (item.name,)
+                new_rel_parts = (*rel_parts, item.name)

Also applies to: 142-142


179-179: Rename unused loop variable to indicate it's intentionally unused.

The plugin_path variable is not used within the loop body. Following Python conventions, prefix it with an underscore to indicate it's intentionally unused.

Apply this diff:

-        for plugin_path, module_name in plugin_candidates:
+        for _plugin_path, module_name in plugin_candidates:

236-239: Use logging.exception for better error diagnostics.

When logging errors in an exception handler, use logging.exception() instead of logging.error() to automatically include the stack trace, which aids debugging.

Apply this diff:

             except KeyError as e:
-                self.logger.error(
+                self.logger.exception(
                     f"Invalid metadata for plugin '{module_name}': {str(e)}"
                 )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between c5e98b7 and 41300b4.

📒 Files selected for processing (2)
  • unstract/core/src/unstract/core/flask/plugin.py (1 hunks)
  • unstract/core/src/unstract/core/plugins/plugin_manager.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.14.2)
unstract/core/src/unstract/core/flask/plugin.py

94-97: Avoid specifying long messages outside the exception class

(TRY003)


166-172: Avoid specifying long messages outside the exception class

(TRY003)

unstract/core/src/unstract/core/plugins/plugin_manager.py

61-61: Unused static method argument: args

(ARG004)


133-133: Consider (*rel_parts, pkg_name) instead of concatenation

Replace with (*rel_parts, pkg_name)

(RUF005)


142-142: Consider (*rel_parts, item.name) instead of concatenation

Replace with (*rel_parts, item.name)

(RUF005)


179-179: Loop control variable plugin_path not used within loop body

Rename unused plugin_path to _plugin_path

(B007)


191-191: Use explicit conversion flag

Replace with conversion flag

(RUF010)


237-239: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


238-238: Use explicit conversion flag

Replace with conversion flag

(RUF010)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (2)
unstract/core/src/unstract/core/flask/plugin.py (2)

24-68: Thread-safe singleton implementation looks good!

The double-checked locking pattern with threading.Lock correctly handles concurrent access in multi-threaded Flask applications. The lock guards both instance creation and state updates, preventing race conditions.


165-175: Parameter validation prevents initialization errors effectively.

The validation requiring both plugins_dir and plugins_pkg correctly prevents the RuntimeError that would occur if the manager were created without proper initialization parameters. The helpful error message with usage example improves developer experience.

Copy link
Contributor

@muhammad-ali-e muhammad-ali-e left a comment

Choose a reason for hiding this comment

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

Added some minor comments .. please address those

from unstract.core.plugins import PluginManager as GenericPluginManager


class FlaskPluginManager:
Copy link
Contributor

Choose a reason for hiding this comment

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

looks like same as the django.plugin .. can we have a Base class for this Plugin manager to accommodate duplicate methods and base methods...?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will see if there is an easy refactor for this, however I tried this since Flask based ones accept a blueprint in case they define URLs (similar to our pluggable_apps). Let me revisit the code duplication here and get back to you

return super().__new__(cls)

def _find_plugin_modules(
self, base_path: Path, max_depth: int = 2
Copy link
Contributor

Choose a reason for hiding this comment

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

why this is 2? is it meant to the main plugin directory?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah @muhammad-ali-e. Kept it 2 to cover all our existing scenarios of how we manage plugins across backend and prompt-service. We sometimes group plugins and nest it under a folder

if item.name.endswith(".so"):
pkg_name = item.name.split(".")[0]
module_path = ".".join(rel_parts + (pkg_name,))
plugins.append((item, module_path))
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it like this? Whe do we remove .so?

Copy link
Contributor Author

@chandrasekharan-zipstack chandrasekharan-zipstack Nov 4, 2025

Choose a reason for hiding this comment

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

This is from our existing plugin loading mechanisms of including a shared library. Left this logic as is to not cause any regression. We could revisit its necessity later maybe
image

"flake8>=6.0.0",
"mypy>=1.5.0"
"mypy>=1.5.0",
"debugpy>=1.8.14",
Copy link
Contributor

Choose a reason for hiding this comment

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

What is it for?

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 for attaching a debugger to a worker process. This is present for the other services as a dev dependency

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.

4 participants