Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion exosphere-runtimes/cloud-storage-runtime/main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
from dotenv import load_dotenv
from exospherehost import Runtime
from nodes.list_s3_files import ListS3FilesNode
from nodes.download_s3_file import DownloadS3FileNode

# Load environment variables from .env file
# EXOSPHERE_STATE_MANAGER_URI is the URI of the state manager
# EXOSPHERE_API_KEY is the key of the runtime
load_dotenv()

# Note on node ordering:
# The order of node classes in the `nodes` list does not define execution sequence.
# Nodes are registered with the state manager; orchestration and dependencies are handled externally.
# `ListS3FilesNode` is listed before `DownloadS3FileNode` for readability only.
Runtime(
name="cloud-storage-runtime",
namespace="exospherehost",
nodes=[ListS3FilesNode]
nodes=[ListS3FilesNode, DownloadS3FileNode]
).start()
33 changes: 33 additions & 0 deletions exosphere-runtimes/cloud-storage-runtime/nodes/download_s3_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import boto3
from exospherehost import BaseNode
from pydantic import BaseModel


class DownloadS3FileNode(BaseNode):

class Inputs(BaseModel):
bucket_name: str
key: str

class Outputs(BaseModel):
file_path: str

class Secrets(BaseModel):
aws_access_key_id: str
aws_secret_access_key: str
aws_region: str

async def execute(self) -> Outputs:

s3_client = boto3.client(
's3',
aws_access_key_id=self.secrets.aws_access_key_id,
aws_secret_access_key=self.secrets.aws_secret_access_key,
region_name=self.secrets.aws_region
)

file_name = self.inputs.key.split('/')[-1]

s3_client.download_file(self.inputs.bucket_name, self.inputs.key, file_name)

return self.Outputs(file_path=self.outputs.file_path)
44 changes: 30 additions & 14 deletions python-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ pip install exospherehost

## Quick Start

> Important: In v1, all fields in `Inputs`, `Outputs`, and `Secrets` must be strings. If you need to pass complex data (e.g., JSON), serialize the data to a string first, then parse that string within your node.

### Basic Node Creation

Create a simple node that processes data:
Expand All @@ -34,24 +36,24 @@ from pydantic import BaseModel
class SampleNode(BaseNode):
class Inputs(BaseModel):
name: str
data: dict
data: str # v1: strings only

class Outputs(BaseModel):
message: str
processed_data: dict
processed_data: str # v1: strings only

async def execute(self) -> Outputs:
print(f"Processing data for: {self.inputs.name}")
# Your processing logic here
processed_data = {"status": "completed", "input": self.inputs.data}
# Your processing logic here; serialize complex data to strings (e.g., JSON)
processed_data = f"completed:{self.inputs.data}"
return self.Outputs(
message="success",
message="success",
processed_data=processed_data
)

# Initialize the runtime
Runtime(
namespace="MyProject",
namespace="MyProject",
name="DataProcessor",
nodes=[SampleNode]
).start()
Expand All @@ -71,6 +73,7 @@ export EXOSPHERE_API_KEY="your-api-key"
- **Distributed Execution**: Run nodes across multiple compute resources
- **State Management**: Automatic state persistence and recovery
- **Type Safety**: Full Pydantic integration for input/output validation
- **String-only data model (v1)**: All `Inputs`, `Outputs`, and `Secrets` fields are strings. Serialize non-string data (e.g., JSON) as needed.
- **Async Support**: Native async/await support for high-performance operations
- **Error Handling**: Built-in retry mechanisms and error recovery
- **Scalability**: Designed for high-volume batch processing and workflows
Expand Down Expand Up @@ -103,15 +106,16 @@ Nodes are the building blocks of your workflows. Each node:
class ConfigurableNode(BaseNode):
class Inputs(BaseModel):
text: str
max_length: int = 100
max_length: str = "100" # v1: strings only

class Outputs(BaseModel):
result: str
length: int
length: str # v1: strings only

async def execute(self) -> Outputs:
result = self.inputs.text[:self.inputs.max_length]
return self.Outputs(result=result, length=len(result))
max_length = int(self.inputs.max_length)
result = self.inputs.text[:max_length]
return self.Outputs(result=result, length=str(len(result)))
```

### Error Handling
Expand All @@ -122,7 +126,7 @@ class RobustNode(BaseNode):
data: str

class Outputs(BaseModel):
success: bool
success: str
result: str

async def execute(self) -> Outputs:
Expand All @@ -137,14 +141,15 @@ Secrets allow you to securely manage sensitive configuration data like API keys,
```python
from exospherehost import Runtime, BaseNode
from pydantic import BaseModel
import json

class APINode(BaseNode):
class Inputs(BaseModel):
user_id: str
query: str

class Outputs(BaseModel):
response: dict
response: str # v1: strings only
status: str

class Secrets(BaseModel):
Expand All @@ -159,14 +164,24 @@ class APINode(BaseNode):
# Use secrets for API calls
import httpx
async with httpx.AsyncClient() as client:
response = await client.post(
http_response = await client.post(
f"{self.secrets.api_endpoint}/process",
headers=headers,
json={"user_id": self.inputs.user_id, "query": self.inputs.query}
)

# Serialize body: prefer JSON if valid; fallback to text or empty string
response_text = http_response.text or ""
if response_text:
try:
response_str = json.dumps(http_response.json())
except Exception:
response_str = response_text
else:
response_str = ""

return self.Outputs(
response=response.json(),
response=response_str,
status="success"
)
```
Expand All @@ -175,6 +190,7 @@ class APINode(BaseNode):

- **Security**: Secrets are stored securely by the ExosphereHost Runtime and are never exposed in logs or error messages
- **Validation**: The `Secrets` class uses Pydantic for automatic validation of secret values
- **String-only (v1)**: All `Secrets` fields must be strings.
- **Access**: Secrets are available via `self.secrets` during node execution
- **Types**: Common secret types include API keys, database credentials, encryption keys, and authentication tokens
- **Injection**: Secrets are injected by the Runtime at execution time, so you don't need to handle them manually
Expand Down
2 changes: 1 addition & 1 deletion python-sdk/exospherehost/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version = "0.0.7b4"
version = "0.0.7b5"
3 changes: 1 addition & 2 deletions python-sdk/exospherehost/node/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,5 @@
"""

from .BaseNode import BaseNode
from .status import Status

__all__ = ["BaseNode", "Status"]
__all__ = ["BaseNode"]
44 changes: 0 additions & 44 deletions python-sdk/exospherehost/node/status.py

This file was deleted.

9 changes: 9 additions & 0 deletions python-sdk/exospherehost/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,15 @@ def _validate_nodes(self):
errors.append(f"{node.__name__} does not have an Secrets class")
if not issubclass(node.Secrets, BaseModel):
errors.append(f"{node.__name__} does not have an Secrets class that inherits from pydantic.BaseModel")

# check all data objects are strings
for field_name, field_info in node.Inputs.model_fields.items():
if field_info.annotation is not str:
errors.append(f"{node.__name__}.Inputs field '{field_name}' must be of type str, got {field_info.annotation}")

for field_name, field_info in node.Outputs.model_fields.items():
if field_info.annotation is not str:
errors.append(f"{node.__name__}.Outputs field '{field_name}' must be of type str, got {field_info.annotation}")

for field_name, field_info in node.Secrets.model_fields.items():
if field_info.annotation is not str:
Expand Down
19 changes: 0 additions & 19 deletions python-sdk/sample.py

This file was deleted.

1 change: 0 additions & 1 deletion state-manager/app/models/node_template_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@ class NodeTemplate(BaseModel):
namespace: str = Field(..., description="Namespace of the node")
identifier: str = Field(..., description="Identifier of the node")
inputs: dict[str, Any] = Field(..., description="Inputs of the node")
store: dict[str, Any] = Field(..., description="Upsert data to store object for the node")
next_nodes: Optional[List[str]] = Field(None, description="Next nodes to execute")