From de3df2ee0606b5916feaa85a3b5f6d1af09a8a7a Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sun, 3 Aug 2025 23:33:22 +0530 Subject: [PATCH 1/4] added docstring --- python-sdk/exospherehost/__init__.py | 32 +++++ python-sdk/exospherehost/_version.py | 2 +- python-sdk/exospherehost/node/BaseNode.py | 72 ++++++++++- python-sdk/exospherehost/node/__init__.py | 16 +++ python-sdk/exospherehost/node/status.py | 24 ++++ python-sdk/exospherehost/runtime.py | 149 +++++++++++++++++++++- python-sdk/pyproject.toml | 1 + python-sdk/sample.py | 17 ++- python-sdk/uv.lock | 84 +++++++++++- 9 files changed, 386 insertions(+), 11 deletions(-) diff --git a/python-sdk/exospherehost/__init__.py b/python-sdk/exospherehost/__init__.py index 28057c69..f9048159 100644 --- a/python-sdk/exospherehost/__init__.py +++ b/python-sdk/exospherehost/__init__.py @@ -1,3 +1,35 @@ +""" +ExosphereHost Python SDK + +A distributed workflow execution framework for building scalable, stateful applications. + +This package provides the core components for creating and executing distributed +workflows using a node-based architecture. The main components are: + +- Runtime: Manages the execution environment and coordinates with the state manager +- BaseNode: Abstract base class for creating executable nodes +- Status constants: Define the various states in the workflow lifecycle + +Example usage: + from exospherehost import Runtime, BaseNode + + # Create a custom node + class MyNode(BaseNode): + class Inputs(BaseModel): + data: str + + class Outputs(BaseModel): + result: str + + async def execute(self, inputs: Inputs) -> Outputs: + return Outputs(result=f"Processed: {inputs.data}") + + # Create and start runtime + runtime = Runtime(namespace="my-namespace", name="my-runtime") + runtime.connect([MyNode()]) + runtime.start() +""" + from ._version import version as __version__ from .runtime import Runtime from .node.BaseNode import BaseNode diff --git a/python-sdk/exospherehost/_version.py b/python-sdk/exospherehost/_version.py index 49f60684..cca38914 100644 --- a/python-sdk/exospherehost/_version.py +++ b/python-sdk/exospherehost/_version.py @@ -1 +1 @@ -version = "0.0.5b" +version = "0.0.6b" diff --git a/python-sdk/exospherehost/node/BaseNode.py b/python-sdk/exospherehost/node/BaseNode.py index 3d6129d5..48281dd2 100644 --- a/python-sdk/exospherehost/node/BaseNode.py +++ b/python-sdk/exospherehost/node/BaseNode.py @@ -1,17 +1,85 @@ from abc import ABC, abstractmethod -from typing import Optional, Any, List +from typing import Any, Optional, List +from pydantic import BaseModel class BaseNode(ABC): + """ + Abstract base class for all nodes in the exospherehost system. + + BaseNode provides the foundation for creating executable nodes that can be + connected to a Runtime for distributed processing. Each node must implement + the execute method and can optionally define Inputs and Outputs models. + + Attributes: + unique_name (Optional[str]): A unique identifier for this node instance. + If None, the class name will be used as the unique name. + state (dict[str, Any]): A dictionary for storing node state between executions. + """ def __init__(self, unique_name: Optional[str] = None): + """ + Initialize a BaseNode instance. + + Args: + unique_name (Optional[str], optional): A unique identifier for this node. + If None, the class name will be used as the unique name. Defaults to None. + """ self.unique_name: Optional[str] = unique_name + self.state: dict[str, Any] = {} + + class Inputs(BaseModel): + """ + Pydantic model for defining the input schema of a node. + + Subclasses should override this class to define the expected input structure. + This ensures type safety and validation of inputs before execution. + """ + pass + + class Outputs(BaseModel): + """ + Pydantic model for defining the output schema of a node. + + Subclasses should override this class to define the expected output structure. + This ensures type safety and validation of outputs after execution. + """ + pass @abstractmethod - async def execute(self, inputs: dict[str, Any]) -> dict[str, Any] | List[dict[str, Any]]: + async def execute(self, inputs: Inputs) -> Outputs | List[Outputs]: + """ + Execute the node's main logic. + + This is the core method that must be implemented by all concrete node classes. + It receives inputs, processes them according to the node's logic, and returns + outputs. The method can return either a single Outputs instance or a list + of Outputs instances for batch processing. + + Args: + inputs (Inputs): The input data for this execution, validated against + the Inputs model defined by the node. + + Returns: + Outputs | List[Outputs]: The output data from this execution. Can be + a single Outputs instance or a list of Outputs instances. + + Raises: + Exception: Any exception that occurs during execution will be caught + by the Runtime and reported as an error state. + """ pass def get_unique_name(self) -> str: + """ + Get the unique name for this node instance. + + Returns the unique_name if it was provided during initialization, + otherwise returns the class name. + + Returns: + str: The unique identifier for this node instance + """ if self.unique_name is not None: return self.unique_name return self.__class__.__name__ \ No newline at end of file diff --git a/python-sdk/exospherehost/node/__init__.py b/python-sdk/exospherehost/node/__init__.py index e69de29b..378965a3 100644 --- a/python-sdk/exospherehost/node/__init__.py +++ b/python-sdk/exospherehost/node/__init__.py @@ -0,0 +1,16 @@ +""" +Node module for the exospherehost package. + +This module contains the core node-related components for building executable +workflow nodes. The main component is BaseNode, which provides the foundation +for creating custom nodes that can be executed by the Runtime. + +Components: +- BaseNode: Abstract base class for all executable nodes +- Status constants: Define the various states in workflow execution +""" + +from .BaseNode import BaseNode +from .status import * + +__all__ = ["BaseNode"] diff --git a/python-sdk/exospherehost/node/status.py b/python-sdk/exospherehost/node/status.py index 66aa8462..fcc83b12 100644 --- a/python-sdk/exospherehost/node/status.py +++ b/python-sdk/exospherehost/node/status.py @@ -1,9 +1,33 @@ +""" +Status constants for state management in the exospherehost system. + +These constants represent the various states that a workflow state can be in +during its lifecycle from creation to completion or failure. +""" + +# State has been created but not yet queued for execution CREATED = 'CREATED' + +# State has been queued and is waiting to be picked up by a worker QUEUED = 'QUEUED' + +# State has been successfully executed by a worker EXECUTED = 'EXECUTED' + +# Next state in the workflow has been created based on successful execution NEXT_CREATED = 'NEXT_CREATED' + +# A retry state has been created due to a previous failure RETRY_CREATED = 'RETRY_CREATED' + +# State execution has timed out TIMEDOUT = 'TIMEDOUT' + +# State execution has failed with an error ERRORED = 'ERRORED' + +# State execution has been cancelled CANCELLED = 'CANCELLED' + +# State has completed successfully (final state) SUCCESS = 'SUCCESS' \ No newline at end of file diff --git a/python-sdk/exospherehost/runtime.py b/python-sdk/exospherehost/runtime.py index ec014a2a..b34ec7ff 100644 --- a/python-sdk/exospherehost/runtime.py +++ b/python-sdk/exospherehost/runtime.py @@ -1,4 +1,5 @@ import asyncio +import os from asyncio import Queue, sleep from typing import Any, List from .node.BaseNode import BaseNode @@ -8,8 +9,50 @@ logger = getLogger(__name__) class Runtime: + """ + A runtime environment for executing nodes connected to exospherehost. + + The Runtime class manages the execution of BaseNode instances in a distributed + environment. It handles state management, worker coordination, and communication + with the state manager service. + + Attributes: + _name (str): The name of this runtime instance + _namespace (str): The namespace this runtime operates in + _key (str): API key for authentication with the state manager + _batch_size (int): Number of states to process in each batch + _connected (bool): Whether the runtime is connected to nodes + _state_queue (Queue): Queue for managing state processing + _workers (int): Number of worker tasks to spawn + _nodes (List[BaseNode]): List of connected node instances + _node_names (List[str]): List of node unique names + _state_manager_uri (str): URI of the state manager service + _state_manager_version (str): Version of the state manager API + _poll_interval (int): Interval between polling operations in seconds + _node_mapping (dict): Mapping of node names to node instances + """ - def __init__(self, namespace: str, state_manager_uri: str, key: str, batch_size: int = 16, workers=4, state_manage_version: str = "v0", poll_interval: int = 1): + def __init__(self, namespace: str, name: str, state_manager_uri: str | None = None, key: str | None = None, batch_size: int = 16, workers=4, state_manage_version: str = "v0", poll_interval: int = 1): + """ + Initialize the Runtime instance. + + Args: + namespace (str): The namespace this runtime operates in + name (str): The name of this runtime instance + state_manager_uri (str | None, optional): URI of the state manager service. + If None, will be read from EXOSPHERE_STATE_MANAGER_URI environment variable. + key (str | None, optional): API key for authentication. + If None, will be read from EXOSPHERE_API_KEY environment variable. + batch_size (int, optional): Number of states to process in each batch. Defaults to 16. + workers (int, optional): Number of worker tasks to spawn. Defaults to 4. + state_manage_version (str, optional): Version of the state manager API. Defaults to "v0". + poll_interval (int, optional): Interval between polling operations in seconds. Defaults to 1. + + Raises: + ValueError: If batch_size or workers is less than 1, or if required + configuration (state_manager_uri, key) is not provided. + """ + self._name = name self._namespace = namespace self._key = key self._batch_size = batch_size @@ -23,27 +66,70 @@ def __init__(self, namespace: str, state_manager_uri: str, key: str, batch_size: self._poll_interval = poll_interval self._node_mapping = {} - if batch_size < 1: + self._set_config_from_env() + self._validate_runtime() + + def _set_config_from_env(self): + """Set configuration from environment variables if not provided.""" + if self._state_manager_uri is None: + self._state_manager_uri = os.environ.get("EXOSPHERE_STATE_MANAGER_URI") + if self._key is None: + self._key = os.environ.get("EXOSPHERE_API_KEY") + + def _validate_runtime(self): + """ + Validate runtime configuration. + + Raises: + ValueError: If batch_size or workers is less than 1, or if required + configuration (state_manager_uri, key) is not provided. + """ + if self._batch_size < 1: raise ValueError("Batch size should be at least 1") - if workers < 1: + if self._workers < 1: raise ValueError("Workers should be at least 1") + if self._state_manager_uri is None: + raise ValueError("State manager URI is not set") + if self._key is None: + raise ValueError("API key is not set") def _get_enque_endpoint(self): + """Get the endpoint URL for enqueueing states.""" return f"{self._state_manager_uri}/{str(self._state_manager_version)}/namespace/{self._namespace}/states/enqueue" def _get_executed_endpoint(self, state_id: str): + """Get the endpoint URL for notifying executed states.""" return f"{self._state_manager_uri}/{str(self._state_manager_version)}/namespace/{self._namespace}/states/{state_id}/executed" def _get_errored_endpoint(self, state_id: str): + """Get the endpoint URL for notifying errored states.""" return f"{self._state_manager_uri}/{str(self._state_manager_version)}/namespace/{self._namespace}/states/{state_id}/errored" def connect(self, nodes: List[BaseNode]): + """ + Connect nodes to the runtime. + + This method validates and registers the provided nodes with the runtime. + The nodes will be available for execution when the runtime starts. + + Args: + nodes (List[BaseNode]): List of BaseNode instances to connect + + Raises: + ValueError: If any node does not inherit from BaseNode + """ self._nodes = self._validate_nodes(nodes) self._node_names = [node.get_unique_name() for node in nodes] self._node_mapping = {node.get_unique_name(): node for node in self._nodes} self._connected = True async def _enqueue_call(self): + """ + Make an API call to enqueue states from the state manager. + + Returns: + dict: Response from the state manager containing states to process + """ async with ClientSession() as session: endpoint = self._get_enque_endpoint() body = {"nodes": self._node_names, "batch_size": self._batch_size} @@ -58,6 +144,12 @@ async def _enqueue_call(self): return res async def _enqueue(self): + """ + Continuously enqueue states from the state manager. + + This method runs in a loop, polling the state manager for new states + to process and adding them to the internal queue. + """ while True: try: if self._state_queue.qsize() < self._batch_size: @@ -70,6 +162,13 @@ async def _enqueue(self): await sleep(self._poll_interval) async def _notify_executed(self, state_id: str, outputs: List[dict[str, Any]]): + """ + Notify the state manager that a state has been executed successfully. + + Args: + state_id (str): The ID of the executed state + outputs (List[dict[str, Any]]): The outputs from the node execution + """ async with ClientSession() as session: endpoint = self._get_executed_endpoint(state_id) body = {"outputs": outputs} @@ -82,6 +181,13 @@ async def _notify_executed(self, state_id: str, outputs: List[dict[str, Any]]): logger.error(f"Failed to notify executed state {state_id}: {res}") async def _notify_errored(self, state_id: str, error: str): + """ + Notify the state manager that a state has encountered an error. + + Args: + state_id (str): The ID of the errored state + error (str): The error message + """ async with ClientSession() as session: endpoint = self._get_errored_endpoint(state_id) body = {"error": error} @@ -94,6 +200,18 @@ async def _notify_errored(self, state_id: str, error: str): logger.error(f"Failed to notify errored state {state_id}: {res}") def _validate_nodes(self, nodes: List[BaseNode]): + """ + Validate that all nodes inherit from BaseNode. + + Args: + nodes (List[BaseNode]): List of nodes to validate + + Returns: + List[BaseNode]: The validated list of nodes + + Raises: + ValueError: If any node does not inherit from BaseNode + """ invalid_nodes = [] for node in nodes: @@ -106,6 +224,12 @@ def _validate_nodes(self, nodes: List[BaseNode]): return nodes async def _worker(self): + """ + Worker task that processes states from the queue. + + This method runs in a loop, taking states from the queue and executing + the corresponding node. It handles both successful execution and errors. + """ while True: state = await self._state_queue.get() @@ -127,6 +251,15 @@ async def _worker(self): self._state_queue.task_done() # type: ignore async def _start(self): + """ + Start the runtime execution. + + This method starts the enqueue polling task and spawns worker tasks + to process states from the queue. + + Raises: + RuntimeError: If the runtime is not connected (no nodes registered) + """ if not self._connected: raise RuntimeError("Runtime not connected, you need to call Runtime.connect() before calling Runtime.start()") @@ -136,6 +269,16 @@ async def _start(self): await asyncio.gather(poller, *worker_tasks) def start(self): + """ + Start the runtime execution. + + This method starts the runtime in the current event loop or creates + a new one if none exists. It returns a task that can be awaited + or runs the runtime until completion. + + Returns: + asyncio.Task: The runtime task if running in an existing event loop + """ try: loop = asyncio.get_running_loop() return loop.create_task(self._start()) diff --git a/python-sdk/pyproject.toml b/python-sdk/pyproject.toml index 36fce89e..d6beb429 100644 --- a/python-sdk/pyproject.toml +++ b/python-sdk/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.12" dynamic = ["version"] dependencies = [ "aiohttp>=3.12.15", + "pydantic>=2.11.7", ] classifiers = [ "Development Status :: 4 - Beta", diff --git a/python-sdk/sample.py b/python-sdk/sample.py index ffa4765b..2ce39b4a 100644 --- a/python-sdk/sample.py +++ b/python-sdk/sample.py @@ -1,13 +1,22 @@ from exospherehost import Runtime, BaseNode -from typing import Any +from pydantic import BaseModel import os class SampleNode(BaseNode): - async def execute(self, inputs: dict[str, Any]) -> dict[str, Any]: + class Inputs(BaseModel): + name: str + + class Outputs(BaseModel): + message: str + + async def execute(self, inputs: Inputs) -> Outputs: print(inputs) - return {"message": "success"} + return self.Outputs(message="success") -runtime = Runtime("SampleNamespace", os.getenv("EXOSPHERE_STATE_MANAGER_URI", "http://localhost:8000"), os.getenv("EXOSPHERE_API_KEY", "")) +runtime = Runtime( + namespace="SampleNamespace", + name="SampleNode" +) runtime.connect([SampleNode()]) runtime.start() \ No newline at end of file diff --git a/python-sdk/uv.lock b/python-sdk/uv.lock index bbe33c67..e74326e4 100644 --- a/python-sdk/uv.lock +++ b/python-sdk/uv.lock @@ -75,6 +75,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + [[package]] name = "attrs" version = "25.3.0" @@ -89,6 +98,7 @@ name = "exospherehost" source = { virtual = "." } dependencies = [ { name = "aiohttp" }, + { name = "pydantic" }, ] [package.dev-dependencies] @@ -97,7 +107,10 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "aiohttp", specifier = ">=3.12.15" }] +requires-dist = [ + { name = "aiohttp", specifier = ">=3.12.15" }, + { name = "pydantic", specifier = ">=2.11.7" }, +] [package.metadata.requires-dev] dev = [{ name = "ruff", specifier = ">=0.12.5" }] @@ -291,6 +304,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, ] +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + [[package]] name = "ruff" version = "0.12.5" @@ -325,6 +395,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + [[package]] name = "yarl" version = "1.20.1" From 686347a7298a0afc206781f8aa46f9b7e7cb6429 Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sun, 3 Aug 2025 23:39:52 +0530 Subject: [PATCH 2/4] added Output schema --- python-sdk/exospherehost/runtime.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/python-sdk/exospherehost/runtime.py b/python-sdk/exospherehost/runtime.py index b34ec7ff..4d654213 100644 --- a/python-sdk/exospherehost/runtime.py +++ b/python-sdk/exospherehost/runtime.py @@ -32,7 +32,7 @@ class Runtime: _node_mapping (dict): Mapping of node names to node instances """ - def __init__(self, namespace: str, name: str, state_manager_uri: str | None = None, key: str | None = None, batch_size: int = 16, workers=4, state_manage_version: str = "v0", poll_interval: int = 1): + def __init__(self, namespace: str, name: str, state_manager_uri: str | None = None, key: str | None = None, batch_size: int = 16, workers: int = 4, state_manage_version: str = "v0", poll_interval: int = 1): """ Initialize the Runtime instance. @@ -135,7 +135,7 @@ async def _enqueue_call(self): body = {"nodes": self._node_names, "batch_size": self._batch_size} headers = {"x-api-key": self._key} - async with session.post(endpoint, json=body, headers=headers) as response: + async with session.post(endpoint, json=body, headers=headers) as response: # type: ignore res = await response.json() if response.status != 200: @@ -161,20 +161,20 @@ async def _enqueue(self): await sleep(self._poll_interval) - async def _notify_executed(self, state_id: str, outputs: List[dict[str, Any]]): + async def _notify_executed(self, state_id: str, outputs: List[BaseNode.Outputs]): """ Notify the state manager that a state has been executed successfully. Args: state_id (str): The ID of the executed state - outputs (List[dict[str, Any]]): The outputs from the node execution + outputs (List[BaseNode.Outputs]): The outputs from the node execution """ async with ClientSession() as session: endpoint = self._get_executed_endpoint(state_id) - body = {"outputs": outputs} + body = {"outputs": [output.model_dump() for output in outputs]} headers = {"x-api-key": self._key} - async with session.post(endpoint, json=body, headers=headers) as response: + async with session.post(endpoint, json=body, headers=headers) as response: # type: ignore res = await response.json() if response.status != 200: @@ -193,7 +193,7 @@ async def _notify_errored(self, state_id: str, error: str): body = {"error": error} headers = {"x-api-key": self._key} - async with session.post(endpoint, json=body, headers=headers) as response: + async with session.post(endpoint, json=body, headers=headers) as response: # type: ignore res = await response.json() if response.status != 200: @@ -240,7 +240,7 @@ async def _worker(self): if outputs is None: outputs = [] - if isinstance(outputs, dict): + if isinstance(outputs, BaseNode.Outputs): outputs = [outputs] await self._notify_executed(state["state_id"], outputs) From 03388a9cb11b1aaeb80bdfcdba2e1a14dedd4119 Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sun, 3 Aug 2025 23:53:35 +0530 Subject: [PATCH 3/4] resolved comments by @coderabbitai --- python-sdk/README.md | 19 ++++++-- python-sdk/exospherehost/__init__.py | 28 ++++++----- python-sdk/exospherehost/_version.py | 2 +- python-sdk/exospherehost/node/status.py | 63 +++++++++++++++---------- python-sdk/sample.py | 1 - 5 files changed, 68 insertions(+), 45 deletions(-) diff --git a/python-sdk/README.md b/python-sdk/README.md index fc2a0097..74f94e42 100644 --- a/python-sdk/README.md +++ b/python-sdk/README.md @@ -6,15 +6,24 @@ You can simply connect to exosphere state manager and start creating your nodes, ```python from exospherehost import Runtime, BaseNode -from typing import Any -import os +from pydantic import BaseModel class SampleNode(BaseNode): - async def execute(self, inputs: dict[str, Any]) -> dict[str, Any]: + class Inputs(BaseModel): + name: str + + class Outputs(BaseModel): + message: str + + async def execute(self, inputs: Inputs) -> Outputs: print(inputs) - return {"message": "success"} + return self.Outputs(message="success") -runtime = Runtime("SampleNamespace", os.getenv("EXOSPHERE_STATE_MANAGER_URI", "http://localhost:8000"), os.getenv("EXOSPHERE_API_KEY", "")) +# EXOSPHERE_STATE_MANAGER_URI and EXOSPHERE_API_KEY are required to be set in the environment variables for authentication with exospherehost +runtime = Runtime( + namespace="SampleNamespace", + name="SampleNode" +) runtime.connect([SampleNode()]) runtime.start() diff --git a/python-sdk/exospherehost/__init__.py b/python-sdk/exospherehost/__init__.py index f9048159..412aa05a 100644 --- a/python-sdk/exospherehost/__init__.py +++ b/python-sdk/exospherehost/__init__.py @@ -12,21 +12,25 @@ Example usage: from exospherehost import Runtime, BaseNode - - # Create a custom node - class MyNode(BaseNode): + from pydantic import BaseModel + + class SampleNode(BaseNode): class Inputs(BaseModel): - data: str - + name: str + class Outputs(BaseModel): - result: str - + message: str + async def execute(self, inputs: Inputs) -> Outputs: - return Outputs(result=f"Processed: {inputs.data}") - - # Create and start runtime - runtime = Runtime(namespace="my-namespace", name="my-runtime") - runtime.connect([MyNode()]) + print(inputs) + return self.Outputs(message="success") + + runtime = Runtime( + namespace="SampleNamespace", + name="SampleNode" + ) + + runtime.connect([SampleNode()]) runtime.start() """ diff --git a/python-sdk/exospherehost/_version.py b/python-sdk/exospherehost/_version.py index cca38914..32eade92 100644 --- a/python-sdk/exospherehost/_version.py +++ b/python-sdk/exospherehost/_version.py @@ -1 +1 @@ -version = "0.0.6b" +version = "0.0.6b0" diff --git a/python-sdk/exospherehost/node/status.py b/python-sdk/exospherehost/node/status.py index fcc83b12..5a416d2b 100644 --- a/python-sdk/exospherehost/node/status.py +++ b/python-sdk/exospherehost/node/status.py @@ -5,29 +5,40 @@ during its lifecycle from creation to completion or failure. """ -# State has been created but not yet queued for execution -CREATED = 'CREATED' - -# State has been queued and is waiting to be picked up by a worker -QUEUED = 'QUEUED' - -# State has been successfully executed by a worker -EXECUTED = 'EXECUTED' - -# Next state in the workflow has been created based on successful execution -NEXT_CREATED = 'NEXT_CREATED' - -# A retry state has been created due to a previous failure -RETRY_CREATED = 'RETRY_CREATED' - -# State execution has timed out -TIMEDOUT = 'TIMEDOUT' - -# State execution has failed with an error -ERRORED = 'ERRORED' - -# State execution has been cancelled -CANCELLED = 'CANCELLED' - -# State has completed successfully (final state) -SUCCESS = 'SUCCESS' \ No newline at end of file +from enum import Enum + + +class Status(str, Enum): + """ + Enumeration of workflow state status values. + + This enum provides type-safe constants for the various states that a workflow + state can be in during its lifecycle from creation to completion or failure. + """ + + # State has been created but not yet queued for execution + CREATED = 'CREATED' + + # State has been queued and is waiting to be picked up by a worker + QUEUED = 'QUEUED' + + # State has been successfully executed by a worker + EXECUTED = 'EXECUTED' + + # Next state in the workflow has been created based on successful execution + NEXT_CREATED = 'NEXT_CREATED' + + # A retry state has been created due to a previous failure + RETRY_CREATED = 'RETRY_CREATED' + + # State execution has timed out + TIMEDOUT = 'TIMEDOUT' + + # State execution has failed with an error + ERRORED = 'ERRORED' + + # State execution has been cancelled + CANCELLED = 'CANCELLED' + + # State has completed successfully (final state) + SUCCESS = 'SUCCESS' \ No newline at end of file diff --git a/python-sdk/sample.py b/python-sdk/sample.py index 2ce39b4a..4ef9f6ab 100644 --- a/python-sdk/sample.py +++ b/python-sdk/sample.py @@ -1,6 +1,5 @@ from exospherehost import Runtime, BaseNode from pydantic import BaseModel -import os class SampleNode(BaseNode): class Inputs(BaseModel): From 44a0c8aa7ce7161e8afc213a129b8b54364c708c Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sun, 3 Aug 2025 23:54:53 +0530 Subject: [PATCH 4/4] fixed ruff errors --- python-sdk/exospherehost/node/__init__.py | 4 ++-- python-sdk/exospherehost/runtime.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python-sdk/exospherehost/node/__init__.py b/python-sdk/exospherehost/node/__init__.py index 378965a3..a00cc39f 100644 --- a/python-sdk/exospherehost/node/__init__.py +++ b/python-sdk/exospherehost/node/__init__.py @@ -11,6 +11,6 @@ """ from .BaseNode import BaseNode -from .status import * +from .status import Status -__all__ = ["BaseNode"] +__all__ = ["BaseNode", "Status"] diff --git a/python-sdk/exospherehost/runtime.py b/python-sdk/exospherehost/runtime.py index 4d654213..9495cb91 100644 --- a/python-sdk/exospherehost/runtime.py +++ b/python-sdk/exospherehost/runtime.py @@ -1,7 +1,7 @@ import asyncio import os from asyncio import Queue, sleep -from typing import Any, List +from typing import List from .node.BaseNode import BaseNode from aiohttp import ClientSession from logging import getLogger