pyromind-sdk 0.0.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ """
2
+ PyroMind Node SDK
3
+
4
+ A lightweight SDK stub for local development and testing of third-party nodes
5
+ without the full platform codebase (without `app.models.nodes`).
6
+
7
+ In the real platform runtime environment, nodes should prioritize importing
8
+ base classes from `app.models.nodes`.
9
+ """
10
+
11
+ __version__ = "0.1.0"
12
+
13
+ # Export YAML nodes functionality
14
+ from .nodes import (
15
+ load_nodes_from_yaml,
16
+ load_all_nodes_from_directory,
17
+ create_node_class_from_yaml,
18
+ convert_node_class_to_yaml,
19
+ yaml_to_node_class,
20
+ )
21
+
22
+ # Export API client functionality
23
+ from .client import (
24
+ PyroMindClient,
25
+ PyroMindAPIError,
26
+ SandboxesClient,
27
+ InstanceClient,
28
+ InferenceClient,
29
+ TrainingClient,
30
+ StorageClient,
31
+ PyroMindAPIClient,
32
+ )
33
+
34
+ # Export workflow functionality
35
+ from .client.workflow import (
36
+ WorkflowLiteConverter,
37
+ LayoutGenerator,
38
+ to_workflow_lite,
39
+ to_workflow_standard,
40
+ )
41
+
42
+ __all__ = [
43
+ "__version__",
44
+ # YAML nodes functionality
45
+ "load_nodes_from_yaml",
46
+ "load_all_nodes_from_directory",
47
+ "create_node_class_from_yaml",
48
+ "convert_node_class_to_yaml",
49
+ "yaml_to_node_class",
50
+ # API client functionality
51
+ "PyroMindClient",
52
+ "PyroMindAPIError",
53
+ "SandboxesClient",
54
+ "InstanceClient",
55
+ "InferenceClient",
56
+ "TrainingClient",
57
+ "StorageClient",
58
+ "PyroMindAPIClient",
59
+ # Workflow functionality
60
+ "WorkflowLiteConverter",
61
+ "LayoutGenerator",
62
+ "to_workflow_lite",
63
+ "to_workflow_standard",
64
+ ]
65
+
@@ -0,0 +1,30 @@
1
+ """
2
+ PyroMind API Client SDK
3
+
4
+ This module provides a Python client SDK for interacting with the PyroMind API v1.
5
+ """
6
+
7
+ from .base import PyroMindClient, PyroMindAPIError
8
+ from .sandboxes import SandboxesClient
9
+ from .instance import InstanceClient
10
+ from .inference import InferenceClient
11
+ from .training import TrainingClient
12
+ from .storage import StorageClient
13
+ from .client import PyroMindAPIClient
14
+ from .workflow import (
15
+ validate_workflow,
16
+ ValidationError
17
+ )
18
+
19
+ __all__ = [
20
+ "PyroMindClient",
21
+ "PyroMindAPIError",
22
+ "SandboxesClient",
23
+ "InstanceClient",
24
+ "InferenceClient",
25
+ "TrainingClient",
26
+ "StorageClient",
27
+ "PyroMindAPIClient",
28
+ "validate_workflow",
29
+ "ValidationError",
30
+ ]
@@ -0,0 +1,270 @@
1
+ """
2
+ Base client class for PyroMind API
3
+
4
+ This module provides the base client class that handles authentication,
5
+ HTTP requests, and error handling for all API clients.
6
+ """
7
+
8
+ import os
9
+ import requests
10
+ from typing import Optional, Dict, Any, Union
11
+ from requests.adapters import HTTPAdapter
12
+ from urllib3.util.retry import Retry
13
+
14
+
15
+ # Constants
16
+ DEFAULT_API_BASE_URL = "https://api.pyromind.ai/api/v1"
17
+ DEFAULT_TIMEOUT = 30
18
+ DEFAULT_MAX_RETRIES = 3
19
+ ENV_API_KEY = "PYROMIND_API_KEY"
20
+ ENV_BASE_URL = "PYROMIND_BASE_URL"
21
+ RETRY_STATUS_CODES = [429, 500, 502, 503, 504]
22
+ RETRY_ALLOWED_METHODS = ["GET", "POST", "PUT", "DELETE"]
23
+
24
+
25
+ class PyroMindAPIError(Exception):
26
+ """Base exception for PyroMind API errors"""
27
+ def __init__(self, message: str, status_code: Optional[int] = None, response: Optional[Dict] = None):
28
+ self.message = message
29
+ self.status_code = status_code
30
+ self.response = response
31
+ super().__init__(self.message)
32
+
33
+
34
+ class PyroMindClient:
35
+ """
36
+ Base client class for PyroMind API
37
+
38
+ Handles authentication, HTTP requests, and error handling.
39
+ All resource-specific clients inherit from this class.
40
+
41
+ Args:
42
+ api_key: Bearer token for API authentication. If not provided, will try to
43
+ read from PYROMIND_API_KEY environment variable. If neither is
44
+ provided, will raise ValueError.
45
+ base_url: Base URL for the API. If not provided, will try to read from
46
+ PYROMIND_BASE_URL environment variable. If neither is provided,
47
+ defaults to https://api.pyromind.ai/api/v1
48
+ timeout: Request timeout in seconds (default: 30)
49
+ max_retries: Maximum number of retries for failed requests (default: 3)
50
+
51
+ Raises:
52
+ ValueError: If api_key is not provided and PYROMIND_API_KEY environment
53
+ variable is not set.
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ api_key: Optional[str] = None,
59
+ base_url: Optional[str] = None,
60
+ timeout: int = DEFAULT_TIMEOUT,
61
+ max_retries: int = DEFAULT_MAX_RETRIES
62
+ ):
63
+ # Get API key from parameter or environment variable
64
+ if api_key is None:
65
+ api_key = os.getenv(ENV_API_KEY)
66
+
67
+ if not api_key:
68
+ raise ValueError(
69
+ f"API key is required. Please provide it either as a parameter "
70
+ f"or set the {ENV_API_KEY} environment variable."
71
+ )
72
+
73
+ # Strip whitespace from API key
74
+ api_key = api_key.strip()
75
+
76
+ # Get base URL from parameter, environment variable, or use default
77
+ if base_url is None:
78
+ base_url = os.getenv(ENV_BASE_URL, DEFAULT_API_BASE_URL)
79
+
80
+ if not base_url:
81
+ raise ValueError(
82
+ f"Base URL is required. Please provide it either as a parameter "
83
+ f"or set the {ENV_BASE_URL} environment variable."
84
+ )
85
+ self.api_key = api_key
86
+ self.base_url = base_url.rstrip('/')
87
+ self.timeout = timeout
88
+
89
+ # Create session with retry strategy
90
+ self.session = requests.Session()
91
+ retry_strategy = Retry(
92
+ total=max_retries,
93
+ backoff_factor=1,
94
+ status_forcelist=RETRY_STATUS_CODES,
95
+ allowed_methods=RETRY_ALLOWED_METHODS
96
+ )
97
+ adapter = HTTPAdapter(max_retries=retry_strategy)
98
+ self.session.mount("http://", adapter)
99
+ self.session.mount("https://", adapter)
100
+
101
+ # Set default headers
102
+ self.session.headers.update({
103
+ "Authorization": f"Bearer {api_key}",
104
+ "Content-Type": "application/json",
105
+ "Accept": "application/json"
106
+ })
107
+
108
+ def _extract_data(self, response: Dict[str, Any]) -> Any:
109
+ """
110
+ Extract data from API response
111
+
112
+ API responses are wrapped in {success: True, data: {...}} format.
113
+ This method extracts the actual data from the response.
114
+
115
+ Args:
116
+ response: API response dictionary
117
+
118
+ Returns:
119
+ Extracted data from response
120
+ """
121
+ if isinstance(response, dict):
122
+ if "data" in response:
123
+ return response["data"]
124
+ # If response doesn't have 'data' field, return the whole response
125
+ # (for backward compatibility)
126
+ return response
127
+ return response
128
+
129
+ def _request(
130
+ self,
131
+ method: str,
132
+ endpoint: str,
133
+ params: Optional[Dict[str, Any]] = None,
134
+ json_data: Optional[Dict[str, Any]] = None,
135
+ **kwargs
136
+ ) -> Dict[str, Any]:
137
+ """
138
+ Make an HTTP request to the API
139
+
140
+ Args:
141
+ method: HTTP method (GET, POST, PUT, DELETE)
142
+ endpoint: API endpoint (relative to base_url)
143
+ params: Query parameters
144
+ json_data: JSON request body
145
+ **kwargs: Additional arguments to pass to requests
146
+
147
+ Returns:
148
+ Response JSON data as dictionary
149
+
150
+ Raises:
151
+ PyroMindAPIError: If the request fails
152
+ """
153
+ url = f"{self.base_url}/{endpoint.lstrip('/')}"
154
+
155
+ try:
156
+ response = self.session.request(
157
+ method=method,
158
+ url=url,
159
+ params=params,
160
+ json=json_data,
161
+ timeout=self.timeout,
162
+ **kwargs
163
+ )
164
+
165
+ # Handle non-2xx responses
166
+ if not response.ok:
167
+ error_data = None
168
+ try:
169
+ error_data = response.json()
170
+ except:
171
+ error_data = {"message": response.text}
172
+
173
+ # Provide more detailed error messages for specific status codes
174
+ if response.status_code == 400:
175
+ error_message = (
176
+ "Bad Request (400). The request was invalid or malformed. "
177
+ "This usually means there's an issue with the request parameters, "
178
+ "request body format, or missing required fields."
179
+ ) + f"\nResponse: {response.text}"
180
+ elif response.status_code == 401:
181
+ error_message = (
182
+ "Authentication failed (401). Please check your API key. "
183
+ "The API key may be invalid, expired, or incorrectly formatted."
184
+ )
185
+ if error_data.get("message"):
186
+ error_message += f" Server message: {error_data.get('message')}"
187
+ elif response.status_code == 404:
188
+ error_message = "Resource not found (404). The requested resource does not exist."
189
+ # Try to extract detailed error information from the response
190
+ if isinstance(error_data, dict):
191
+ # Check for nested error structure (APIResponse format)
192
+ if "error" in error_data:
193
+ error_obj = error_data.get("error", {})
194
+ if isinstance(error_obj, dict):
195
+ error_code = error_obj.get("code", "")
196
+ error_msg = error_obj.get("message", "")
197
+ if error_code:
198
+ error_message = f"{error_code}: {error_msg}"
199
+ elif error_msg:
200
+ error_message = error_msg
201
+ elif "detail" in error_data:
202
+ detail = error_data.get("detail")
203
+ if isinstance(detail, dict) and "error" in detail:
204
+ error_obj = detail.get("error", {})
205
+ if isinstance(error_obj, dict):
206
+ error_code = error_obj.get("code", "")
207
+ error_msg = error_obj.get("message", "")
208
+ if error_code:
209
+ error_message = f"{error_code}: {error_msg}"
210
+ elif error_msg:
211
+ error_message = error_msg
212
+ elif isinstance(detail, str):
213
+ error_message = detail
214
+ elif error_data.get("message"):
215
+ error_message += f" Details: {error_data.get('message')}"
216
+ elif response.status_code == 422:
217
+ error_message = "Unprocessable Entity (422). The request was well-formed but contains semantic errors."
218
+ if error_data.get("message"):
219
+ error_message += f" Details: {error_data.get('message')}"
220
+ if error_data.get("detail"):
221
+ error_message += f"\nValidation details: {error_data.get('detail')}"
222
+ else:
223
+ error_message = error_data.get("message", f"API request failed with status {response.status_code}")
224
+ if error_data.get("detail"):
225
+ error_message += f"\nDetails: {error_data.get('detail')}"
226
+
227
+ raise PyroMindAPIError(
228
+ message=error_message,
229
+ status_code=response.status_code,
230
+ response=error_data
231
+ )
232
+
233
+ # Return JSON response
234
+ if response.content:
235
+ return response.json()
236
+ return {}
237
+
238
+ except requests.exceptions.RequestException as e:
239
+ raise PyroMindAPIError(
240
+ message=f"Request failed: {str(e)}",
241
+ status_code=None
242
+ )
243
+
244
+ def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs) -> Dict[str, Any]:
245
+ """Make a GET request"""
246
+ return self._request("GET", endpoint, params=params, **kwargs)
247
+
248
+ def post(self, endpoint: str, json_data: Optional[Dict[str, Any]] = None, **kwargs) -> Dict[str, Any]:
249
+ """Make a POST request"""
250
+ return self._request("POST", endpoint, json_data=json_data, **kwargs)
251
+
252
+ def put(self, endpoint: str, json_data: Optional[Dict[str, Any]] = None, **kwargs) -> Dict[str, Any]:
253
+ """Make a PUT request"""
254
+ return self._request("PUT", endpoint, json_data=json_data, **kwargs)
255
+
256
+ def delete(self, endpoint: str, **kwargs) -> Dict[str, Any]:
257
+ """Make a DELETE request"""
258
+ return self._request("DELETE", endpoint, **kwargs)
259
+
260
+ def close(self):
261
+ """Close the session"""
262
+ self.session.close()
263
+
264
+ def __enter__(self):
265
+ """Context manager entry"""
266
+ return self
267
+
268
+ def __exit__(self, exc_type, exc_val, exc_tb):
269
+ """Context manager exit"""
270
+ self.close()
@@ -0,0 +1,138 @@
1
+ """
2
+ Main PyroMind API Client
3
+
4
+ This module provides the main client class that integrates all resource clients.
5
+ """
6
+
7
+ import os
8
+ from typing import Optional
9
+ from .base import PyroMindClient
10
+ from .sandboxes import SandboxesClient
11
+ from .instance import InstanceClient
12
+ from .inference import InferenceClient
13
+ from .training import TrainingClient
14
+
15
+
16
+ class PyroMindAPIClient:
17
+ """
18
+ Main PyroMind API Client
19
+
20
+ This class provides a unified interface to all PyroMind API resources.
21
+ It integrates all resource-specific clients (Sandboxes, Instance, Inference, Training).
22
+
23
+ Args:
24
+ api_key: Bearer token for API authentication. If not provided, will try to
25
+ read from PYROMIND_API_KEY environment variable. If neither is
26
+ provided, will raise ValueError.
27
+ base_url: Base URL for the API. If not provided, will try to read from
28
+ PYROMIND_BASE_URL environment variable. If neither is provided,
29
+ defaults to https://api.pyromind.ai/api/v1
30
+ timeout: Request timeout in seconds (default: 30)
31
+ max_retries: Maximum number of retries for failed requests (default: 3)
32
+
33
+ Raises:
34
+ ValueError: If api_key is not provided and PYROMIND_API_KEY environment
35
+ variable is not set.
36
+
37
+ Example:
38
+ ```python
39
+ from pyromind_sdk import PyroMindAPIClient
40
+
41
+ client = PyroMindAPIClient(api_key="your-api-key")
42
+
43
+ # List all sandboxes
44
+ sandboxes = client.sandboxes.list()
45
+
46
+ # Create a Jupyter instance
47
+ from pyromind_sdk.client.models import JupyterRequest, ResourceConfig
48
+ jupyter = client.instance.create(
49
+ JupyterRequest(
50
+ name="my-jupyter",
51
+ image="jupyter/scipy-notebook:latest",
52
+ resources=ResourceConfig(cpu="2", memory="4Gi")
53
+ )
54
+ )
55
+ ```
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ api_key: Optional[str] = None,
61
+ base_url: Optional[str] = None,
62
+ timeout: int = 30,
63
+ max_retries: int = 3
64
+ ):
65
+ """
66
+ Initialize the PyroMind API Client
67
+
68
+ Args:
69
+ api_key: Bearer token for API authentication. If not provided, will try to
70
+ read from PYROMIND_API_KEY environment variable.
71
+ base_url: Base URL for the API. If not provided, will try to read from
72
+ PYROMIND_BASE_URL environment variable. If neither is provided,
73
+ defaults to https://api.pyromind.ai/api/v1
74
+ timeout: Request timeout in seconds
75
+ max_retries: Maximum number of retries for failed requests
76
+ """
77
+ # Get API key from parameter or environment variable
78
+ if api_key is None:
79
+ api_key = os.getenv("PYROMIND_API_KEY")
80
+
81
+ if not api_key:
82
+ raise ValueError(
83
+ "API key is required. Please provide it either as a parameter "
84
+ "or set the PYROMIND_API_KEY environment variable."
85
+ )
86
+
87
+ # Get base URL from parameter, environment variable, or use default
88
+ if base_url is None:
89
+ base_url = os.getenv("PYROMIND_BASE_URL", "https://api.pre2-pyromind.ai/api/v1")
90
+
91
+ self._base_client = PyroMindClient(
92
+ api_key=api_key,
93
+ base_url=base_url,
94
+ timeout=timeout,
95
+ max_retries=max_retries
96
+ )
97
+
98
+ # Initialize resource clients
99
+ self.sandboxes = SandboxesClient(
100
+ api_key=api_key,
101
+ base_url=base_url,
102
+ timeout=timeout,
103
+ max_retries=max_retries
104
+ )
105
+ self.instance = InstanceClient(
106
+ api_key=api_key,
107
+ base_url=base_url,
108
+ timeout=timeout,
109
+ max_retries=max_retries
110
+ )
111
+ self.inference = InferenceClient(
112
+ api_key=api_key,
113
+ base_url=base_url,
114
+ timeout=timeout,
115
+ max_retries=max_retries
116
+ )
117
+ self.training = TrainingClient(
118
+ api_key=api_key,
119
+ base_url=base_url,
120
+ timeout=timeout,
121
+ max_retries=max_retries
122
+ )
123
+
124
+ def close(self):
125
+ """Close all client sessions"""
126
+ self._base_client.close()
127
+ self.sandboxes.close()
128
+ self.instance.close()
129
+ self.inference.close()
130
+ self.training.close()
131
+
132
+ def __enter__(self):
133
+ """Context manager entry"""
134
+ return self
135
+
136
+ def __exit__(self, exc_type, exc_val, exc_tb):
137
+ """Context manager exit"""
138
+ self.close()
@@ -0,0 +1,135 @@
1
+ """
2
+ Inference API Client
3
+
4
+ This module provides a client for managing inference jobs via the PyroMind API.
5
+ """
6
+
7
+ from typing import List
8
+ from .base import PyroMindClient
9
+ from .models import (
10
+ InferenceJobRequest,
11
+ InferenceJobResponse,
12
+ )
13
+
14
+
15
+ class InferenceClient(PyroMindClient):
16
+ """
17
+ Client for managing inference jobs
18
+
19
+ Provides methods for creating, listing, getting, and deleting inference jobs.
20
+ """
21
+
22
+ def list(self) -> List[InferenceJobResponse]:
23
+ """
24
+ List all inference jobs
25
+
26
+ Returns:
27
+ List of InferenceJobResponse objects
28
+ """
29
+ response = self.get("/inference")
30
+ # API returns {success: True, data: {...}} format
31
+ data = self._extract_data(response)
32
+
33
+ # Handle different possible data structures
34
+ if isinstance(data, dict) and "inference_jobs" in data:
35
+ jobs_data = data["inference_jobs"]
36
+ elif isinstance(data, dict) and "pagination" in data:
37
+ # Response format: {inference_jobs: [...], pagination: {...}}
38
+ jobs_data = data.get("inference_jobs", [])
39
+ elif isinstance(data, list):
40
+ jobs_data = data
41
+ else:
42
+ jobs_data = []
43
+
44
+ # Convert each job data to InferenceJobResponse
45
+ return [InferenceJobResponse(**job) if isinstance(job, dict) else job for job in jobs_data]
46
+
47
+ def create(self, request: InferenceJobRequest) -> str:
48
+ """
49
+ Create a new inference job
50
+
51
+ Args:
52
+ request: InferenceJobCreateRequest with job configuration
53
+
54
+ Returns:
55
+ Job ID string
56
+ """
57
+ response = self.post("/inference", json_data=request.model_dump())
58
+ # API returns {success: True, data: {...}} format
59
+ data = self._extract_data(response)
60
+
61
+ # Backend returns either {job_id: "..."} or full job object
62
+ if isinstance(data, dict) and "job_id" in data:
63
+ return data["job_id"]
64
+ elif isinstance(data, dict) and "id" in data:
65
+ return data["id"]
66
+ else:
67
+ # Try to parse as InferenceJobResponse and extract ID
68
+ job_response = InferenceJobResponse(**data)
69
+ return job_response.id
70
+
71
+ def get_job(self, job_id: str) -> InferenceJobResponse:
72
+ """
73
+ Get a specific inference job by ID
74
+
75
+ Args:
76
+ job_id: ID of the inference job to retrieve
77
+
78
+ Returns:
79
+ InferenceJobResponse object
80
+ """
81
+ response = self.get(f"/inference/{job_id}")
82
+ # API returns {success: True, data: {...}} format
83
+ data = self._extract_data(response)
84
+
85
+ # Backend returns the job data directly in the data field
86
+ return InferenceJobResponse(**data)
87
+
88
+ def update(self, job_id: str, request) -> InferenceJobResponse:
89
+ """
90
+ Update an inference job
91
+
92
+ Args:
93
+ job_id: ID of the inference job to update
94
+ request: InferenceJobRequest with updated configuration
95
+
96
+ Returns:
97
+ InferenceJobResponse object
98
+ """
99
+ # Import here to avoid circular dependency
100
+ from .models import InferenceJobRequest
101
+ if not isinstance(request, InferenceJobRequest):
102
+ request = InferenceJobRequest(**request)
103
+
104
+ request_dict = request.model_dump(exclude_none=True)
105
+
106
+ response = self.put(f"/inference/{job_id}", json_data=request_dict)
107
+ # API returns {success: True, data: {...}} format
108
+ data = self._extract_data(response)
109
+
110
+ return InferenceJobResponse(**data)
111
+
112
+ def delete(self, job_id: str) -> None:
113
+ """
114
+ Delete an inference job
115
+
116
+ Args:
117
+ job_id: ID of the inference job to delete
118
+ """
119
+ self._request("DELETE", f"/inference/{job_id}")
120
+
121
+ def pause(self, job_id: str) -> InferenceJobResponse:
122
+ """
123
+ Pause an inference job
124
+
125
+ Args:
126
+ job_id: ID of the inference job to pause
127
+
128
+ Returns:
129
+ InferenceJobResponse object
130
+ """
131
+ response = self.post(f"/inference/{job_id}/pause")
132
+ # API returns {success: True, data: {...}} format
133
+ data = self._extract_data(response)
134
+
135
+ return InferenceJobResponse(**data)