Skip to content

Conversation

@NiveditJain
Copy link
Member

No description provided.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 23, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Configurable default logging with environment variables; richer, more structured runtime logs.
    • Clear per-worker logging for easier tracing in multi-worker scenarios.
  • Bug Fixes
    • Improved error handling for enqueue and notification paths, with better retry behavior and clearer failure messages.
  • Tests
    • Added comprehensive unit and integration tests covering runtime, nodes, state manager, package exports, and versioning.
  • Chores
    • Bumped SDK version to 0.0.7b9.
    • Updated dev dependencies and pytest configuration (asyncio auto mode, defaults, test paths).

Walkthrough

Adds a default logging initializer (env-configurable), expands logging across runtime flows, changes Runtime._worker to accept a per-worker index and creates per-worker tasks, makes enqueue exceptions propagate, and adds many comprehensive tests; minor type-ignore edits in one test.

Changes

Cohort / File(s) Summary
Runtime logging, worker indexing, and enqueue behavior
python-sdk/exospherehost/runtime.py
Add _setup_default_logging() (respects EXOSPHERE_DISABLE_DEFAULT_LOGGING, EXOSPHERE_LOG_LEVEL); switch to logging.getLogger; add debug/info/error logs across config, register, enqueue, notify, secrets, and worker flows; _worker(self)_worker(self, idx: int) and create per-worker tasks; _enqueue/calls now raise/log failures and back off on exceptions.
Comprehensive runtime & state manager tests
python-sdk/tests/test_runtime_comprehensive.py, python-sdk/tests/test_statemanager_comprehensive.py, python-sdk/tests/test_integration.py, python-sdk/tests/test_base_node_comprehensive.py
Add extensive tests exercising Runtime and StateManager behaviors, graph upsert/poll, registration, enqueue/notify flows, secrets handling, node execution variants (single/list/none/error), concurrency, integration flows, and mocked aiohttp interactions.
Package & version tests
python-sdk/tests/test_package_init.py, python-sdk/tests/test_version.py
Add tests validating package exports, all, imports, package metadata, and version consistency.
Runtime validation minor edits
python-sdk/tests/test_runtime_validation.py
Add # type: ignore comments to silence specific type-check warnings; no runtime behavior change.
Version bump
python-sdk/exospherehost/_version.py
Update version from "0.0.7b8" to "0.0.7b9".
Dev dependency & pytest config
python-sdk/pyproject.toml
Add pytest-asyncio>=0.24.0 to dev dependencies and add [tool.pytest.ini_options] (asyncio_mode, addopts, testpaths).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant R as Runtime
  participant L as Logger
  participant SM as StateManager (HTTP)
  participant S as Secrets (HTTP)
  participant N as Node Impl

  rect rgba(220,235,255,0.35)
    note over R,L: Startup: default logging + env config
    R->>L: _setup_default_logging()
    R->>R: _set_config_from_env() (logs fallbacks)
    R->>SM: _register(nodes) (logs request/response)
    SM-->>R: registration response (logged)
  end

  par Workers[0..N-1]
    rect rgba(230,255,230,0.35)
      note over R,N: Per-worker indexed loop (idx logged)
      R->>R: _worker(idx) start (log idx)
      loop Poll/Enqueue/Execute
        R->>SM: poll states
        SM-->>R: states
        R->>R: _enqueue(states)
        alt enqueue failure
          R->>L: log failure
          R--x R: exception re-raised
        else enqueue success
          R->>S: _get_secrets(state)
          S-->>R: secrets (or 404 logged)
          R->>N: execute(inputs,secrets) (log start/result)
          alt execution success
            R->>SM: _notify_executed(state, outputs)
          else execution error
            R->>SM: _notify_errored(state, traceback)
          end
        end
      end
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • nk-ag

Poem

A rabbit flips the logging switch,
Hops count workers—idx by twitch.
States line up, secrets in tow,
We log each hop, each high and low.
Thump-thump: the runtime's tidy glow! 🐇


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5dec961 and 85da3a5.

📒 Files selected for processing (3)
  • python-sdk/exospherehost/runtime.py (11 hunks)
  • python-sdk/pyproject.toml (1 hunks)
  • python-sdk/tests/test_integration.py (1 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Summary of Changes

Hello @NiveditJain, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces comprehensive logging capabilities to the Python SDK's Runtime component. It establishes a flexible default logging configuration that respects existing user setups and environment variables, and integrates detailed informational and error logging across crucial operations such as runtime initialization, node registration, state enqueueing, and worker execution. This enhancement significantly improves observability and debugging for the SDK.

Highlights

  • Configurable Default Logging: A new _setup_default_logging function is introduced to configure basic logging if no handlers are already present, allowing customization via EXOSPHERE_LOG_LEVEL and EXOSPHERE_DISABLE_DEFAULT_LOGGING environment variables.
  • Enhanced Operational Visibility: Informational logs have been added for key runtime events, including the use of environment variables for configuration, node registration status, and the enqueueing of states.
  • Improved Error Diagnostics: Error logging is now more robust, capturing failures during node registration, state enqueueing, and worker execution, with tracebacks included for execution errors.
  • Detailed Worker Activity Logging: The _worker function now logs its startup, the execution of individual states, and the retrieval of secrets and outputs, providing better insight into processing flow.
  • Refined Exception Handling: Exceptions in _enqueue_call and _enqueue are now explicitly re-raised after logging, ensuring proper error propagation.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@codecov
Copy link

codecov bot commented Aug 23, 2025

Codecov Report

❌ Patch coverage is 86.48649% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
python-sdk/exospherehost/runtime.py 86.11% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces logging to the SDK, which is a valuable addition for observability. The implementation of a default logging setup that respects user configurations is well-done. However, I've identified a few critical issues in the new logging code. There are a couple of instances where log messages will cause KeyError exceptions due to incorrect dictionary key usage. Additionally, there's a potential UnboundLocalError in an exception handler that could crash worker tasks. Addressing these issues is crucial for the stability of the runtime.

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: 10

Caution

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

⚠️ Outside diff range comments (3)
python-sdk/exospherehost/runtime.py (3)

170-198: Registration logs are fine; consider reducing payload verbosity.

Listing every node at INFO can get noisy in large deployments. Prefer logging the count at INFO and full names at DEBUG.

-        logger.info(f"Registering nodes: {[f\"{self._namespace}/{node.__name__}\" for node in self._nodes]}")
+        logger.info("Registering %d nodes for namespace %s", len(self._nodes), self._namespace)
+        logger.debug("Nodes: %s", [f"{self._namespace}/{node.__name__}" for node in self._nodes])
...
-                logger.info(f"Registered nodes: {[f\"{self._namespace}/{node.__name__}\" for node in self._nodes]}")
+                logger.info("Registered %d nodes for namespace %s", len(self._nodes), self._namespace)
+                logger.debug("Registered nodes: %s", [f"{self._namespace}/{node.__name__}" for node in self._nodes])

253-259: Optional: consistent error handling for notify paths.

_enqueue_call raises on non-200, but _notify_executed/_notify_errored do not. Consider a consistent policy (either log-and-continue or raise) and possibly retries with backoff to handle transient errors.


200-220: Efficiency: consider a shared aiohttp session.

Creating a new ClientSession per request is expensive. Consider a lifecycle-managed session created in _start and closed on shutdown.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a813edd and e6de04e.

📒 Files selected for processing (2)
  • python-sdk/exospherehost/runtime.py (12 hunks)
  • python-sdk/tests/test_runtime_validation.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
python-sdk/tests/test_runtime_validation.py (2)
python-sdk/tests/test_base_node.py (2)
  • Outputs (10-11)
  • Inputs (7-8)
python-sdk/exospherehost/node/BaseNode.py (1)
  • BaseNode (6-100)
python-sdk/exospherehost/runtime.py (1)
python-sdk/exospherehost/node/BaseNode.py (4)
  • BaseNode (6-100)
  • _execute (70-83)
  • Inputs (29-35)
  • Secrets (45-68)
🔇 Additional comments (4)
python-sdk/exospherehost/runtime.py (4)

12-12: LGTM: module-level logger.

Using a module-scoped logger is consistent and appropriate.


84-85: Initialize logging before using the runtime — good placement.

Calling _setup_default_logging() in __init__ ensures logs from validation and startup are captured. No concerns.


406-407: LGTM: per-worker indices make logs traceable.

Passing indices to _worker improves observability and troubleshooting.


216-218: Confirm desired fail-fast semantics for state-enqueue failures

It looks like the code in
python-sdk/exospherehost/runtime.py (lines 216–218)
now logs and immediately raises on any non-200 response:

    logger.error(f"Failed to enqueue states: {res}")
    raise RuntimeError(f"Failed to enqueue states: {res}")

That change will cause the poller to stop and the entire runtime to tear down (via your asyncio.gather). If the downstream API can return transient errors (e.g. occasional 500s or network blips), you may want to retry with exponential backoff rather than failing fast on the first hiccup.

Please verify whether you intend to:

  • Fail fast on any enqueue error (current behavior).
  • Or implement a retry/backoff strategy to tolerate transient failures.

If you’d like to tolerate transients, here’s a minimal sketch:

-                if response.status != 200:
-                    logger.error(f"Failed to enqueue states: {res}")
-                    raise RuntimeError(f"Failed to enqueue states: {res}")
+                if response.status != 200:
+                    logger.error("Failed to enqueue states (status=%s): %s", response.status, res)
+                    return {"states": []}  # signal “no work” so upstream can back off

And in your _enqueue loop, track backoff:

# pseudo-code in _enqueue
backoff = min(backoff * 2, 30) if not data.get("states") else 1
await asyncio.sleep(backoff)

If you do want true fail-fast semantics, this change is fine as-is.

- Introduced a suite of tests for BaseNode, covering initialization, execution, and edge cases, ensuring robust validation of node behavior.
- Added integration tests for Runtime and StateManager, validating state management, error handling, and interaction with external services.
- Implemented tests for package imports and structure, ensuring all expected components are accessible and correctly configured.
- Enhanced test coverage for concurrency scenarios, verifying the correct handling of multiple simultaneous executions and state triggers.
- Improved overall test organization and clarity, contributing to a more maintainable and reliable codebase.
- Added `# type: ignore` comments to suppress type checking warnings in various test files, ensuring compatibility with type checkers while maintaining existing functionality.
- Updated test cases across multiple modules, including BaseNode, StateManager, and integration tests, to enhance clarity and prevent type-related issues during testing.
- Improved overall test robustness by ensuring that type hints do not interfere with test execution.
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: 26

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e6de04e and 1458925.

📒 Files selected for processing (6)
  • python-sdk/tests/test_base_node_comprehensive.py (1 hunks)
  • python-sdk/tests/test_integration.py (1 hunks)
  • python-sdk/tests/test_package_init.py (1 hunks)
  • python-sdk/tests/test_runtime_comprehensive.py (1 hunks)
  • python-sdk/tests/test_statemanager_comprehensive.py (1 hunks)
  • python-sdk/tests/test_version.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
python-sdk/tests/test_version.py (1)
python-sdk/tests/test_package_init.py (1)
  • test_version_import (73-80)
python-sdk/tests/test_runtime_comprehensive.py (3)
python-sdk/exospherehost/runtime.py (14)
  • Runtime (45-424)
  • _setup_default_logging (14-42)
  • _get_enque_endpoint (133-137)
  • _get_executed_endpoint (139-143)
  • _get_errored_endpoint (145-149)
  • _get_register_endpoint (151-155)
  • _get_secrets_endpoint (157-161)
  • _register (163-198)
  • _enqueue_call (200-219)
  • _worker (356-392)
  • _notify_executed (240-259)
  • _notify_errored (261-280)
  • _get_secrets (282-297)
  • start (410-424)
python-sdk/exospherehost/node/BaseNode.py (1)
  • BaseNode (6-100)
python-sdk/tests/test_integration.py (13)
  • create_mock_aiohttp_session (9-40)
  • Inputs (44-46)
  • Inputs (64-65)
  • Inputs (79-80)
  • Outputs (48-50)
  • Outputs (67-68)
  • Outputs (82-83)
  • Secrets (52-54)
  • Secrets (70-71)
  • Secrets (85-86)
  • execute (56-60)
  • execute (73-75)
  • execute (88-91)
python-sdk/tests/test_package_init.py (3)
python-sdk/exospherehost/runtime.py (1)
  • Runtime (45-424)
python-sdk/exospherehost/node/BaseNode.py (1)
  • BaseNode (6-100)
python-sdk/exospherehost/statemanager.py (2)
  • StateManager (42-227)
  • TriggerState (10-39)
python-sdk/tests/test_base_node_comprehensive.py (1)
python-sdk/exospherehost/node/BaseNode.py (2)
  • BaseNode (6-100)
  • _execute (70-83)
python-sdk/tests/test_statemanager_comprehensive.py (1)
python-sdk/exospherehost/statemanager.py (8)
  • StateManager (42-227)
  • TriggerState (10-39)
  • _get_trigger_state_endpoint (61-62)
  • _get_upsert_graph_endpoint (64-65)
  • _get_get_graph_endpoint (67-68)
  • trigger (70-133)
  • get_graph (135-168)
  • upsert_graph (170-227)
python-sdk/tests/test_integration.py (4)
python-sdk/exospherehost/runtime.py (4)
  • Runtime (45-424)
  • _register (163-198)
  • _worker (356-392)
  • _enqueue_call (200-219)
python-sdk/exospherehost/node/BaseNode.py (1)
  • BaseNode (6-100)
python-sdk/exospherehost/statemanager.py (4)
  • StateManager (42-227)
  • TriggerState (10-39)
  • upsert_graph (170-227)
  • trigger (70-133)
python-sdk/tests/test_runtime_comprehensive.py (17)
  • create_mock_aiohttp_session (11-42)
  • Inputs (46-47)
  • Inputs (60-61)
  • Inputs (75-76)
  • Inputs (91-92)
  • Outputs (49-50)
  • Outputs (63-64)
  • Outputs (78-79)
  • Outputs (94-95)
  • Secrets (52-53)
  • Secrets (66-67)
  • Secrets (81-82)
  • Secrets (97-98)
  • execute (55-56)
  • execute (69-71)
  • execute (84-87)
  • execute (100-101)
🪛 GitHub Actions: Ruff check on changed files only
python-sdk/tests/test_version.py

[error] 1-1: F401: pytest imported but unused.

python-sdk/tests/test_runtime_comprehensive.py

[error] 3-3: F401: os imported but unused.


[error] 568-568: F841: Local variable runtime assigned to but never used.

python-sdk/tests/test_package_init.py

[error] 103-103: F401: sys imported but unused.


[error] 110-110: F401: exospherehost imported but unused.

python-sdk/tests/test_base_node_comprehensive.py

[error] 3-3: F401: unittest.mock.patch imported but unused.


[error] 3-3: F401: unittest.mock.MagicMock imported but unused.

python-sdk/tests/test_statemanager_comprehensive.py

[error] 3-3: F401: os imported but unused.

python-sdk/tests/test_integration.py

[error] 3-3: F401: os imported but unused.

🔇 Additional comments (3)
python-sdk/tests/test_base_node_comprehensive.py (1)

1-99: General: comprehensive and well-structured BaseNode tests.

Good coverage across happy paths, list/none outputs, error paths, and edge cases.

python-sdk/tests/test_runtime_comprehensive.py (2)

125-184: LGTM! Comprehensive initialization and validation tests.

The TestRuntimeInitialization class provides thorough coverage of runtime initialization scenarios, including parameter validation, environment variable configuration, and error cases. The tests are well-structured and cover all the validation rules.


186-216: LGTM! Well-structured endpoint construction tests.

The TestRuntimeEndpointConstruction tests comprehensively verify that all endpoint URLs are correctly constructed with the proper namespace, version, and path components.

- Included `pytest-asyncio` in both `pyproject.toml` and `uv.lock` to support asynchronous testing.
- Updated the version specification for `pytest-asyncio` to `>=0.24.0` to ensure compatibility with existing test frameworks.
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: 12

♻️ Duplicate comments (23)
python-sdk/tests/test_version.py (4)

20-26: Avoid brittle identity check for strings; use equality only.

Object identity for strings is an implementation detail and can be brittle. Equality is sufficient.

 def test_version_consistency():
     """Test that version is consistent across imports."""
     from exospherehost._version import version as version1
     from exospherehost._version import version as version2
     
     assert version1 == version2
-    assert version1 is version2

10-17: Optionally validate PEP 440 compliance instead of just non-empty.

Strengthen this test by asserting the version string parses under packaging.version.Version.

 def test_version_format():
     """Test that version follows a reasonable format."""
-    # Version should be a string that could be a semantic version
-    assert version is not None
-    # Should not be empty
-    assert version.strip() != ""
-    # Should not contain only whitespace
-    assert not version.isspace()
+    from packaging.version import Version
+    # Non-empty and parses under PEP 440
+    assert isinstance(version, str)
+    assert version.strip() != ""
+    # Raises if invalid
+    Version(version)

29-34: Avoid redundancy with package-level version test or keep both consistent.

This duplicates assertions already covered in test_package_init.py. Either remove this block or leave it as-is; if keeping it, there’s no need to also assert identity in the other test.


34-34: Nit: ensure trailing newline at EOF.

Many tools expect a trailing newline; add one if missing.

python-sdk/tests/test_statemanager_comprehensive.py (2)

7-39: DRY the aiohttp session mock helper across tests.

create_mock_aiohttp_session is duplicated in multiple modules. Extract it to tests/conftest.py as a fixture (e.g., mock_aiohttp_session) and reuse.

Example fixture (tests/conftest.py):

import pytest
from unittest.mock import AsyncMock, MagicMock

@pytest.fixture
def mock_aiohttp_session():
    mock_session = AsyncMock()
    mock_post_response = AsyncMock()
    mock_get_response = AsyncMock()
    mock_put_response = AsyncMock()

    post_ctx = AsyncMock(); post_ctx.__aenter__.return_value = mock_post_response; post_ctx.__aexit__.return_value = None
    get_ctx = AsyncMock();  get_ctx.__aenter__.return_value = mock_get_response;  get_ctx.__aexit__.return_value = None
    put_ctx = AsyncMock();  put_ctx.__aenter__.return_value = mock_put_response;  put_ctx.__aexit__.return_value = None

    mock_session.post = MagicMock(return_value=post_ctx)
    mock_session.get = MagicMock(return_value=get_ctx)
    mock_session.put = MagicMock(return_value=put_ctx)
    mock_session.__aenter__.return_value = mock_session
    mock_session.__aexit__.return_value = None

    return mock_session, mock_post_response, mock_get_response, mock_put_response

Then replace local calls with the fixture argument and mock_session_class.return_value = mock_session.


170-187: Don’t use asyncio.run inside pytest tests; convert to async tests.

Mixing asyncio.run with pytest’s event loop is error-prone. Use @pytest.mark.asyncio and await the coroutine.

-    def test_trigger_validation_no_state_or_states(self, state_manager_config):
-        sm = StateManager(**state_manager_config)
-        
-        with pytest.raises(ValueError, match="Either state or states must be provided"):
-            asyncio.run(sm.trigger("test_graph"))
+    @pytest.mark.asyncio
+    async def test_trigger_validation_no_state_or_states(self, state_manager_config):
+        sm = StateManager(**state_manager_config)
+        with pytest.raises(ValueError, match="Either state or states must be provided"):
+            await sm.trigger("test_graph")

-    def test_trigger_validation_both_state_and_states(self, state_manager_config):
+    @pytest.mark.asyncio
+    async def test_trigger_validation_both_state_and_states(self, state_manager_config):
         sm = StateManager(**state_manager_config)
         state = TriggerState(identifier="test", inputs={"key": "value"})
         
         with pytest.raises(ValueError, match="Only one of state or states must be provided"):
-            asyncio.run(sm.trigger("test_graph", state=state, states=[state]))
+            await sm.trigger("test_graph", state=state, states=[state])

-    def test_trigger_validation_empty_states_list(self, state_manager_config):
+    @pytest.mark.asyncio
+    async def test_trigger_validation_empty_states_list(self, state_manager_config):
         sm = StateManager(**state_manager_config)
         
         with pytest.raises(ValueError, match="States must be a non-empty list"):
-            asyncio.run(sm.trigger("test_graph", states=[]))
+            await sm.trigger("test_graph", states=[])
python-sdk/tests/test_integration.py (6)

8-40: Consolidate duplicated aiohttp mocking helper into a shared fixture.

Same helper exists in multiple files. Move it to tests/conftest.py and reuse to reduce maintenance.


131-156: Patch the symbol where it’s used: exospherehost.runtime.ClientSession (not aiohttp.ClientSession).

Runtime imports ClientSession into its module scope; patch that symbol to avoid real network I/O.

-    async def test_runtime_worker_with_state_manager(self, mock_env_vars):
-        with patch('aiohttp.ClientSession') as mock_session:
+    async def test_runtime_worker_with_state_manager(self, mock_env_vars):
+        with patch('exospherehost.runtime.ClientSession') as mock_session:
             # Mock all HTTP responses
             mock_register_response = AsyncMock()
             mock_register_response.status = 200
             mock_register_response.json = AsyncMock(return_value={"status": "registered"})
@@
-            mock_session.return_value.__aenter__.return_value.post.return_value.__aenter__.return_value = mock_enqueue_response
-            mock_session.return_value.__aenter__.return_value.get.return_value.__aenter__.return_value = mock_secrets_response
+            # Worker path uses GET (secrets) and POST (notify executed)
+            mock_session.return_value.__aenter__.return_value.get.return_value.__aenter__.return_value = mock_secrets_response
+            mock_session.return_value.__aenter__.return_value.post.return_value.__aenter__.return_value = mock_executed_response

278-309: Same patching issue here; target runtime’s imported ClientSession.

Ensure tests don’t accidentally hit the network.

-        with patch('aiohttp.ClientSession') as mock_session:
+        with patch('exospherehost.runtime.ClientSession') as mock_session:

264-266: Tests rely on a runtime logging bug: _node_mapping keyed by state_id. Remove workaround and fix runtime logging.

_runtime.node_mapping is keyed by node class name, not state_id. Populating by state_id in tests masks a bug in Runtime._notify_executed/_notify_errored where logs index by state_id.

-            # Add state to node mapping
-            runtime._node_mapping[state["state_id"]] = MultiOutputNode

Proposed fix in runtime (outside this file) to log without indexing by state_id:

--- a/python-sdk/exospherehost/runtime.py
+++ b/python-sdk/exospherehost/runtime.py
@@
-                if response.status != 200:
-                    logger.error(f"Failed to notify executed state {state_id}: {res}")
-
-                logger.info(f"Notified executed state {state_id} with outputs: {outputs} for node {self._node_mapping[state_id].__name__}")
+                if response.status != 200:
+                    logger.error(f"Failed to notify executed state {state_id}: {res}")
+                logger.info(f"Notified executed state {state_id} with outputs: {outputs}")
@@
-                if response.status != 200:
-                    logger.error(f"Failed to notify errored state {state_id}: {res}")
-
-                logger.info(f"Notified errored state {state_id} with error: {error} for node {self._node_mapping[state_id].__name__}")
+                if response.status != 200:
+                    logger.error(f"Failed to notify errored state {state_id}: {res}")
+                logger.info(f"Notified errored state {state_id} with error: {error}")

I can open a follow-up PR to adjust runtime and then simplify these tests.


379-381: Same mapping-by-state_id issue; remove the workaround.

-            # Add state to node mapping
-            runtime._node_mapping[state["state_id"]] = IntegrationTestNode

503-506: Same mapping-by-state_id issue across multiple states; remove this block.

-            # Add states to node mapping
-            for state in states:
-                runtime._node_mapping[state["state_id"]] = IntegrationTestNode
python-sdk/tests/test_package_init.py (2)

23-25: Don’t over-constrain all; prefer subset check.

As the public surface evolves, forbidding additional exports is brittle. Assert that expected symbols are present.

-    # Check that __all__ doesn't contain unexpected exports
-    for export in __all__:
-        assert export in expected_exports, f"Unexpected export: {export}"
+    # Optional: avoid forbidding additional public exports
+    assert set(expected_exports).issubset(set(__all__)), "__all__ must include the expected exports"

44-46: Loosen the abstract instantiation error assertion.

Exact TypeError messages vary across Python versions. Assert the exception type only.

-    with pytest.raises(TypeError, match="Can't instantiate abstract class"):
+    with pytest.raises(TypeError):
         BaseNode()  # type: ignore
python-sdk/tests/test_runtime_comprehensive.py (8)

10-41: Extract duplicated aiohttp session mock helper to a shared test utility.

This helper is duplicated (see prior review). Centralize it (e.g., tests/utils/mock_http.py) and import where needed to reduce drift.

+# tests/utils/mock_http.py
+from unittest.mock import AsyncMock, MagicMock
+
+def create_mock_aiohttp_session():
+    """Helper function to create a properly mocked aiohttp session."""
+    mock_session = AsyncMock()
+    mock_post_response = AsyncMock()
+    mock_get_response = AsyncMock()
+    mock_put_response = AsyncMock()
+
+    mock_post_context = AsyncMock()
+    mock_post_context.__aenter__.return_value = mock_post_response
+    mock_post_context.__aexit__.return_value = None
+
+    mock_get_context = AsyncMock()
+    mock_get_context.__aenter__.return_value = mock_get_response
+    mock_get_context.__aexit__.return_value = None
+
+    mock_put_context = AsyncMock()
+    mock_put_context.__aenter__.return_value = mock_put_response
+    mock_put_context.__aexit__.return_value = None
+
+    mock_session.post = MagicMock(return_value=mock_post_context)
+    mock_session.get = MagicMock(return_value=mock_get_context)
+    mock_session.put = MagicMock(return_value=mock_put_context)
+
+    mock_session.__aenter__.return_value = mock_session
+    mock_session.__aexit__.return_value = None
+    return mock_session, mock_post_response, mock_get_response, mock_put_response

And update imports in tests:

-from ... import create_mock_aiohttp_session
+from tests.utils.mock_http import create_mock_aiohttp_session

59-71: Type count as int to avoid runtime casting and remove type-ignores.

Use proper types in the Inputs model and adjust the test payload accordingly.

 class MockTestNodeWithListOutput(BaseNode):
     class Inputs(BaseModel):
-        count: str
+        count: int
@@
     async def execute(self):
-        count = int(self.inputs.count) # type: ignore
+        count = self.inputs.count
         return [self.Outputs(numbers=str(i)) for i in range(count)]

And in the test state (line 338):

-                "inputs": {"count": "3"}
+                "inputs": {"count": 3}

74-87: Use a boolean flag for should_fail.

This is a boolean control; typing it as bool clarifies intent and removes string comparisons/type-ignores.

 class MockTestNodeWithError(BaseNode):
     class Inputs(BaseModel):
-        should_fail: str
+        should_fail: bool
@@
     async def execute(self):
-        if self.inputs.should_fail == "true": # type: ignore
+        if self.inputs.should_fail:
             raise ValueError("Test error")
         return self.Outputs(result="success")

Update test payloads at lines 405-406:

-                "inputs": {"should_fail": "true"}
+                "inputs": {"should_fail": True}

300-311: Replace sleep with deterministic queue join to avoid flakiness.

Relying on asyncio.sleep races tests. Wait for the queue to drain with join() then cancel the worker.

-            # Wait a bit for processing
-            await asyncio.sleep(0.1)
-
-            # Cancel the worker
-            worker_task.cancel()
+            # Wait deterministically for processing
+            await runtime._state_queue.join()
+            # Cancel the worker once the queue is empty
+            worker_task.cancel()
             try:
                 await worker_task
             except asyncio.CancelledError:
                 pass

343-349: Same flakiness here — prefer join() over sleep().

-            worker_task = asyncio.create_task(runtime._worker(1))
-            await asyncio.sleep(0.1)
-            worker_task.cancel()
+            worker_task = asyncio.create_task(runtime._worker(1))
+            await runtime._state_queue.join()
+            worker_task.cancel()
             try:
                 await worker_task
             except asyncio.CancelledError:
                 pass

378-384: Same pattern — use join() for reliable completion.

-            worker_task = asyncio.create_task(runtime._worker(1))
-            await asyncio.sleep(0.1)
-            worker_task.cancel()
+            worker_task = asyncio.create_task(runtime._worker(1))
+            await runtime._state_queue.join()
+            worker_task.cancel()
             try:
                 await worker_task
             except asyncio.CancelledError:
                 pass

410-416: Same fix — eliminate sleep() in favor of queue draining.

-            worker_task = asyncio.create_task(runtime._worker(1))
-            await asyncio.sleep(0.1)
-            worker_task.cancel()
+            worker_task = asyncio.create_task(runtime._worker(1))
+            await runtime._state_queue.join()
+            worker_task.cancel()
             try:
                 await worker_task
             except asyncio.CancelledError:
                 pass

574-607: Isolate global logging state with a fixture.

Current tests mutate the root logger without restoring it, potentially leaking state to other tests. Use a fixture to snapshot and restore handlers/level.

+import pytest
+import logging
+
+@pytest.fixture
+def clean_logger():
+    logger = logging.getLogger()
+    original_handlers = logger.handlers.copy()
+    original_level = logger.level
+    try:
+        yield logger
+    finally:
+        logger.handlers = original_handlers
+        logger.setLevel(original_level)

Use in tests:

-    def test_setup_default_logging_with_existing_handlers(self):
+    def test_setup_default_logging_with_existing_handlers(self, clean_logger):
-        logger = logging.getLogger()
+        logger = clean_logger
@@
-    def test_setup_default_logging_with_env_disable(self, monkeypatch):
+    def test_setup_default_logging_with_env_disable(self, monkeypatch, clean_logger):
@@
-    def test_setup_default_logging_with_custom_level(self, monkeypatch):
+    def test_setup_default_logging_with_custom_level(self, monkeypatch, clean_logger):
-        logger = logging.getLogger()
+        logger = clean_logger
python-sdk/tests/test_base_node_comprehensive.py (1)

306-316: Don’t reuse the same node instance across concurrent executions.

BaseNode stores inputs and secrets on the instance; concurrent calls on a shared instance race. Create a fresh instance per task.

-        node = ValidNode()
         inputs = ValidNode.Inputs(name="test", count="1")
         secrets = ValidNode.Secrets(api_key="key", token="token")
         
         # Run multiple concurrent executions
         tasks = [
-            node._execute(inputs, secrets) for _ in range(5) # type: ignore
+            ValidNode()._execute(inputs, secrets) for _ in range(5) # type: ignore
         ]
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1458925 and 09b7021.

📒 Files selected for processing (7)
  • python-sdk/exospherehost/_version.py (1 hunks)
  • python-sdk/tests/test_base_node_comprehensive.py (1 hunks)
  • python-sdk/tests/test_integration.py (1 hunks)
  • python-sdk/tests/test_package_init.py (1 hunks)
  • python-sdk/tests/test_runtime_comprehensive.py (1 hunks)
  • python-sdk/tests/test_statemanager_comprehensive.py (1 hunks)
  • python-sdk/tests/test_version.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
python-sdk/tests/test_statemanager_comprehensive.py (1)
python-sdk/exospherehost/statemanager.py (8)
  • StateManager (42-227)
  • TriggerState (10-39)
  • _get_trigger_state_endpoint (61-62)
  • _get_upsert_graph_endpoint (64-65)
  • _get_get_graph_endpoint (67-68)
  • trigger (70-133)
  • get_graph (135-168)
  • upsert_graph (170-227)
python-sdk/tests/test_package_init.py (3)
python-sdk/exospherehost/runtime.py (1)
  • Runtime (45-424)
python-sdk/exospherehost/node/BaseNode.py (1)
  • BaseNode (6-100)
python-sdk/exospherehost/statemanager.py (2)
  • StateManager (42-227)
  • TriggerState (10-39)
python-sdk/tests/test_version.py (1)
python-sdk/tests/test_package_init.py (1)
  • test_version_import (73-80)
python-sdk/tests/test_runtime_comprehensive.py (2)
python-sdk/exospherehost/runtime.py (13)
  • Runtime (45-424)
  • _setup_default_logging (14-42)
  • _get_enque_endpoint (133-137)
  • _get_executed_endpoint (139-143)
  • _get_errored_endpoint (145-149)
  • _get_register_endpoint (151-155)
  • _get_secrets_endpoint (157-161)
  • _register (163-198)
  • _enqueue_call (200-219)
  • _worker (356-392)
  • _notify_executed (240-259)
  • _notify_errored (261-280)
  • _get_secrets (282-297)
python-sdk/exospherehost/node/BaseNode.py (1)
  • BaseNode (6-100)
python-sdk/tests/test_integration.py (3)
python-sdk/exospherehost/runtime.py (4)
  • Runtime (45-424)
  • _register (163-198)
  • _worker (356-392)
  • _enqueue_call (200-219)
python-sdk/exospherehost/node/BaseNode.py (1)
  • BaseNode (6-100)
python-sdk/exospherehost/statemanager.py (4)
  • StateManager (42-227)
  • TriggerState (10-39)
  • upsert_graph (170-227)
  • trigger (70-133)
python-sdk/tests/test_base_node_comprehensive.py (1)
python-sdk/exospherehost/node/BaseNode.py (2)
  • BaseNode (6-100)
  • _execute (70-83)
🪛 GitHub Actions: Python SDK Tests
python-sdk/tests/test_base_node_comprehensive.py

[warning] 123-123: PytestUnknownMarkWarning: Unknown pytest.mark.asyncio - is this a typo? You can register custom marks to avoid this warning.


[warning] 137-137: PytestUnknownMarkWarning: Unknown pytest.mark.asyncio - is this a typo? You can register custom marks to avoid this warning.

🔇 Additional comments (1)
python-sdk/exospherehost/_version.py (1)

1-1: Version bump looks good.

The pre-release bump to 0.0.7b9 is PEP 440–compliant and aligns with the rest of the tests asserting VERSION consistency.

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 09b7021 and 5dec961.

⛔ Files ignored due to path filters (1)
  • python-sdk/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • python-sdk/pyproject.toml (1 hunks)
🔇 Additional comments (1)
python-sdk/pyproject.toml (1)

36-36: LGTM: Adding pytest-asyncio aligns with new asyncio-based tests

The dependency is appropriate given the async tests introduced in this PR and Python 3.12/3.13 support.

- Updated the integration tests in `test_integration.py` to use `exospherehost.runtime.ClientSession` for mocking HTTP responses, enhancing clarity and maintainability.
- Simplified the mocking of HTTP responses by consolidating the setup process, ensuring consistent behavior across tests.
- Fixed a bug in the runtime by adding `state_id` to the node mapping, improving the functionality of the state management system.
- Updated logging format in `runtime.py` to include the logger's name for better context in log messages.
- Changed info log statements to debug level for state manager URI and API key fallback, reducing log verbosity.
- Added pytest configuration options in `pyproject.toml` to specify asyncio mode and test paths, improving test execution setup.
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