tetra-rp 0.5.5__tar.gz → 0.7.0__tar.gz

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.
Files changed (45) hide show
  1. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/PKG-INFO +1 -1
  2. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/pyproject.toml +1 -1
  3. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/__init__.py +2 -0
  4. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/client.py +2 -0
  5. tetra_rp-0.7.0/src/tetra_rp/core/api/__init__.py +6 -0
  6. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/api/runpod.py +84 -3
  7. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/resources/__init__.py +2 -0
  8. tetra_rp-0.7.0/src/tetra_rp/core/resources/constants.py +4 -0
  9. tetra_rp-0.7.0/src/tetra_rp/core/resources/network_volume.py +107 -0
  10. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/resources/serverless.py +42 -14
  11. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp.egg-info/PKG-INFO +1 -1
  12. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp.egg-info/SOURCES.txt +2 -0
  13. tetra_rp-0.5.5/src/tetra_rp/core/api/__init__.py +0 -5
  14. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/README.md +0 -0
  15. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/setup.cfg +0 -0
  16. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/__init__.py +0 -0
  17. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/pool/__init__.py +0 -0
  18. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/pool/cluster_manager.py +0 -0
  19. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/pool/dataclass.py +0 -0
  20. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/pool/ex.py +0 -0
  21. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/pool/job.py +0 -0
  22. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/pool/worker.py +0 -0
  23. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/resources/base.py +0 -0
  24. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/resources/cloud.py +0 -0
  25. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/resources/cpu.py +0 -0
  26. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/resources/environment.py +0 -0
  27. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/resources/gpu.py +0 -0
  28. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/resources/live_serverless.py +0 -0
  29. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/resources/resource_manager.py +0 -0
  30. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/resources/template.py +0 -0
  31. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/resources/utils.py +0 -0
  32. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/utils/__init__.py +0 -0
  33. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/utils/backoff.py +0 -0
  34. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/utils/json.py +0 -0
  35. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/core/utils/singleton.py +0 -0
  36. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/logger.py +0 -0
  37. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/protos/__init__.py +0 -0
  38. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/protos/remote_execution.py +0 -0
  39. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/stubs/__init__.py +0 -0
  40. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/stubs/live_serverless.py +0 -0
  41. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/stubs/registry.py +0 -0
  42. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp/stubs/serverless.py +0 -0
  43. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp.egg-info/dependency_links.txt +0 -0
  44. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp.egg-info/requires.txt +0 -0
  45. {tetra_rp-0.5.5 → tetra_rp-0.7.0}/src/tetra_rp.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tetra_rp
3
- Version: 0.5.5
3
+ Version: 0.7.0
4
4
  Summary: A Python library for distributed inference and serving of machine learning models
5
5
  Author-email: Marut Pandya <pandyamarut@gmail.com>, Patrick Rachford <prachford@icloud.com>, Dean Quinanola <dean.quinanola@runpod.io>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tetra_rp"
3
- version = "0.5.5"
3
+ version = "0.7.0"
4
4
  description = "A Python library for distributed inference and serving of machine learning models"
5
5
  authors = [
6
6
  { name = "Marut Pandya", email = "pandyamarut@gmail.com" },
@@ -20,6 +20,7 @@ from .core.resources import ( # noqa: E402
20
20
  ResourceManager,
21
21
  ServerlessEndpoint,
22
22
  runpod,
23
+ NetworkVolume,
23
24
  )
24
25
 
25
26
 
@@ -34,4 +35,5 @@ __all__ = [
34
35
  "ResourceManager",
35
36
  "ServerlessEndpoint",
36
37
  "runpod",
38
+ "NetworkVolume",
37
39
  ]
@@ -24,6 +24,8 @@ def remote(
24
24
  to be provisioned or used.
25
25
  dependencies (List[str], optional): A list of pip package names to be installed in the remote
26
26
  environment before executing the function. Defaults to None.
27
+ mount_volume (NetworkVolume, optional): Configuration for creating and mounting a network volume.
28
+ Should contain 'size', 'datacenter_id', and 'name' keys. Defaults to None.
27
29
  extra (dict, optional): Additional parameters for the execution of the resource. Defaults to an empty dict.
28
30
 
29
31
  Returns:
@@ -0,0 +1,6 @@
1
+ from .runpod import RunpodGraphQLClient, RunpodRestClient
2
+
3
+ __all__ = [
4
+ "RunpodGraphQLClient",
5
+ "RunpodRestClient",
6
+ ]
@@ -3,15 +3,17 @@ Direct GraphQL communication with Runpod API.
3
3
  Bypasses the outdated runpod-python SDK limitations.
4
4
  """
5
5
 
6
- import os
7
6
  import json
8
- import aiohttp
9
- from typing import Dict, Any, Optional
10
7
  import logging
8
+ import os
9
+ from typing import Any, Dict, Optional
10
+
11
+ import aiohttp
11
12
 
12
13
  log = logging.getLogger(__name__)
13
14
 
14
15
  RUNPOD_API_BASE_URL = os.environ.get("RUNPOD_API_BASE_URL", "https://api.runpod.io")
16
+ RUNPOD_REST_API_URL = os.environ.get("RUNPOD_REST_API_URL", "https://rest.runpod.io/v1")
15
17
 
16
18
 
17
19
  class RunpodGraphQLClient:
@@ -210,3 +212,82 @@ class RunpodGraphQLClient:
210
212
 
211
213
  async def __aexit__(self, exc_type, exc_val, exc_tb):
212
214
  await self.close()
215
+
216
+
217
+ class RunpodRestClient:
218
+ """
219
+ Runpod REST client for Runpod API.
220
+ Provides methods to interact with Runpod's REST endpoints.
221
+ """
222
+
223
+ def __init__(self, api_key: Optional[str] = None):
224
+ self.api_key = api_key or os.getenv("RUNPOD_API_KEY")
225
+ if not self.api_key:
226
+ raise ValueError("Runpod API key is required")
227
+
228
+ self.session: Optional[aiohttp.ClientSession] = None
229
+
230
+ async def _get_session(self) -> aiohttp.ClientSession:
231
+ """Get or create an aiohttp session."""
232
+ if self.session is None or self.session.closed:
233
+ timeout = aiohttp.ClientTimeout(total=300) # 5 minute timeout
234
+ self.session = aiohttp.ClientSession(
235
+ timeout=timeout,
236
+ headers={
237
+ "Authorization": f"Bearer {self.api_key}",
238
+ "Content-Type": "application/json",
239
+ },
240
+ )
241
+ return self.session
242
+
243
+ async def _execute_rest(
244
+ self, method: str, url: str, data: Optional[Dict[str, Any]] = None
245
+ ) -> Dict[str, Any]:
246
+ """Execute a REST API request."""
247
+ session = await self._get_session()
248
+
249
+ log.debug(f"REST Request: {method} {url}")
250
+ log.debug(f"REST Data: {json.dumps(data, indent=2) if data else 'None'}")
251
+
252
+ try:
253
+ async with session.request(method, url, json=data) as response:
254
+ response_data = await response.json()
255
+
256
+ log.debug(f"REST Response Status: {response.status}")
257
+ log.debug(f"REST Response: {json.dumps(response_data, indent=2)}")
258
+
259
+ if response.status >= 400:
260
+ raise Exception(
261
+ f"REST request failed: {response.status} - {response_data}"
262
+ )
263
+
264
+ return response_data
265
+
266
+ except aiohttp.ClientError as e:
267
+ log.error(f"HTTP client error: {e}")
268
+ raise Exception(f"HTTP request failed: {e}")
269
+
270
+ async def create_network_volume(self, payload: Dict[str, Any]) -> Dict[str, Any]:
271
+ """Create a network volume in Runpod."""
272
+ log.debug(f"Creating network volume: {payload.get('name', 'unnamed')}")
273
+
274
+ result = await self._execute_rest(
275
+ "POST", f"{RUNPOD_REST_API_URL}/networkvolumes", payload
276
+ )
277
+
278
+ log.info(
279
+ f"Created network volume: {result.get('id', 'unknown')} - {result.get('name', 'unnamed')}"
280
+ )
281
+
282
+ return result
283
+
284
+ async def close(self):
285
+ """Close the HTTP session."""
286
+ if self.session and not self.session.closed:
287
+ await self.session.close()
288
+
289
+ async def __aenter__(self):
290
+ return self
291
+
292
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
293
+ await self.close()
@@ -12,6 +12,7 @@ from .serverless import (
12
12
  CudaVersion,
13
13
  )
14
14
  from .template import PodTemplate
15
+ from .network_volume import NetworkVolume
15
16
 
16
17
 
17
18
  __all__ = [
@@ -30,4 +31,5 @@ __all__ = [
30
31
  "ServerlessResource",
31
32
  "ServerlessEndpoint",
32
33
  "PodTemplate",
34
+ "NetworkVolume",
33
35
  ]
@@ -0,0 +1,4 @@
1
+ import os
2
+
3
+ CONSOLE_BASE_URL = os.environ.get("CONSOLE_BASE_URL", "https://console.runpod.io")
4
+ CONSOLE_URL = f"{CONSOLE_BASE_URL}/serverless/user/endpoint/%s"
@@ -0,0 +1,107 @@
1
+ import logging
2
+ from enum import Enum
3
+ from typing import Optional
4
+
5
+ from pydantic import (
6
+ Field,
7
+ field_serializer,
8
+ )
9
+
10
+ from ..api.runpod import RunpodRestClient
11
+ from .base import DeployableResource
12
+ from .constants import CONSOLE_BASE_URL
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ class DataCenter(str, Enum):
18
+ """
19
+ Enum representing available data centers for network volumes.
20
+ #TODO: Add more data centers as needed. Lock this to the available data center.
21
+ """
22
+
23
+ EU_RO_1 = "EU-RO-1"
24
+
25
+
26
+ class NetworkVolume(DeployableResource):
27
+ """
28
+ NetworkVolume resource for creating and managing Runpod netowrk volumes.
29
+
30
+ This class handles the creation, deployment, and management of network volumes
31
+ that can be attached to serverless resources.
32
+
33
+ """
34
+
35
+ # Internal fixed value
36
+ dataCenterId: DataCenter = Field(default=DataCenter.EU_RO_1, frozen=True)
37
+
38
+ id: Optional[str] = Field(default=None)
39
+ name: Optional[str] = None
40
+ size: Optional[int] = Field(default=10, gt=0) # Size in GB
41
+
42
+ def __str__(self) -> str:
43
+ return f"{self.__class__.__name__}:{self.id}"
44
+
45
+ @field_serializer("dataCenterId")
46
+ def serialize_data_center_id(self, value: Optional[DataCenter]) -> Optional[str]:
47
+ """Convert DataCenter enum to string."""
48
+ return value.value if value is not None else None
49
+
50
+ @property
51
+ def is_created(self) -> bool:
52
+ "Returns True if the network volume already exists."
53
+ return self.id is not None
54
+
55
+ @property
56
+ def url(self) -> str:
57
+ """
58
+ Returns the URL for the network volume resource.
59
+ """
60
+ if not self.id:
61
+ raise ValueError("Network volume ID is not set")
62
+ return f"{CONSOLE_BASE_URL}/user/storage"
63
+
64
+ async def create_network_volume(self) -> str:
65
+ """
66
+ Creates a network volume using the provided configuration.
67
+ Returns the volume ID.
68
+ """
69
+ async with RunpodRestClient() as client:
70
+ # Create the network volume
71
+ payload = self.model_dump(exclude_none=True)
72
+ result = await client.create_network_volume(payload)
73
+
74
+ if volume := self.__class__(**result):
75
+ return volume
76
+
77
+ def is_deployed(self) -> bool:
78
+ """
79
+ Checks if the network volume resource is deployed and available.
80
+ """
81
+ return self.id is not None
82
+
83
+ async def deploy(self) -> "DeployableResource":
84
+ """
85
+ Deploys the network volume resource using the provided configuration.
86
+ Returns a DeployableResource object.
87
+ """
88
+ try:
89
+ # If the resource is already deployed, return it
90
+ if self.is_deployed():
91
+ log.debug(f"{self} exists")
92
+ return self
93
+
94
+ # Create the network volume
95
+ async with RunpodRestClient() as client:
96
+ # Create the network volume
97
+ payload = self.model_dump(exclude_none=True)
98
+ result = await client.create_network_volume(payload)
99
+
100
+ if volume := self.__class__(**result):
101
+ return volume
102
+
103
+ raise ValueError("Deployment failed, no volume was created.")
104
+
105
+ except Exception as e:
106
+ log.error(f"{self} failed to deploy: {e}")
107
+ raise
@@ -1,27 +1,27 @@
1
1
  import asyncio
2
2
  import logging
3
- import os
4
- from typing import Any, Dict, List, Optional
5
3
  from enum import Enum
4
+ from typing import Any, Dict, List, Optional
5
+
6
6
  from pydantic import (
7
+ BaseModel,
8
+ Field,
7
9
  field_serializer,
8
10
  field_validator,
9
11
  model_validator,
10
- BaseModel,
11
- Field,
12
12
  )
13
-
14
13
  from runpod.endpoint.runner import Job
15
14
 
16
15
  from ..api.runpod import RunpodGraphQLClient
17
16
  from ..utils.backoff import get_backoff_delay
18
-
19
- from .cloud import runpod
20
17
  from .base import DeployableResource
21
- from .template import PodTemplate, KeyValuePair
22
- from .gpu import GpuGroup
18
+ from .cloud import runpod
19
+ from .constants import CONSOLE_URL
23
20
  from .cpu import CpuInstanceType
24
21
  from .environment import EnvironmentVars
22
+ from .gpu import GpuGroup
23
+ from .network_volume import NetworkVolume
24
+ from .template import KeyValuePair, PodTemplate
25
25
 
26
26
 
27
27
  # Environment variables are loaded from the .env file
@@ -39,10 +39,6 @@ def get_env_vars() -> Dict[str, str]:
39
39
  log = logging.getLogger(__name__)
40
40
 
41
41
 
42
- CONSOLE_BASE_URL = os.environ.get("CONSOLE_BASE_URL", "https://console.runpod.io")
43
- CONSOLE_URL = f"{CONSOLE_BASE_URL}/serverless/user/endpoint/%s"
44
-
45
-
46
42
  class ServerlessScalerType(Enum):
47
43
  QUEUE_DELAY = "QUEUE_DELAY"
48
44
  REQUEST_COUNT = "REQUEST_COUNT"
@@ -66,7 +62,15 @@ class ServerlessResource(DeployableResource):
66
62
  Base class for GPU serverless resource
67
63
  """
68
64
 
69
- _input_only = {"id", "cudaVersions", "env", "gpus", "flashboot", "imageName"}
65
+ _input_only = {
66
+ "id",
67
+ "cudaVersions",
68
+ "env",
69
+ "gpus",
70
+ "flashboot",
71
+ "imageName",
72
+ "networkVolume",
73
+ }
70
74
 
71
75
  # === Input-only Fields ===
72
76
  cudaVersions: Optional[List[CudaVersion]] = [] # for allowedCudaVersions
@@ -75,6 +79,8 @@ class ServerlessResource(DeployableResource):
75
79
  gpus: Optional[List[GpuGroup]] = [GpuGroup.ANY] # for gpuIds
76
80
  imageName: Optional[str] = "" # for template.imageName
77
81
 
82
+ networkVolume: Optional[NetworkVolume] = None
83
+
78
84
  # === Input Fields ===
79
85
  executionTimeoutMs: Optional[int] = None
80
86
  gpuCount: Optional[int] = 1
@@ -146,6 +152,10 @@ class ServerlessResource(DeployableResource):
146
152
  if self.flashboot:
147
153
  self.name += "-fb"
148
154
 
155
+ if self.networkVolume and self.networkVolume.is_created:
156
+ # Volume already exists, use its ID
157
+ self.networkVolumeId = self.networkVolume.id
158
+
149
159
  if self.instanceIds:
150
160
  return self._sync_input_fields_cpu()
151
161
  else:
@@ -181,6 +191,21 @@ class ServerlessResource(DeployableResource):
181
191
 
182
192
  return self
183
193
 
194
+ async def _ensure_network_volume_deployed(self) -> None:
195
+ """
196
+ Ensures network volume is deployed and ready.
197
+ Updates networkVolumeId with the deployed volume ID.
198
+ """
199
+ if self.networkVolumeId:
200
+ return
201
+
202
+ if not self.networkVolume:
203
+ log.info(f"{self.name} requires a default network volume")
204
+ self.networkVolume = NetworkVolume(name=f"{self.name}-volume")
205
+
206
+ if deployedNetworkVolume := await self.networkVolume.deploy():
207
+ self.networkVolumeId = deployedNetworkVolume.id
208
+
184
209
  def is_deployed(self) -> bool:
185
210
  """
186
211
  Checks if the serverless resource is deployed and available.
@@ -206,6 +231,9 @@ class ServerlessResource(DeployableResource):
206
231
  log.debug(f"{self} exists")
207
232
  return self
208
233
 
234
+ # NEW: Ensure network volume is deployed first
235
+ await self._ensure_network_volume_deployed()
236
+
209
237
  async with RunpodGraphQLClient() as client:
210
238
  payload = self.model_dump(exclude=self._input_only, exclude_none=True)
211
239
  result = await client.create_endpoint(payload)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tetra_rp
3
- Version: 0.5.5
3
+ Version: 0.7.0
4
4
  Summary: A Python library for distributed inference and serving of machine learning models
5
5
  Author-email: Marut Pandya <pandyamarut@gmail.com>, Patrick Rachford <prachford@icloud.com>, Dean Quinanola <dean.quinanola@runpod.io>
6
6
  License: MIT
@@ -20,10 +20,12 @@ src/tetra_rp/core/pool/worker.py
20
20
  src/tetra_rp/core/resources/__init__.py
21
21
  src/tetra_rp/core/resources/base.py
22
22
  src/tetra_rp/core/resources/cloud.py
23
+ src/tetra_rp/core/resources/constants.py
23
24
  src/tetra_rp/core/resources/cpu.py
24
25
  src/tetra_rp/core/resources/environment.py
25
26
  src/tetra_rp/core/resources/gpu.py
26
27
  src/tetra_rp/core/resources/live_serverless.py
28
+ src/tetra_rp/core/resources/network_volume.py
27
29
  src/tetra_rp/core/resources/resource_manager.py
28
30
  src/tetra_rp/core/resources/serverless.py
29
31
  src/tetra_rp/core/resources/template.py
@@ -1,5 +0,0 @@
1
- from .runpod import RunpodGraphQLClient
2
-
3
- __all__ = [
4
- "RunpodGraphQLClient",
5
- ]
File without changes
File without changes