Skip to content

Feat/improve proxy handle#21

Merged
touale merged 5 commits intomasterfrom
feat/improve-proxy-handle
Nov 26, 2025
Merged

Feat/improve proxy handle#21
touale merged 5 commits intomasterfrom
feat/improve-proxy-handle

Conversation

@touale
Copy link
Copy Markdown
Owner

@touale touale commented Nov 26, 2025

Summary by CodeRabbit

  • Chores

    • Split release pipeline into separate Test PyPI and PyPI deployment stages.
    • Reverted test runner proxy settings to defaults.
  • Bug Fixes

    • Stricter validation and error reporting for proxy API responses; safer data extraction.
  • Refactor

    • Simplified plugin API call behavior and result processing (public call signature updated).
  • New Features

    • Server configuration now accepts customizable legal proxy status codes.
  • Tests

    • Expanded coverage for plugin API, proxy scenarios, and data conversion.

✏️ Tip: You can customize this high-level summary in your review settings.

@codecov
Copy link
Copy Markdown

codecov bot commented Nov 26, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Nov 26, 2025

Walkthrough

Refactors release workflow into separate Test/Prod PyPI jobs, comments out proxy lines in pytest.ini, removes return_model_dump from call_plugin_api and adds explicit proxy-result validation/extraction with always-dumped BaseModel results, adds legal_proxy_code config, and expands tests for proxy and API behaviors.

Changes

Cohort / File(s) Summary
CI/CD Deployment Workflow
.github/workflows/release.yml
Removed the single deploy job and added two jobs: public-testpypi (depends on release, publishes to Test PyPI) and public-pypi (depends on public-testpypi and release, publishes to PyPI with production env, added permissions, and artifact download).
Test Configuration
pytest.ini
Two proxy-related configuration lines were commented out (disabled), reverting those options to defaults.
Plugin API Refactor
src/framex/plugin/__init__.py
Removed return_model_dump parameter from call_plugin_api; added local use_proxy tracking and explicit handling of proxy-wrapped dict results (validate dict, require status, check against ServerConfig.legal_proxy_code, raise on invalid status, warn on empty data, extract/return data); always call model_dump(by_alias=True) for BaseModel returns.
Server Config
src/framex/config.py
Added public legal_proxy_code: list[int] field to ServerConfig with default [200].
Config Test Update
tests/test_config.py
Updated assertions to expect a ProxyPluginConfig instance and non-None proxy_urls instead of asserting an empty list.
Plugin API Tests
tests/test_plugin.py
Added extensive tests covering: API lookup, BaseModel→dict conversion, proxy success/empty/error flows and logging, not-found scenarios, dict→BaseModel param conversion, interval_apis override, proxy PluginApi creation, raw-dict returns, and multi-arg handling; uses mocks/patches and a SampleModel.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Caller
    participant call_plugin_api
    participant Manager
    participant Adapter
    participant RemoteAPI

    Caller->>call_plugin_api: call_plugin_api(api_name, **kwargs)
    call_plugin_api->>Manager: lookup API (may be proxy)
    alt PluginApi.proxy == true
        call_plugin_api->>Adapter: invoke proxy PluginApi
        Adapter->>RemoteAPI: forward request
        RemoteAPI-->>Adapter: proxy_response (expected dict)
        Adapter-->>call_plugin_api: proxy_response
        alt proxy_response is not dict
            note right of call_plugin_api `#f9f9c5`: invalid proxy format
            call_plugin_api-->>Caller: raise error
        else proxy_response.status not in legal_proxy_code
            note right of call_plugin_api `#ffd6d6`: error status
            call_plugin_api->>call_plugin_api: log error
            call_plugin_api-->>Caller: raise exception
        else
            call_plugin_api->>call_plugin_api: extract proxy_response["data"]
            call_plugin_api-->>Caller: return extracted data
        end
    else
        call_plugin_api->>Adapter: invoke direct PluginApi
        Adapter->>RemoteAPI: request
        RemoteAPI-->>Adapter: result (BaseModel or dict)
        Adapter-->>call_plugin_api: result
        alt result is BaseModel
            call_plugin_api->>call_plugin_api: result.model_dump(by_alias=True)
            call_plugin_api-->>Caller: return dict
        else
            call_plugin_api-->>Caller: return result as-is
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing extra attention:
    • src/framex/plugin/init.py — proxy vs non-proxy branches, validation, logging/exception semantics, and the public signature change.
    • tests/test_plugin.py — ensure mocks/patches align with new proxy semantics and cover edge cases.
    • .github/workflows/release.yml — verify job dependencies, permissions, and artifact handling.
    • src/framex/config.py — confirm default legal_proxy_code and any downstream uses.

Poem

🐇 I nibbled at CI, split deploys with delight,
I fetched proxy packets, checked status by sight,
If data was empty I sniffed and I warned,
If codes were not legal I patted the logs scorned,
Hooray — tests hop on, and everything's bright.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Feat/improve proxy handle' is vague and uses non-descriptive terminology like 'improve' and 'handle' without specifying what proxy functionality is being improved or how it's being handled. Revise the title to be more specific about the proxy improvement, such as 'Add legal_proxy_code configuration and refactor proxy handling' or 'Improve proxy error handling with configurable status codes'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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/improve-proxy-handle

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

@touale
Copy link
Copy Markdown
Owner Author

touale commented Nov 26, 2025

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Nov 26, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Nov 26, 2025

Note

Unit test generation is an Early Access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

Copy link
Copy Markdown

@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)
tests/test_config.py (1)

1-6: Strengthen the test assertion.

The test now only verifies that cfg is truthy, which is a very weak validation. Consider asserting specific properties of the configuration to ensure it's properly constructed.

For example:

 def test_config():
     from framex.plugin import get_plugin_config
     from framex.plugins.proxy.config import ProxyPluginConfig
 
     cfg = get_plugin_config("proxy", ProxyPluginConfig)
-    assert cfg
+    assert isinstance(cfg, ProxyPluginConfig)
+    # Verify the config has expected attributes
+    assert hasattr(cfg, 'proxy_urls')
+    assert hasattr(cfg, 'force_stream_apis')
src/framex/plugin/__init__.py (2)

84-95: Consider extracting proxy API creation logic.

The logic for creating proxy APIs when an API is not found (lines 86-95) is similar to the logic in init_all_deployments (lines 52-62). Both check for "/" prefix and enable_proxy, then create a PluginApi with call_type=ApiType.PROXY.

Consider extracting this into a helper function:

def _create_proxy_api(api_name: str) -> PluginApi:
    return PluginApi(
        api=api_name,
        deployment_name=PROXY_PLUGIN_NAME,
        call_type=ApiType.PROXY,
    )

Then use it in both locations to reduce duplication.


113-122: Consider reordering conditions for clarity.

The current order checks isinstance(result, BaseModel) before use_proxy. While this likely works correctly (proxy responses are presumably dicts, not BaseModels), the logic would be clearer if proxy handling came first since it's a special case.

Consider reordering:

     result = await get_adapter().call_func(api, **kwargs)
+    if use_proxy:
+        # Handle proxy response format
+        res = result.get("data")
+        if res is None:
+            logger.opt(colors=True).warning(f"API {api_name} return empty data")
+        if result.get("status") != 200:
+            logger.opt(colors=True).error(f"Proxy API {api_name} call unnormal: <r>{result}</r>")
+        return res
     if isinstance(result, BaseModel):
         return result.model_dump(by_alias=True)
-    if use_proxy:
-        res = result.get("data")
-        if res is None:
-            logger.opt(colors=True).warning(f"API {api_name} return empty data")
-        if result.get("status") != 200:
-            logger.opt(colors=True).error(f"Proxy API {api_name} call unnormal: <r>{result}</r>")
-        return res
     return result

This makes it explicit that proxy responses are handled specially before other return type processing.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81a2a81 and 5356ed8.

📒 Files selected for processing (4)
  • .github/workflows/release.yml (2 hunks)
  • pytest.ini (1 hunks)
  • src/framex/plugin/__init__.py (3 hunks)
  • tests/test_config.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/framex/plugin/__init__.py (2)
src/framex/adapter/__init__.py (1)
  • get_adapter (9-13)
src/framex/adapter/base.py (1)
  • call_func (29-39)
🪛 actionlint (1.7.8)
.github/workflows/release.yml

88-88: property "release" is not defined in object type {public-testpypi: {outputs: {}; result: string}}

(expression)

🔇 Additional comments (2)
.github/workflows/release.yml (1)

59-84: LGTM: Test PyPI publishing job is well-structured.

The public-testpypi job correctly:

  • Depends on the release job
  • Checks the release output before running
  • Publishes to Test PyPI with appropriate permissions
  • Uses trusted publishing with OIDC
pytest.ini (1)

25-26: I'll help you verify the default proxy plugin configuration behavior. Let me search for the ProxyPluginConfig class definition and understand the defaults.
<function_calls>


#!/bin/bash

Find ProxyPluginConfig class definition

rg -nP --type=py 'class ProxyPluginConfig' -A 20


</function_calls>

Let me also search for where this configuration is used:
<function_calls>


#!/bin/bash

Search for proxy plugin files

fd -name 'proxy' --type f


</function_calls>

Now let me check the proxy plugin init.py file mentioned in the review:
<function_calls>


#!/bin/bash

Find and examine proxy plugin init.py

rg -nP --type=py 'proxy' src/framex/plugin/ -l


</function_calls>

Comment thread .github/workflows/release.yml
Comment thread src/framex/plugin/__init__.py
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Nov 26, 2025

Here are the copyable unit test edits:

Copyable Edits

ruff.toml

@@ -1,93 +1,30 @@
-target-version = "py311"
-line-length = 119
-respect-gitignore = true
-exclude = [
-  ".bzr",
-  ".direnv",
-  ".eggs",
-  ".git",
-  ".git-rewrite",
-  ".hg",
-  ".ipynb_checkpoints",
-  ".mypy_cache",
-  ".nox",
-  ".pants.d",
-  ".pyenv",
-  ".pytest_cache",
-  ".pytype",
-  ".ruff_cache",
-  ".svn",
-  ".tox",
-  ".venv",
-  ".vscode",
-  "__pypackages__",
-  "_build",
-  "buck-out",
-  "build",
-  "dist",
-  "node_modules",
-  "site-packages",
-  ".venv",
-  ".history",
-]
-
 [lint]
-extend-select = [
-  "A",     # bad name
-  "ANN",   # flake8-annotations
-  "ARG",   # bad arguments
-  "ASYNC", # async
-  "B",     # flake8-bugbear
-  "C",     # Complexity
-  "C4",    #flake8-comprehensions
-  "E",     # PEP8 errors
-  "ERA",   # commented-out-code
-  "F",     # PEP8 formatting
-  "FLY",   # static-join-to-f-string
-  "FAST",  # FastAPI (FAST)
-  "FLY",   # static-join-to-f-string
-  "FURB",  # refurb
-  "I",     # Import sorting
-  "INP",   # flake8-no-pep420
-  "N",     # name-conventions
-  "PERF",  # Perflint
-  "PIE",   # Improper comments
-  "PTH",   # use-pathlib
-  "PT",    # pytest
-  "RET",   # unnecessary-return
-  "RUF",   # specific rules
-  "S",     # flake8-bandit
-  "SIM",   # simplify
-  "T10",   # debug found
-  "T201",  # print found
-  "TID",   # tidy-imports
-  "UP",    # Pyupgrade upgrades
-  "W",     # PEP8 warnings
-]
+select = [
+		# Pyflakes - catches logical errors and unused imports
+		"F",
+		# flake8-bugbear - catches common bugs and design issues
+		"B",
+		# flake8-bandit - security vulnerabilities
+		"S",
+		# flake8-blind-except - catches bare except clauses
+		"BLE",
+		# flake8-debugger - catches debugger statements
+		"T10",
+		# flake8-executable - catches executable files
+		"EXE",
+		# flake8-unused-arguments - catches unused function arguments
+		"ARG",
+		# tryceratops - catches try/except anti-patterns
+		"TRY",
+		# Ruff-specific rules for bugs and errors
+		"RUF",
+		# pycodestyle subset rules - only error-level issues
+		"E7",  # Syntax errors
+		"E9",  # Runtime errors
 
-ignore = [
-  "B008",   # Do not perform function calls in argument defaults
-  "B009",   # get-attr-with-constant
-  "B010",   # set-attr-with-constant
-  "ERA001",
-  "C901",
-  "E402",   # Module level import not at top of file
-  "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in `**kwargs`
-  "ANN204", # ANN204 Missing return type annotation for special method
-  # "ANN201", # Missing return type annotation for public function
-  "E501", # Line too long
 ]
-
+# suppress common non critical issues
+ignore = ["I001", "F401", "W291", "W293", "Q000", "ANN001", "ANN201", "UP006", "UP045", "E501", "S101"]
 [lint.per-file-ignores]
-"tests/**/*.py" = ["S101", "ANN201", "ANN001", "ANN002"]
-
-[lint.flake8-builtins]
-builtins-ignorelist = ["input", "id", "bytes", "type"]
-
-[lint.isort]
-force-wrap-aliases = true
-combine-as-imports = true
-split-on-trailing-comma = true
-
-[lint.pydocstyle]
-convention = "numpy"
+"**/__init__.py" = ["E402"]
+"**/conftest.py" = ["E402"]

tests/test_config.py

@@ -1,6 +1,100 @@
-def test_config():
-    from framex.plugin import get_plugin_config
-    from framex.plugins.proxy.config import ProxyPluginConfig
+import pytest
+from pydantic import BaseModel, Field
 
-    cfg = get_plugin_config("proxy", ProxyPluginConfig)
-    assert cfg
+from framex.plugin import get_plugin_config, check_plugin_config_exists
+from framex.plugins.proxy.config import ProxyPluginConfig
+
+
+class TestPluginConfig:
+    """Comprehensive tests for plugin configuration."""
+
+    def test_config_exists(self):
+        """Test that proxy config can be loaded."""
+        cfg = get_plugin_config("proxy", ProxyPluginConfig)
+        assert cfg is not None
+        assert isinstance(cfg, ProxyPluginConfig)
+
+    def test_config_default_values(self):
+        """Test that config has expected default values."""
+        cfg = get_plugin_config("proxy", ProxyPluginConfig)
+        # Based on pytest.ini, these should be commented out/empty
+        assert isinstance(cfg.proxy_urls, list)
+        assert isinstance(cfg.force_stream_apis, list)
+        assert isinstance(cfg.black_list, list)
+        assert isinstance(cfg.white_list, list)
+
+    def test_config_attributes_accessible(self):
+        """Test all config attributes are accessible."""
+        cfg = get_plugin_config("proxy", ProxyPluginConfig)
+        # Should not raise AttributeError
+        _ = cfg.proxy_urls
+        _ = cfg.force_stream_apis
+        _ = cfg.black_list
+        _ = cfg.white_list
+
+    def test_check_plugin_config_exists_for_proxy(self):
+        """Test checking if proxy plugin config exists."""
+        # Note: This depends on environment/config files
+        exists = check_plugin_config_exists("proxy")
+        assert isinstance(exists, bool)
+
+    def test_check_plugin_config_exists_for_nonexistent(self):
+        """Test checking non-existent plugin config returns False."""
+        exists = check_plugin_config_exists("nonexistent_plugin_xyz")
+        assert exists is False
+
+    def test_get_plugin_config_with_default(self):
+        """Test getting config for non-existent plugin returns defaults."""
+        class CustomConfig(BaseModel):
+            value: int = Field(default=42)
+            name: str = Field(default="default")
+        
+        cfg = get_plugin_config("nonexistent_custom_plugin", CustomConfig)
+        assert cfg.value == 42
+        assert cfg.name == "default"
+
+    def test_config_is_pydantic_model(self):
+        """Test that config is a valid Pydantic model."""
+        cfg = get_plugin_config("proxy", ProxyPluginConfig)
+        assert hasattr(cfg, "model_dump")
+        assert hasattr(cfg, "model_validate")
+        
+        # Should be able to dump to dict
+        config_dict = cfg.model_dump()
+        assert isinstance(config_dict, dict)
+        assert "proxy_urls" in config_dict
+
+    def test_config_caching(self):
+        """Test that get_plugin_config uses caching (lru_cache)."""
+        cfg1 = get_plugin_config("proxy", ProxyPluginConfig)
+        cfg2 = get_plugin_config("proxy", ProxyPluginConfig)
+        # Should be the same object due to lru_cache
+        assert cfg1 is cfg2
+
+    def test_proxy_config_fields_types(self):
+        """Test that ProxyPluginConfig fields have correct types."""
+        cfg = get_plugin_config("proxy", ProxyPluginConfig)
+        assert isinstance(cfg.proxy_urls, list)
+        assert isinstance(cfg.force_stream_apis, list)
+        assert isinstance(cfg.black_list, list)
+        assert isinstance(cfg.white_list, list)
+        
+        # All should be lists of strings
+        assert all(isinstance(url, str) for url in cfg.proxy_urls)
+        assert all(isinstance(api, str) for api in cfg.force_stream_apis)
+        assert all(isinstance(item, str) for item in cfg.black_list)
+        assert all(isinstance(item, str) for item in cfg.white_list)
+
+    def test_config_immutability_check(self):
+        """Test config behavior when attempting modifications."""
+        cfg = get_plugin_config("proxy", ProxyPluginConfig)
+        original_proxy_urls = cfg.proxy_urls.copy()
+        
+        # Modifying the returned list shouldn't affect future calls
+        cfg.proxy_urls.append("http://test.com")
+        
+        # Get fresh config
+        cfg2 = get_plugin_config("proxy", ProxyPluginConfig)
+        # Due to caching, this will be the same object, so this test
+        # verifies the caching behavior
+        assert cfg is cfg2

tests/test_consts.py

This is a new file.

"""
Comprehensive tests for constants defined in src/framex/consts.py.

Tests verify that critical constants maintain expected values and types,
ensuring stability across version changes and deployments.
"""

import pytest

from framex.consts import (
    PROJECT_NAME,
    VERSION,
    API_STR,
    BACKEND_NAME,
    APP_NAME,
    PROXY_PLUGIN_NAME,
    DEFAULT_ENV,
)


class TestConstants:
    """Test suite for framework constants."""

    def test_project_name(self):
        """Test PROJECT_NAME is correctly set."""
        assert PROJECT_NAME == "FrameX"
        assert isinstance(PROJECT_NAME, str)
        assert len(PROJECT_NAME) > 0

    def test_version_format(self):
        """Test VERSION follows semantic versioning format."""
        assert VERSION == "0.1.0"
        assert isinstance(VERSION, str)
        
        # Verify semantic version format (X.Y.Z)
        parts = VERSION.split(".")
        assert len(parts) == 3, "Version should have 3 parts (major.minor.patch)"
        assert all(part.isdigit() for part in parts), "All version parts should be numeric"

    def test_version_matches_pyproject(self):
        """Test VERSION matches the version in pyproject.toml."""
        import tomli
        from pathlib import Path
        
        pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
        with open(pyproject_path, "rb") as f:
            pyproject_data = tomli.load(f)
        
        pyproject_version = pyproject_data["project"]["version"]
        assert VERSION == pyproject_version, \
            f"consts.VERSION ({VERSION}) should match pyproject.toml version ({pyproject_version})"

    def test_api_str(self):
        """Test API_STR is properly formatted."""
        assert API_STR == "/api/v1"
        assert isinstance(API_STR, str)
        assert API_STR.startswith("/")
        assert "api" in API_STR.lower()

    def test_backend_name(self):
        """Test BACKEND_NAME is set."""
        assert BACKEND_NAME == "backend"
        assert isinstance(BACKEND_NAME, str)
        assert len(BACKEND_NAME) > 0

    def test_app_name(self):
        """Test APP_NAME is set."""
        assert APP_NAME == "default"
        assert isinstance(APP_NAME, str)
        assert len(APP_NAME) > 0

    def test_proxy_plugin_name(self):
        """Test PROXY_PLUGIN_NAME follows expected format."""
        assert PROXY_PLUGIN_NAME == "proxy.ProxyPlugin"
        assert isinstance(PROXY_PLUGIN_NAME, str)
        assert "." in PROXY_PLUGIN_NAME
        assert "proxy" in PROXY_PLUGIN_NAME.lower()

    def test_default_env_structure(self):
        """Test DEFAULT_ENV contains expected environment variables."""
        assert isinstance(DEFAULT_ENV, dict)
        assert len(DEFAULT_ENV) > 0
        
        # Verify expected keys
        expected_keys = {
            "RAY_COLOR_PREFIX",
            "RAY_DEDUP_LOGS",
            "RAY_SERVE_RUN_SYNC_IN_THREADPOOL"
        }
        assert set(DEFAULT_ENV.keys()) == expected_keys

    def test_default_env_values(self):
        """Test DEFAULT_ENV values are correctly set."""
        assert DEFAULT_ENV["RAY_COLOR_PREFIX"] == "1"
        assert DEFAULT_ENV["RAY_DEDUP_LOGS"] == "1"
        assert DEFAULT_ENV["RAY_SERVE_RUN_SYNC_IN_THREADPOOL"] == "1"
        
        # All values should be strings
        assert all(isinstance(v, str) for v in DEFAULT_ENV.values())

    def test_default_env_ray_related(self):
        """Test that all DEFAULT_ENV keys are Ray-related."""
        for key in DEFAULT_ENV.keys():
            assert key.startswith("RAY_"), f"Expected Ray-related env var, got {key}"

    def test_constants_immutability(self):
        """Test that constants are of immutable types."""
        # String constants should be strings
        assert isinstance(PROJECT_NAME, str)
        assert isinstance(VERSION, str)
        assert isinstance(API_STR, str)
        assert isinstance(BACKEND_NAME, str)
        assert isinstance(APP_NAME, str)
        assert isinstance(PROXY_PLUGIN_NAME, str)
        
        # DEFAULT_ENV is dict, but in practice should not be modified
        assert isinstance(DEFAULT_ENV, dict)

    def test_version_not_empty(self):
        """Test VERSION is not empty or placeholder."""
        assert VERSION
        assert VERSION != "0.0.0"
        assert VERSION != "unknown"
        assert VERSION != ""

    def test_api_str_valid_path(self):
        """Test API_STR is a valid URL path component."""
        assert not API_STR.endswith("/"), "API_STR should not end with slash"
        assert " " not in API_STR, "API_STR should not contain spaces"
        assert "\n" not in API_STR, "API_STR should not contain newlines"

    def test_all_constants_exported(self):
        """Test that all expected constants are importable."""
        from framex import consts
        
        assert hasattr(consts, "PROJECT_NAME")
        assert hasattr(consts, "VERSION")
        assert hasattr(consts, "API_STR")
        assert hasattr(consts, "BACKEND_NAME")
        assert hasattr(consts, "APP_NAME")
        assert hasattr(consts, "PROXY_PLUGIN_NAME")
        assert hasattr(consts, "DEFAULT_ENV")


class TestVersionSpecificBehavior:
    """Tests specific to the current version (0.1.0)."""

    def test_version_is_0_1_0(self):
        """Explicitly test that version is 0.1.0 as per the diff."""
        assert VERSION == "0.1.0", \
            "Version should be 0.1.0 after the downgrade from 0.2.0"

    def test_version_consistency_across_files(self):
        """Test version is consistent across different locations."""
        from framex.consts import VERSION as consts_version
        
        # Import from main package
        import framex
        plugin = framex.get_plugin("export")
        
        # Plugin version should match
        assert plugin.version == consts_version

    def test_version_not_dev(self):
        """Test that version is not a development version."""
        assert "dev" not in VERSION.lower()
        assert "alpha" not in VERSION.lower()
        assert "beta" not in VERSION.lower()
        assert "rc" not in VERSION.lower()

    def test_version_components(self):
        """Test individual version components."""
        major, minor, patch = VERSION.split(".")
        assert major == "0", "Major version should be 0"
        assert minor == "1", "Minor version should be 1"
        assert patch == "0", "Patch version should be 0"


class TestDefaultEnvConfiguration:
    """Detailed tests for DEFAULT_ENV configuration."""

    def test_ray_color_prefix_enabled(self):
        """Test RAY_COLOR_PREFIX is enabled."""
        assert DEFAULT_ENV["RAY_COLOR_PREFIX"] == "1"

    def test_ray_dedup_logs_enabled(self):
        """Test RAY_DEDUP_LOGS is enabled."""
        assert DEFAULT_ENV["RAY_DEDUP_LOGS"] == "1"

    def test_ray_serve_sync_in_threadpool_enabled(self):
        """Test RAY_SERVE_RUN_SYNC_IN_THREADPOOL is enabled."""
        assert DEFAULT_ENV["RAY_SERVE_RUN_SYNC_IN_THREADPOOL"] == "1"

    def test_default_env_can_be_used_for_os_environ(self):
        """Test DEFAULT_ENV can be used to update os.environ."""
        import os
        
        # Should be safe to update environ with these values
        for key, value in DEFAULT_ENV.items():
            assert isinstance(key, str)
            assert isinstance(value, str)
            # Verify they're valid env var names
            assert key.replace("_", "").isalnum()

    def test_default_env_no_sensitive_data(self):
        """Test DEFAULT_ENV doesn't contain sensitive information."""
        sensitive_keywords = ["password", "secret", "key", "token", "credential"]
        
        for key, value in DEFAULT_ENV.items():
            key_lower = key.lower()
            value_lower = value.lower()
            
            for keyword in sensitive_keywords:
                assert keyword not in key_lower, \
                    f"Env var name should not contain '{keyword}'"
                assert keyword not in value_lower, \
                    f"Env var value should not contain '{keyword}'"


class TestProxyPluginNameFormat:
    """Detailed tests for PROXY_PLUGIN_NAME format and usage."""

    def test_proxy_plugin_name_format(self):
        """Test PROXY_PLUGIN_NAME follows module.Class format."""
        parts = PROXY_PLUGIN_NAME.split(".")
        assert len(parts) == 2, "Should be in format 'module.Class'"
        
        module_name, class_name = parts
        assert module_name == "proxy"
        assert class_name == "ProxyPlugin"

    def test_proxy_plugin_name_case_sensitivity(self):
        """Test PROXY_PLUGIN_NAME case sensitivity."""
        assert "ProxyPlugin" in PROXY_PLUGIN_NAME
        assert PROXY_PLUGIN_NAME != PROXY_PLUGIN_NAME.lower()
        assert PROXY_PLUGIN_NAME != PROXY_PLUGIN_NAME.upper()

    def test_proxy_plugin_name_no_whitespace(self):
        """Test PROXY_PLUGIN_NAME has no whitespace."""
        assert " " not in PROXY_PLUGIN_NAME
        assert "\t" not in PROXY_PLUGIN_NAME
        assert "\n" not in PROXY_PLUGIN_NAME

    def test_proxy_plugin_name_matches_actual_plugin(self):
        """Test PROXY_PLUGIN_NAME corresponds to actual plugin structure."""
        # The format should match how plugins are referenced internally
        assert PROXY_PLUGIN_NAME.startswith("proxy.")
        assert not PROXY_PLUGIN_NAME.startswith(".")
        assert not PROXY_PLUGIN_NAME.endswith(".")

tests/test_plugin.py

@@ -12,3 +12,243 @@
 
     assert plugin.config
     assert plugin.config.model_dump() == {"id": 123, "name": "test"}
+
+import pytest
+from unittest.mock import AsyncMock, MagicMock, patch
+from pydantic import BaseModel
+
+from framex.plugin import call_plugin_api
+from framex.plugin.model import ApiType, PluginApi
+from framex.consts import PROXY_PLUGIN_NAME
+
+
+class SampleModel(BaseModel):
+    """Sample model for testing parameter conversion."""
+    field1: str
+    field2: int
+
+
+class TestCallPluginApi:
+    """Comprehensive tests for call_plugin_api function with proxy handling."""
+
+    @pytest.mark.asyncio
+    async def test_call_plugin_api_with_existing_api(self):
+        """Test calling an API that exists in the manager."""
+        # Setup
+        api = PluginApi(
+            api="test_api",
+            deployment_name="test_deployment",
+            params=[("param1", str), ("param2", int)]
+        )
+        
+        with patch("framex.plugin._manager.get_api", return_value=api), \
+             patch("framex.plugin.get_adapter") as mock_adapter:
+            mock_adapter.return_value.call_func = AsyncMock(return_value="test_result")
+            
+            # Execute
+            result = await call_plugin_api("test_api", param1="value1", param2=42)
+            
+            # Assert
+            assert result == "test_result"
+            mock_adapter.return_value.call_func.assert_called_once()
+
+    @pytest.mark.asyncio
+    async def test_call_plugin_api_with_basemodel_result(self):
+        """Test that BaseModel results are converted to dict with aliases."""
+        api = PluginApi(api="test_api", deployment_name="test_deployment")
+        model_result = SampleModel(field1="test", field2=123)
+        
+        with patch("framex.plugin._manager.get_api", return_value=api), \
+             patch("framex.plugin.get_adapter") as mock_adapter:
+            mock_adapter.return_value.call_func = AsyncMock(return_value=model_result)
+            
+            result = await call_plugin_api("test_api")
+            
+            assert isinstance(result, dict)
+            assert result == {"field1": "test", "field2": 123}
+
+    @pytest.mark.asyncio
+    async def test_call_plugin_api_with_proxy_success(self):
+        """Test proxy API call with successful response (status 200)."""
+        with patch("framex.plugin._manager.get_api", return_value=None), \
+             patch("framex.plugin.settings.server.enable_proxy", True), \
+             patch("framex.plugin.get_adapter") as mock_adapter:
+            
+            # Simulate proxy response
+            proxy_response = {"status": 200, "data": {"result": "proxy_success"}}
+            mock_adapter.return_value.call_func = AsyncMock(return_value=proxy_response)
+            
+            result = await call_plugin_api("/external/api")
+            
+            # Should return just the data field
+            assert result == {"result": "proxy_success"}
+
+    @pytest.mark.asyncio
+    async def test_call_plugin_api_with_proxy_empty_data(self):
+        """Test proxy API call that returns empty data with warning."""
+        with patch("framex.plugin._manager.get_api", return_value=None), \
+             patch("framex.plugin.settings.server.enable_proxy", True), \
+             patch("framex.plugin.get_adapter") as mock_adapter, \
+             patch("framex.plugin.logger") as mock_logger:
+            
+            proxy_response = {"status": 200, "data": None}
+            mock_adapter.return_value.call_func = AsyncMock(return_value=proxy_response)
+            
+            result = await call_plugin_api("/external/api")
+            
+            assert result is None
+            # Verify warning was logged
+            mock_logger.opt.return_value.warning.assert_called()
+
+    @pytest.mark.asyncio
+    async def test_call_plugin_api_with_proxy_error_status(self):
+        """Test proxy API call with non-200 status logs error."""
+        with patch("framex.plugin._manager.get_api", return_value=None), \
+             patch("framex.plugin.settings.server.enable_proxy", True), \
+             patch("framex.plugin.get_adapter") as mock_adapter, \
+             patch("framex.plugin.logger") as mock_logger:
+            
+            proxy_response = {"status": 500, "data": None}
+            mock_adapter.return_value.call_func = AsyncMock(return_value=proxy_response)
+            
+            result = await call_plugin_api("/external/api")
+            
+            assert result is None
+            # Verify error was logged
+            mock_logger.opt.return_value.error.assert_called()
+
+    @pytest.mark.asyncio
+    async def test_call_plugin_api_with_proxy_non_200_with_data(self):
+        """Test proxy API call with non-200 status but still returns data."""
+        with patch("framex.plugin._manager.get_api", return_value=None), \
+             patch("framex.plugin.settings.server.enable_proxy", True), \
+             patch("framex.plugin.get_adapter") as mock_adapter, \
+             patch("framex.plugin.logger") as mock_logger:
+            
+            proxy_response = {"status": 400, "data": {"error": "bad request"}}
+            mock_adapter.return_value.call_func = AsyncMock(return_value=proxy_response)
+            
+            result = await call_plugin_api("/external/api")
+            
+            # Should still return data even with error status
+            assert result == {"error": "bad request"}
+            # Verify error was logged
+            mock_logger.opt.return_value.error.assert_called()
+
+    @pytest.mark.asyncio
+    async def test_call_plugin_api_not_found_no_proxy(self):
+        """Test API not found when proxy is disabled raises RuntimeError."""
+        with patch("framex.plugin._manager.get_api", return_value=None), \
+             patch("framex.plugin.settings.server.enable_proxy", False):
+            
+            with pytest.raises(RuntimeError, match="API test_api is not found"):
+                await call_plugin_api("test_api")
+
+    @pytest.mark.asyncio
+    async def test_call_plugin_api_not_found_non_slash_with_proxy(self):
+        """Test non-slash prefixed API not found with proxy enabled raises error."""
+        with patch("framex.plugin._manager.get_api", return_value=None), \
+             patch("framex.plugin.settings.server.enable_proxy", True):
+            
+            with pytest.raises(RuntimeError, match="API test_api is not found"):
+                await call_plugin_api("test_api")
+
+    @pytest.mark.asyncio
+    async def test_call_plugin_api_with_dict_to_basemodel_conversion(self):
+        """Test automatic conversion of dict parameters to BaseModel."""
+        api = PluginApi(
+            api="test_api",
+            deployment_name="test_deployment",
+            params=[("model_param", SampleModel)]
+        )
+        
+        with patch("framex.plugin._manager.get_api", return_value=api), \
+             patch("framex.plugin.get_adapter") as mock_adapter:
+            mock_adapter.return_value.call_func = AsyncMock(return_value="success")
+            
+            # Pass dict that should be converted to SampleModel
+            result = await call_plugin_api(
+                "test_api",
+                model_param={"field1": "test", "field2": 456}
+            )
+            
+            # Verify the call was made and dict was converted
+            assert result == "success"
+            call_args = mock_adapter.return_value.call_func.call_args
+            assert isinstance(call_args[1]["model_param"], SampleModel)
+
+    @pytest.mark.asyncio
+    async def test_call_plugin_api_with_interval_apis(self):
+        """Test using interval_apis parameter to override manager lookup."""
+        api = PluginApi(api="test_api", deployment_name="test_deployment")
+        interval_apis = {"test_api": api}
+        
+        with patch("framex.plugin._manager.get_api") as mock_get_api, \
+             patch("framex.plugin.get_adapter") as mock_adapter:
+            mock_adapter.return_value.call_func = AsyncMock(return_value="interval_result")
+            
+            result = await call_plugin_api("test_api", interval_apis=interval_apis)
+            
+            # Manager get_api should not be called
+            mock_get_api.assert_not_called()
+            assert result == "interval_result"
+
+    @pytest.mark.asyncio
+    async def test_call_plugin_api_proxy_creates_correct_plugin_api(self):
+        """Test that proxy fallback creates PluginApi with correct parameters."""
+        with patch("framex.plugin._manager.get_api", return_value=None), \
+             patch("framex.plugin.settings.server.enable_proxy", True), \
+             patch("framex.plugin.get_adapter") as mock_adapter, \
+             patch("framex.plugin.logger"):
+            
+            mock_adapter.return_value.call_func = AsyncMock(
+                return_value={"status": 200, "data": "ok"}
+            )
+            
+            await call_plugin_api("/proxy/test")
+            
+            # Check the PluginApi passed to call_func
+            call_args = mock_adapter.return_value.call_func.call_args
+            api = call_args[0][0]
+            assert isinstance(api, PluginApi)
+            assert api.api == "/proxy/test"
+            assert api.deployment_name == PROXY_PLUGIN_NAME
+            assert api.call_type == ApiType.PROXY
+
+    @pytest.mark.asyncio
+    async def test_call_plugin_api_regular_dict_result_not_proxy(self):
+        """Test that regular dict results (non-proxy) are returned as-is."""
+        api = PluginApi(api="test_api", deployment_name="test_deployment")
+        
+        with patch("framex.plugin._manager.get_api", return_value=api), \
+             patch("framex.plugin.get_adapter") as mock_adapter:
+            # Regular dict result (not from proxy)
+            mock_adapter.return_value.call_func = AsyncMock(
+                return_value={"key": "value", "status": 200}
+            )
+            
+            result = await call_plugin_api("test_api")
+            
+            # Should return the entire dict, not extract "data"
+            assert result == {"key": "value", "status": 200}
+
+    @pytest.mark.asyncio
+    async def test_call_plugin_api_with_multiple_kwargs(self):
+        """Test calling API with multiple keyword arguments."""
+        api = PluginApi(
+            api="test_api",
+            deployment_name="test_deployment",
+            params=[("a", int), ("b", str), ("c", bool)]
+        )
+        
+        with patch("framex.plugin._manager.get_api", return_value=api), \
+             patch("framex.plugin.get_adapter") as mock_adapter:
+            mock_adapter.return_value.call_func = AsyncMock(return_value="multi_args")
+            
+            result = await call_plugin_api("test_api", a=1, b="test", c=True)
+            
+            assert result == "multi_args"
+            call_kwargs = mock_adapter.return_value.call_func.call_args[1]
+            assert call_kwargs["a"] == 1
+            assert call_kwargs["b"] == "test"
+            assert call_kwargs["c"] is True

Copy link
Copy Markdown

@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

♻️ Duplicate comments (2)
.github/workflows/release.yml (1)

85-111: Fix the dependency declaration to access release outputs.

The public-pypi job depends on public-testpypi (line 87) but tries to access needs.release.outputs.released (line 88). In GitHub Actions, you can only access outputs from jobs explicitly listed in the needs array.

Apply this diff:

   public-pypi:
     runs-on: ubuntu-latest
-    needs: public-testpypi
+    needs: [public-testpypi, release]
     if: ${{ needs.release.outputs.released == 'true' }}

Based on learnings, static analysis hint at line 88 correctly identifies this issue.

src/framex/plugin/__init__.py (1)

112-122: Add validation and error handling for proxy responses.

The proxy result handling has several issues:

  1. Line 116: result.get("data") assumes result is a dict, but there's no validation. If the proxy returns a non-dict, this raises AttributeError.
  2. Lines 119-120: When status ≠ 200, an error is logged but the function still returns res (which may be None), potentially causing silent failures downstream.
  3. No validation of the proxy response structure {"data": ..., "status": ...}.

Apply this diff to add validation and proper error handling:

     result = await get_adapter().call_func(api, **kwargs)
     if isinstance(result, BaseModel):
         return result.model_dump(by_alias=True)
     if use_proxy:
+        if not isinstance(result, dict):
+            raise RuntimeError(f"Proxy API {api_name} returned non-dict result: {type(result)}")
+        if "status" not in result:
+            raise RuntimeError(f"Proxy API {api_name} returned invalid response: missing 'status' field")
         res = result.get("data")
-        if res is None:
-            logger.opt(colors=True).warning(f"API {api_name} return empty data")
-        if result.get("status") != 200:
+        status = result.get("status")
+        if status != 200:
             logger.opt(colors=True).error(f"Proxy API {api_name} call unnormal: <r>{result}</r>")
+            raise RuntimeError(f"Proxy API {api_name} returned status {status}")
+        if res is None:
+            logger.opt(colors=True).warning(f"API {api_name} returned empty data")
         return res
     return result
🧹 Nitpick comments (1)
tests/test_config.py (1)

1-6: Strengthen the test assertion.

The test assertion was changed from verifying a specific attribute value (cfg.proxy_urls == []) to only checking truthiness (assert cfg). This significantly weakens the test coverage and provides minimal validation.

Consider strengthening the test by verifying specific ProxyPluginConfig attributes:

 def test_config():
     from framex.plugin import get_plugin_config
     from framex.plugins.proxy.config import ProxyPluginConfig
 
     cfg = get_plugin_config("proxy", ProxyPluginConfig)
-    assert cfg
+    assert isinstance(cfg, ProxyPluginConfig)
+    assert cfg.proxy_urls is not None
+    # Add more specific assertions based on expected default values
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81a2a81 and ae641bd.

📒 Files selected for processing (5)
  • .github/workflows/release.yml (2 hunks)
  • pytest.ini (1 hunks)
  • src/framex/plugin/__init__.py (3 hunks)
  • tests/test_config.py (1 hunks)
  • tests/test_plugin.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/test_plugin.py (3)
src/framex/plugin/__init__.py (1)
  • call_plugin_api (78-122)
src/framex/plugin/model.py (2)
  • ApiType (22-26)
  • PluginApi (29-37)
src/framex/adapter/base.py (1)
  • call_func (29-39)
src/framex/plugin/__init__.py (2)
src/framex/adapter/__init__.py (1)
  • get_adapter (9-13)
src/framex/adapter/base.py (1)
  • call_func (29-39)
🪛 actionlint (1.7.8)
.github/workflows/release.yml

88-88: property "release" is not defined in object type {public-testpypi: {outputs: {}; result: string}}

(expression)

🔇 Additional comments (4)
tests/test_plugin.py (3)

17-72: Excellent test structure and coverage.

The test class is well-organized with:

  • Clear imports and helper models
  • Comprehensive docstrings
  • Proper async test patterns with AsyncMock
  • Good coverage of BaseModel conversion behavior

73-144: Tests validate current proxy behavior, not the recommended fix.

The proxy-related tests (lines 74-144) comprehensively cover the current implementation, including:

  • Success with status 200 (lines 74-88)
  • Empty data with warning (lines 91-106)
  • Error status logging (lines 109-124)
  • Non-200 status with data (lines 127-143)

However, these tests validate the current behavior where non-200 status codes log errors but still return data (lines 122, 141). Based on the earlier review comment on src/framex/plugin/__init__.py (lines 112-122), the recommended fix would raise exceptions for non-200 status codes instead of returning potentially invalid data.

If you implement the recommended fix for proxy error handling (raising exceptions on non-200 status), these tests will need to be updated to expect exceptions instead of return values. Consider whether the current "log but continue" behavior or the "raise exception" behavior better serves your use case.


145-259: Comprehensive test coverage for remaining scenarios.

These tests provide excellent coverage for:

  • API not found behavior with/without proxy (lines 146-163)
  • Automatic dict-to-BaseModel conversion (lines 166-182)
  • interval_apis override mechanism (lines 185-200)
  • Proxy PluginApi creation with correct parameters (lines 203-221)
  • Critical: Regular dict results returned as-is without data extraction (lines 224-238) - this test ensures non-proxy responses aren't incorrectly processed
  • Multiple keyword arguments handling (lines 241-259)

The test at lines 224-238 is particularly important as it verifies that only proxy responses undergo data extraction, preventing incorrect processing of regular dict returns.

pytest.ini (1)

25-26: Default proxy configuration values are appropriate and match the commented-out settings.

The ProxyPluginConfig class in src/framex/plugins/proxy/config.py defines both proxy_urls and force_stream_apis with default values of empty lists ([]), which exactly matches the values that are now commented out in pytest.ini lines 25-26. Commenting out these lines will use the defaults from the class definition, which is safe and appropriate for the test environment—no proxies are configured and no stream APIs are forced by default.

Copy link
Copy Markdown

@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

♻️ Duplicate comments (2)
src/framex/plugin/__init__.py (1)

112-128: Proxy response validation is properly implemented.

The validation sequence (dict type → status field presence → status code check → data extraction) correctly addresses the concerns from the previous review. The error messages are descriptive and aid debugging.

.github/workflows/release.yml (1)

85-111: LGTM! Dependency declaration is now correct.

The public-pypi job correctly declares needs: [public-testpypi, release], ensuring it can access needs.release.outputs.released while maintaining the sequential execution after test PyPI publishing.

🧹 Nitpick comments (3)
tests/test_config.py (1)

6-7: Consider a more meaningful assertion for proxy_urls.

Since ProxyPluginConfig.proxy_urls has a default value of [], the is not None check will always pass. Consider asserting the type or expected value instead:

     assert isinstance(cfg, ProxyPluginConfig)
-    assert cfg.proxy_urls is not None
+    assert isinstance(cfg.proxy_urls, list)
tests/test_plugin.py (2)

15-24: Move imports to the top of the file.

Imports are conventionally placed at the top of Python files. Having imports mid-file (after test_get_plugin) reduces readability and violates PEP 8 conventions.

+import framex
+from unittest.mock import AsyncMock, patch
+
+import pytest
+from pydantic import BaseModel
+
+from framex.consts import PROXY_PLUGIN_NAME, VERSION
+from framex.plugin import call_plugin_api
+from framex.plugin.model import ApiType, PluginApi
-import framex
-from framex.consts import VERSION
 # ... (move remaining imports to top)

108-123: Add tests for new proxy validation error paths.

The implementation now validates that proxy results are dicts with a status field. Consider adding tests for these error cases:

@pytest.mark.asyncio
async def test_call_plugin_api_with_proxy_non_dict_result(self):
    """Test proxy API call raises when result is not a dict."""
    with (
        patch("framex.plugin._manager.get_api", return_value=None),
        patch("framex.plugin.settings.server.enable_proxy", True),
        patch("framex.plugin.get_adapter") as mock_adapter,
        patch("framex.plugin.logger"),
    ):
        mock_adapter.return_value.call_func = AsyncMock(return_value="not_a_dict")
        with pytest.raises(RuntimeError, match="returned non-dict result"):
            await call_plugin_api("/external/api")

@pytest.mark.asyncio
async def test_call_plugin_api_with_proxy_missing_status(self):
    """Test proxy API call raises when status field is missing."""
    with (
        patch("framex.plugin._manager.get_api", return_value=None),
        patch("framex.plugin.settings.server.enable_proxy", True),
        patch("framex.plugin.get_adapter") as mock_adapter,
        patch("framex.plugin.logger"),
    ):
        mock_adapter.return_value.call_func = AsyncMock(return_value={"data": "value"})
        with pytest.raises(RuntimeError, match="missing 'status' field"):
            await call_plugin_api("/external/api")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae641bd and fd98e21.

📒 Files selected for processing (4)
  • .github/workflows/release.yml (2 hunks)
  • src/framex/plugin/__init__.py (3 hunks)
  • tests/test_config.py (1 hunks)
  • tests/test_plugin.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
tests/test_config.py (1)
src/framex/plugins/proxy/config.py (1)
  • ProxyPluginConfig (6-11)
tests/test_plugin.py (3)
src/framex/plugin/__init__.py (1)
  • call_plugin_api (78-128)
src/framex/plugin/model.py (2)
  • ApiType (22-26)
  • PluginApi (29-37)
src/framex/adapter/base.py (1)
  • call_func (29-39)
src/framex/plugin/__init__.py (2)
src/framex/adapter/__init__.py (1)
  • get_adapter (9-13)
src/framex/adapter/base.py (1)
  • call_func (29-39)
🪛 Ruff (0.14.5)
src/framex/plugin/__init__.py

117-117: Avoid specifying long messages outside the exception class

(TRY003)


119-119: Avoid specifying long messages outside the exception class

(TRY003)


124-124: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (3)
src/framex/plugin/__init__.py (1)

84-95: LGTM! Proxy flag tracking is well-implemented.

The use_proxy flag cleanly separates the proxy path from regular API calls, enabling distinct response handling downstream.

tests/test_plugin.py (1)

34-54: LGTM! Test class is well-structured with comprehensive coverage.

The TestCallPluginApi class provides thorough coverage of the call_plugin_api function, including existing API calls, BaseModel conversion, proxy scenarios, error paths, and parameter handling.

.github/workflows/release.yml (1)

59-83: LGTM! Test PyPI publishing job is well-configured.

The public-testpypi job correctly uses trusted publishing with id-token: write permission and specifies the test PyPI repository URL.

Copy link
Copy Markdown

@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 (4)
src/framex/config.py (1)

36-46: New legal_proxy_code config looks good; consider immutability as a minor tweak

The legal_proxy_code: list[int] = [200] default works well for proxy validation and keeps behavior configurable. If you want to avoid accidental mutation and make membership checks semantically clearer, you could switch to a tuple (or a small set) later, but that’s purely a style/defensive choice, not a blocker.

tests/test_plugin.py (1)

108-124: Error-status proxy behavior test is good; consider legal codes configurability

The 500‑status case correctly expects a RuntimeError and verifies error logging. One small thought: since legal_proxy_code is now configurable, these tests assume the default [200]; if you foresee running tests under custom configs, you may want to patch settings.server.legal_proxy_code here to decouple from environment-specific overrides.

src/framex/plugin/__init__.py (2)

112-128: Result handling and proxy validation are much safer; minor edge case to consider

The new flow:

  • Always dumps BaseModel results to dicts.
  • For proxy calls, enforces dict type, requires a status field, validates against settings.server.legal_proxy_code, logs on illegal statuses, and warns on empty data.

This addresses the earlier concerns about unsafe .get usage and silent failures, and ties nicely into the new legal_proxy_code config. One nuance: if a proxy plugin itself returns a BaseModel, it will bypass the proxy validations because the BaseModel branch runs before the use_proxy block; if you intend proxy results to always follow the {status, data} dict contract, you might want to either (a) document that requirement clearly or (b) run the proxy validation before the generic BaseModel check.


116-124: Exception messages are clear; Ruff TRY003 warnings are low priority

Raising RuntimeError with detailed messages for non‑dict results, missing status, and illegal proxy statuses makes debugging straightforward. Ruff’s TRY003 hints about “long messages outside the exception class” are stylistic only here; I’d treat them as non‑blocking unless you want stricter adherence to that rule.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd98e21 and 328fb17.

📒 Files selected for processing (3)
  • src/framex/config.py (1 hunks)
  • src/framex/plugin/__init__.py (3 hunks)
  • tests/test_plugin.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/test_plugin.py (3)
src/framex/plugin/__init__.py (1)
  • call_plugin_api (78-128)
src/framex/plugin/model.py (2)
  • ApiType (22-26)
  • PluginApi (29-37)
src/framex/adapter/base.py (1)
  • call_func (29-39)
src/framex/plugin/__init__.py (2)
src/framex/adapter/__init__.py (1)
  • get_adapter (9-13)
src/framex/adapter/base.py (1)
  • call_func (29-39)
🪛 Ruff (0.14.5)
src/framex/plugin/__init__.py

117-117: Avoid specifying long messages outside the exception class

(TRY003)


119-119: Avoid specifying long messages outside the exception class

(TRY003)


124-124: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (15)
tests/test_plugin.py (13)

17-32: Imports and SampleModel helper are well‑structured

The additional imports and SampleModel helper keep the tests focused and readable; reusing a single BaseModel test type avoids duplication across multiple cases.


37-55: Happy path test for existing API is solid

test_call_plugin_api_with_existing_api cleanly verifies the non‑proxy, non‑conversion path and asserts the adapter is called once, which is a good sanity check around the basic flow.


56-72: BaseModel result conversion test matches new behavior

test_call_plugin_api_with_basemodel_result correctly asserts that a BaseModel result is converted to a dict, aligning with the new unconditional model_dump(by_alias=True) behavior.


73-89: Proxy success case is covered appropriately

test_call_plugin_api_with_proxy_success neatly validates that proxy responses with status=200 return only the data field, matching the proxy extraction logic.


90-107: Empty-data proxy response and logging are well tested

The empty data + warning case is explicitly exercised, including logger expectations; this is a good guard against regressions in “no content” proxy responses.


125-144: Not-found cases with/without proxy are well distinguished

The two tests for “API not found” with proxy disabled vs. enabled/non‑slash nicely pin down the intended error semantics for these branches.


145-163: Dict→BaseModel parameter conversion is validated correctly

test_call_plugin_api_with_dict_to_basemodel_conversion confirms that dict kwargs are converted into the expected SampleModel instance before reaching the adapter, which is exactly what the param‑mapping logic is supposed to do.


164-181: Interval APIs override behavior is precise

The interval_apis test ensures manager lookup is bypassed when an override is provided, and asserts both result and non‑usage of _manager.get_api; this is a good characterization of that feature.


182-202: Proxy PluginApi construction is thoroughly asserted

test_call_plugin_api_proxy_creates_correct_plugin_api nicely inspects the actual PluginApi instance passed to call_func, ensuring api, deployment_name, and call_type are all as expected for proxy fallbacks.


203-219: Non-proxy dict results are clearly distinguished from proxy behavior

The “regular dict result” test accurately verifies that non‑proxy calls returning dicts are passed through unchanged, which guards against accidental overloading of proxy-specific handling.


220-239: Multi-kwargs propagation test is clear and sufficient

The multiple‑kwargs case confirms both the return value and exact kwargs passed into call_func, which is a good regression check on the basic argument plumbing.


241-252: Proxy non-dict result failure path is well covered

The test for non‑dict proxy results correctly expects a RuntimeError and checks only the key substring in the message, making it robust to minor wording changes.


254-265: Missing-status proxy response is validated correctly

The missing status field case is explicitly tested and tied to the new validation logic, which is important for keeping proxy contracts strict.

src/framex/plugin/__init__.py (2)

78-99: Proxy fallback setup and use_proxy flag are coherent; consider impact of signature change

The use_proxy flag and construction of a PluginApi with call_type=ApiType.PROXY when an API is missing but proxy is enabled are consistent with the adapter’s proxy handling and the new tests. One thing to double‑check: removing the return_model_dump parameter from call_plugin_api and always returning a dict for BaseModel results is a behavior change—please ensure any existing call sites (including external users) are updated or that this is an intentional breaking change.


100-111: Dict-to-BaseModel parameter coercion is appropriate and defensive

The param_type_map + issubclass(expected_type, BaseModel) guard, along with the wrapped exception on conversion failure, makes the kwargs→model conversion predictable and debuggable. This is a reasonable balance between convenience and safety.

@touale touale merged commit fb50b6a into master Nov 26, 2025
7 checks passed
@touale touale deleted the feat/improve-proxy-handle branch December 18, 2025 02:39
@coderabbitai coderabbitai bot mentioned this pull request Dec 26, 2025
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