tetra-rp 0.7.0__py3-none-any.whl → 0.8.0__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.
tetra_rp/client.py CHANGED
@@ -1,17 +1,19 @@
1
+ import inspect
1
2
  import logging
2
3
  from functools import wraps
3
- from typing import List
4
- from .core.resources import ServerlessResource, ResourceManager
5
- from .stubs import stub_resource
4
+ from typing import List, Optional
6
5
 
6
+ from .core.resources import ResourceManager, ServerlessResource
7
+ from .execute_class import create_remote_class
8
+ from .stubs import stub_resource
7
9
 
8
10
  log = logging.getLogger(__name__)
9
11
 
10
12
 
11
13
  def remote(
12
14
  resource_config: ServerlessResource,
13
- dependencies: List[str] = None,
14
- system_dependencies: List[str] = None,
15
+ dependencies: Optional[List[str]] = None,
16
+ system_dependencies: Optional[List[str]] = None,
15
17
  **extra,
16
18
  ):
17
19
  """
@@ -24,8 +26,6 @@ def remote(
24
26
  to be provisioned or used.
25
27
  dependencies (List[str], optional): A list of pip package names to be installed in the remote
26
28
  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.
29
29
  extra (dict, optional): Additional parameters for the execution of the resource. Defaults to an empty dict.
30
30
 
31
31
  Returns:
@@ -45,17 +45,26 @@ def remote(
45
45
  ```
46
46
  """
47
47
 
48
- def decorator(func):
49
- @wraps(func)
50
- async def wrapper(*args, **kwargs):
51
- resource_manager = ResourceManager()
52
- remote_resource = await resource_manager.get_or_deploy_resource(
53
- resource_config
48
+ def decorator(func_or_class):
49
+ if inspect.isclass(func_or_class):
50
+ # Handle class decoration
51
+ return create_remote_class(
52
+ func_or_class, resource_config, dependencies, system_dependencies, extra
54
53
  )
54
+ else:
55
+ # Handle function decoration (unchanged)
56
+ @wraps(func_or_class)
57
+ async def wrapper(*args, **kwargs):
58
+ resource_manager = ResourceManager()
59
+ remote_resource = await resource_manager.get_or_deploy_resource(
60
+ resource_config
61
+ )
55
62
 
56
- stub = stub_resource(remote_resource, **extra)
57
- return await stub(func, dependencies, system_dependencies, *args, **kwargs)
63
+ stub = stub_resource(remote_resource, **extra)
64
+ return await stub(
65
+ func_or_class, dependencies, system_dependencies, *args, **kwargs
66
+ )
58
67
 
59
- return wrapper
68
+ return wrapper
60
69
 
61
70
  return decorator
@@ -0,0 +1,178 @@
1
+ import base64
2
+ import inspect
3
+ import logging
4
+ import textwrap
5
+ import uuid
6
+ from typing import List, Type, Optional
7
+
8
+ import cloudpickle
9
+
10
+ from .core.resources import ResourceManager, ServerlessResource
11
+ from .protos.remote_execution import FunctionRequest
12
+ from .stubs import stub_resource
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ def extract_class_code_simple(cls: Type) -> str:
18
+ """Extract clean class code without decorators and proper indentation"""
19
+ try:
20
+ # Get source code
21
+ source = inspect.getsource(cls)
22
+
23
+ # Split into lines
24
+ lines = source.split("\n")
25
+
26
+ # Find the class definition line (starts with 'class' and contains ':')
27
+ class_start_idx = -1
28
+ for i, line in enumerate(lines):
29
+ stripped = line.strip()
30
+ if stripped.startswith("class ") and ":" in stripped:
31
+ class_start_idx = i
32
+ break
33
+
34
+ if class_start_idx == -1:
35
+ raise ValueError("Could not find class definition")
36
+
37
+ # Take lines from class definition onwards (ignore everything before)
38
+ class_lines = lines[class_start_idx:]
39
+
40
+ # Remove empty lines at the end
41
+ while class_lines and not class_lines[-1].strip():
42
+ class_lines.pop()
43
+
44
+ # Join back and dedent to remove any leading indentation
45
+ class_code = "\n".join(class_lines)
46
+ class_code = textwrap.dedent(class_code)
47
+
48
+ # Validate the code by trying to compile it
49
+ compile(class_code, "<string>", "exec")
50
+
51
+ log.debug(f"Successfully extracted class code for {cls.__name__}")
52
+ return class_code
53
+
54
+ except Exception as e:
55
+ log.warning(f"Could not extract class code for {cls.__name__}: {e}")
56
+ log.warning("Falling back to basic class structure")
57
+
58
+ # Enhanced fallback: try to preserve method signatures
59
+ fallback_methods = []
60
+ for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
61
+ try:
62
+ sig = inspect.signature(method)
63
+ fallback_methods.append(f" def {name}{sig}:")
64
+ fallback_methods.append(" pass")
65
+ fallback_methods.append("")
66
+ except (TypeError, ValueError, OSError) as e:
67
+ log.warning(f"Could not extract method signature for {name}: {e}")
68
+ fallback_methods.append(f" def {name}(self, *args, **kwargs):")
69
+ fallback_methods.append(" pass")
70
+ fallback_methods.append("")
71
+
72
+ fallback_code = f"""class {cls.__name__}:
73
+ def __init__(self, *args, **kwargs):
74
+ pass
75
+
76
+ {chr(10).join(fallback_methods)}"""
77
+
78
+ return fallback_code
79
+
80
+
81
+ def create_remote_class(
82
+ cls: Type,
83
+ resource_config: ServerlessResource,
84
+ dependencies: Optional[List[str]],
85
+ system_dependencies: Optional[List[str]],
86
+ extra: dict,
87
+ ):
88
+ """
89
+ Create a remote class wrapper.
90
+ """
91
+ # Validate inputs
92
+ if not inspect.isclass(cls):
93
+ raise TypeError(f"Expected a class, got {type(cls).__name__}")
94
+ if not hasattr(cls, "__name__"):
95
+ raise ValueError("Class must have a __name__ attribute")
96
+
97
+ class RemoteClassWrapper:
98
+ def __init__(self, *args, **kwargs):
99
+ self._class_type = cls
100
+ self._resource_config = resource_config
101
+ self._dependencies = dependencies or []
102
+ self._system_dependencies = system_dependencies or []
103
+ self._extra = extra
104
+ self._constructor_args = args
105
+ self._constructor_kwargs = kwargs
106
+ self._instance_id = f"{cls.__name__}_{uuid.uuid4().hex[:8]}"
107
+ self._initialized = False
108
+
109
+ self._clean_class_code = extract_class_code_simple(cls)
110
+
111
+ log.debug(f"Created remote class wrapper for {cls.__name__}")
112
+
113
+ async def _ensure_initialized(self):
114
+ """Ensure the remote instance is created."""
115
+ if self._initialized:
116
+ return
117
+
118
+ # Get remote resource
119
+ resource_manager = ResourceManager()
120
+ remote_resource = await resource_manager.get_or_deploy_resource(
121
+ self._resource_config
122
+ )
123
+ self._stub = stub_resource(remote_resource, **self._extra)
124
+
125
+ # Create the remote instance by calling a method (which will trigger instance creation)
126
+ # We'll do this on first method call
127
+ self._initialized = True
128
+
129
+ def __getattr__(self, name):
130
+ """Dynamically create method proxies for all class methods."""
131
+ if name.startswith("_"):
132
+ raise AttributeError(
133
+ f"'{self.__class__.__name__}' object has no attribute '{name}'"
134
+ )
135
+
136
+ async def method_proxy(*args, **kwargs):
137
+ await self._ensure_initialized()
138
+
139
+ # Create class method request
140
+
141
+ # class_code = inspect.getsource(self._class_type)
142
+ class_code = self._clean_class_code
143
+
144
+ request = FunctionRequest(
145
+ execution_type="class",
146
+ class_name=self._class_type.__name__,
147
+ class_code=class_code,
148
+ method_name=name,
149
+ args=[
150
+ base64.b64encode(cloudpickle.dumps(arg)).decode("utf-8")
151
+ for arg in args
152
+ ],
153
+ kwargs={
154
+ k: base64.b64encode(cloudpickle.dumps(v)).decode("utf-8")
155
+ for k, v in kwargs.items()
156
+ },
157
+ constructor_args=[
158
+ base64.b64encode(cloudpickle.dumps(arg)).decode("utf-8")
159
+ for arg in self._constructor_args
160
+ ],
161
+ constructor_kwargs={
162
+ k: base64.b64encode(cloudpickle.dumps(v)).decode("utf-8")
163
+ for k, v in self._constructor_kwargs.items()
164
+ },
165
+ dependencies=self._dependencies,
166
+ system_dependencies=self._system_dependencies,
167
+ instance_id=self._instance_id,
168
+ create_new_instance=not hasattr(
169
+ self, "_stub"
170
+ ), # Create new only on first call
171
+ )
172
+
173
+ # Execute via stub
174
+ return await self._stub.execute_class_method(request) # type: ignore
175
+
176
+ return method_proxy
177
+
178
+ return RemoteClassWrapper
@@ -1,15 +1,18 @@
1
1
  # TODO: generate using betterproto
2
-
3
2
  from abc import ABC, abstractmethod
4
- from typing import List, Dict, Optional
5
- from pydantic import BaseModel, Field
3
+ from typing import Dict, List, Optional
4
+
5
+ from pydantic import BaseModel, Field, model_validator
6
6
 
7
7
 
8
8
  class FunctionRequest(BaseModel):
9
- function_name: str = Field(
9
+ # MADE OPTIONAL - can be None for class-only execution
10
+ function_name: Optional[str] = Field(
11
+ default=None,
10
12
  description="Name of the function to execute",
11
13
  )
12
- function_code: str = Field(
14
+ function_code: Optional[str] = Field(
15
+ default=None,
13
16
  description="Source code of the function to execute",
14
17
  )
15
18
  args: List = Field(
@@ -29,8 +32,67 @@ class FunctionRequest(BaseModel):
29
32
  description="Optional list of system dependencies to install before executing the function",
30
33
  )
31
34
 
35
+ # NEW FIELDS FOR CLASS SUPPORT
36
+ execution_type: str = Field(
37
+ default="function", description="Type of execution: 'function' or 'class'"
38
+ )
39
+ class_name: Optional[str] = Field(
40
+ default=None,
41
+ description="Name of the class to instantiate (for class execution)",
42
+ )
43
+ class_code: Optional[str] = Field(
44
+ default=None,
45
+ description="Source code of the class to instantiate (for class execution)",
46
+ )
47
+ constructor_args: Optional[List] = Field(
48
+ default_factory=list,
49
+ description="List of base64-encoded cloudpickle-serialized constructor arguments",
50
+ )
51
+ constructor_kwargs: Optional[Dict] = Field(
52
+ default_factory=dict,
53
+ description="Dictionary of base64-encoded cloudpickle-serialized constructor keyword arguments",
54
+ )
55
+ method_name: str = Field(
56
+ default="__call__",
57
+ description="Name of the method to call on the class instance",
58
+ )
59
+ instance_id: Optional[str] = Field(
60
+ default=None,
61
+ description="Unique identifier for the class instance (for persistence)",
62
+ )
63
+ create_new_instance: bool = Field(
64
+ default=True,
65
+ description="Whether to create a new instance or reuse existing one",
66
+ )
67
+
68
+ @model_validator(mode="after")
69
+ def validate_execution_requirements(self) -> "FunctionRequest":
70
+ """Validate that required fields are provided based on execution_type"""
71
+ if self.execution_type == "function":
72
+ if self.function_name is None:
73
+ raise ValueError(
74
+ 'function_name is required when execution_type is "function"'
75
+ )
76
+ if self.function_code is None:
77
+ raise ValueError(
78
+ 'function_code is required when execution_type is "function"'
79
+ )
80
+
81
+ elif self.execution_type == "class":
82
+ if self.class_name is None:
83
+ raise ValueError(
84
+ 'class_name is required when execution_type is "class"'
85
+ )
86
+ if self.class_code is None:
87
+ raise ValueError(
88
+ 'class_code is required when execution_type is "class"'
89
+ )
90
+
91
+ return self
92
+
32
93
 
33
94
  class FunctionResponse(BaseModel):
95
+ # EXISTING FIELDS (unchanged)
34
96
  success: bool = Field(
35
97
  description="Indicates if the function execution was successful",
36
98
  )
@@ -47,6 +109,15 @@ class FunctionResponse(BaseModel):
47
109
  description="Captured standard output from the function execution",
48
110
  )
49
111
 
112
+ # NEW FIELDS FOR CLASS SUPPORT
113
+ instance_id: Optional[str] = Field(
114
+ default=None, description="ID of the class instance that was used/created"
115
+ )
116
+ instance_info: Optional[Dict] = Field(
117
+ default=None,
118
+ description="Metadata about the class instance (creation time, call count, etc.)",
119
+ )
120
+
50
121
 
51
122
  class RemoteExecutorStub(ABC):
52
123
  """Abstract base class for remote execution."""
@@ -1,13 +1,13 @@
1
1
  import logging
2
2
  from functools import singledispatch
3
- from .live_serverless import LiveServerlessStub
4
- from .serverless import ServerlessEndpointStub
3
+
5
4
  from ..core.resources import (
6
5
  CpuServerlessEndpoint,
7
6
  LiveServerless,
8
7
  ServerlessEndpoint,
9
8
  )
10
-
9
+ from .live_serverless import LiveServerlessStub
10
+ from .serverless import ServerlessEndpointStub
11
11
 
12
12
  log = logging.getLogger(__name__)
13
13
 
@@ -22,20 +22,29 @@ def stub_resource(resource, **extra):
22
22
 
23
23
  @stub_resource.register(LiveServerless)
24
24
  def _(resource, **extra):
25
+ stub = LiveServerlessStub(resource)
26
+
27
+ # Function execution
25
28
  async def stubbed_resource(
26
29
  func, dependencies, system_dependencies, *args, **kwargs
27
30
  ) -> dict:
28
31
  if args == (None,):
29
- # cleanup: when the function is called with no args
30
32
  args = []
31
33
 
32
- stub = LiveServerlessStub(resource)
33
34
  request = stub.prepare_request(
34
35
  func, dependencies, system_dependencies, *args, **kwargs
35
36
  )
36
37
  response = await stub.ExecuteFunction(request)
37
38
  return stub.handle_response(response)
38
39
 
40
+ # Class method execution
41
+ async def execute_class_method(request):
42
+ response = await stub.ExecuteFunction(request)
43
+ return stub.handle_response(response)
44
+
45
+ # Attach the method to the function
46
+ stubbed_resource.execute_class_method = execute_class_method
47
+
39
48
  return stubbed_resource
40
49
 
41
50
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tetra_rp
3
- Version: 0.7.0
3
+ Version: 0.8.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,5 +1,6 @@
1
1
  tetra_rp/__init__.py,sha256=-1S5sYIKtnUV8V1HlSIbX1yZwiUrsO8J5b3ZEIR_phU,687
2
- tetra_rp/client.py,sha256=5zerW5tTUnTSe75cRGgTBhqsKNXoCVWgb3Kzh9tJvPA,2209
2
+ tetra_rp/client.py,sha256=rAMMmn4ejAayFXJMZzx7dG_8Y65tCEMI6wSSKgur4zQ,2500
3
+ tetra_rp/execute_class.py,sha256=OXP1IkORELNFxOi1WHQOfUepmQGfkKmw85iZccaMEww,6515
3
4
  tetra_rp/logger.py,sha256=gk5-PWp3k_GQ5DxndsRkBCX0jarp_3lgZ1oiTFuThQg,1125
4
5
  tetra_rp/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
6
  tetra_rp/core/api/__init__.py,sha256=oldrEKMwxYoBPLvPfVlaFS3wfUtTTxCN6-HzlpTh6vE,124
@@ -28,12 +29,12 @@ tetra_rp/core/utils/backoff.py,sha256=1pfa0smFNpib8nztcIgBbtrVvQeECKh-aNOfL2Tztg
28
29
  tetra_rp/core/utils/json.py,sha256=q0r7aEdfh8kKVeHGeh9fBDfuhHYNopSreislAMB6HhM,1163
29
30
  tetra_rp/core/utils/singleton.py,sha256=JRli0HhBfq4P9mBUOg1TZUUwMvIenRqWdymX3qFMm2k,210
30
31
  tetra_rp/protos/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
- tetra_rp/protos/remote_execution.py,sha256=I4641-Dlzj4O7AuZbVei8O9aV2VNrRTcE8r7Fm0e-V8,1901
32
+ tetra_rp/protos/remote_execution.py,sha256=F4uwobnp5q-lX3lR7NCAB23J6OzlzcsB35cezwuoSnI,4638
32
33
  tetra_rp/stubs/__init__.py,sha256=ozKsHs8q0T7o2qhQEquub9hqomh1Htys53mMraaRu2E,72
33
34
  tetra_rp/stubs/live_serverless.py,sha256=o1NH5XEwUD-27NXJsEGO0IwnuDp8iXwUiw5nZtaZZOI,4199
34
- tetra_rp/stubs/registry.py,sha256=V4m3CeXl8j1pguHuuflxqpWeBgVDQ93YkhxJbElyP7Q,2599
35
+ tetra_rp/stubs/registry.py,sha256=dmbyC7uBp04_sXsG2wJCloFfFRzYjYQ-naEBKhTRo-U,2839
35
36
  tetra_rp/stubs/serverless.py,sha256=BM_a5Ml5VADBYu2WRNmo9qnicP8NnXDGl5ywifulbD0,947
36
- tetra_rp-0.7.0.dist-info/METADATA,sha256=jcXAGoiAFJVTYosJV2SGMw8L14UkUw2PHNKKH5mXkR8,28055
37
- tetra_rp-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
- tetra_rp-0.7.0.dist-info/top_level.txt,sha256=bBay7JTDwJXsTYvVjrwno9hnF-j0q272lk65f2AcPjU,9
39
- tetra_rp-0.7.0.dist-info/RECORD,,
37
+ tetra_rp-0.8.0.dist-info/METADATA,sha256=M0qEc5SQITXYUy_FQLy_EE24CiTbJtrXjU6g5-q3fws,28055
38
+ tetra_rp-0.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
39
+ tetra_rp-0.8.0.dist-info/top_level.txt,sha256=bBay7JTDwJXsTYvVjrwno9hnF-j0q272lk65f2AcPjU,9
40
+ tetra_rp-0.8.0.dist-info/RECORD,,