indexify 0.2.48__py3-none-any.whl → 0.3.1__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.
- indexify/{cli.py → cli/cli.py} +75 -82
- indexify/executor/README.md +35 -0
- indexify/executor/api_objects.py +9 -3
- indexify/executor/downloader.py +5 -5
- indexify/executor/executor.py +35 -22
- indexify/executor/function_executor/function_executor.py +14 -3
- indexify/executor/function_executor/function_executor_state.py +13 -10
- indexify/executor/function_executor/invocation_state_client.py +2 -1
- indexify/executor/function_executor/server/subprocess_function_executor_server_factory.py +22 -10
- indexify/executor/function_executor/single_task_runner.py +43 -26
- indexify/executor/function_executor/task_input.py +1 -3
- indexify/executor/task_fetcher.py +5 -7
- indexify/executor/task_reporter.py +3 -5
- indexify/executor/task_runner.py +31 -24
- indexify/function_executor/README.md +18 -0
- indexify/function_executor/handlers/run_function/function_inputs_loader.py +13 -14
- indexify/function_executor/handlers/run_function/handler.py +16 -40
- indexify/function_executor/handlers/run_function/request_validator.py +7 -5
- indexify/function_executor/handlers/run_function/response_helper.py +6 -8
- indexify/function_executor/initialize_request_validator.py +1 -2
- indexify/function_executor/invocation_state/invocation_state_proxy_server.py +1 -1
- indexify/function_executor/invocation_state/proxied_invocation_state.py +1 -3
- indexify/function_executor/main.py +50 -0
- indexify/function_executor/proto/configuration.py +8 -0
- indexify/function_executor/proto/function_executor.proto +9 -4
- indexify/function_executor/proto/function_executor_pb2.py +24 -24
- indexify/function_executor/proto/function_executor_pb2.pyi +24 -4
- indexify/function_executor/server.py +4 -6
- indexify/function_executor/{function_executor_service.py → service.py} +35 -24
- indexify/utils/README.md +3 -0
- indexify/{common_util.py → utils/http_client.py} +2 -2
- indexify/{logging.py → utils/logging.py} +36 -2
- indexify-0.3.1.dist-info/METADATA +38 -0
- indexify-0.3.1.dist-info/RECORD +44 -0
- {indexify-0.2.48.dist-info → indexify-0.3.1.dist-info}/WHEEL +1 -1
- indexify-0.3.1.dist-info/entry_points.txt +4 -0
- indexify/__init__.py +0 -31
- indexify/data_loaders/__init__.py +0 -58
- indexify/data_loaders/local_directory_loader.py +0 -37
- indexify/data_loaders/url_loader.py +0 -52
- indexify/error.py +0 -8
- indexify/functions_sdk/data_objects.py +0 -27
- indexify/functions_sdk/graph.py +0 -364
- indexify/functions_sdk/graph_definition.py +0 -63
- indexify/functions_sdk/graph_validation.py +0 -70
- indexify/functions_sdk/image.py +0 -222
- indexify/functions_sdk/indexify_functions.py +0 -354
- indexify/functions_sdk/invocation_state/invocation_state.py +0 -22
- indexify/functions_sdk/invocation_state/local_invocation_state.py +0 -30
- indexify/functions_sdk/object_serializer.py +0 -68
- indexify/functions_sdk/pipeline.py +0 -33
- indexify/http_client.py +0 -379
- indexify/remote_graph.py +0 -138
- indexify/remote_pipeline.py +0 -25
- indexify/settings.py +0 -1
- indexify-0.2.48.dist-info/LICENSE.txt +0 -201
- indexify-0.2.48.dist-info/METADATA +0 -154
- indexify-0.2.48.dist-info/RECORD +0 -60
- indexify-0.2.48.dist-info/entry_points.txt +0 -3
indexify/functions_sdk/image.py
DELETED
@@ -1,222 +0,0 @@
|
|
1
|
-
import datetime
|
2
|
-
import hashlib
|
3
|
-
import importlib
|
4
|
-
import logging
|
5
|
-
import os
|
6
|
-
import pathlib
|
7
|
-
import sys
|
8
|
-
import tarfile
|
9
|
-
from io import BytesIO
|
10
|
-
from typing import Dict, List, Optional
|
11
|
-
|
12
|
-
import docker
|
13
|
-
import docker.api.build
|
14
|
-
from pydantic import BaseModel
|
15
|
-
|
16
|
-
|
17
|
-
# Pydantic object for API
|
18
|
-
class ImageInformation(BaseModel):
|
19
|
-
image_name: str
|
20
|
-
image_hash: str
|
21
|
-
image_url: Optional[str] = ""
|
22
|
-
sdk_version: str
|
23
|
-
|
24
|
-
# These are deprecated and here for backwards compatibility
|
25
|
-
run_strs: List[str] | None = []
|
26
|
-
tag: str | None = ""
|
27
|
-
base_image: str | None = ""
|
28
|
-
|
29
|
-
|
30
|
-
HASH_BUFF_SIZE = 1024**2
|
31
|
-
|
32
|
-
|
33
|
-
class BuildOp(BaseModel):
|
34
|
-
op_type: str
|
35
|
-
options: Dict[str, str] = {}
|
36
|
-
args: List[str]
|
37
|
-
|
38
|
-
def hash(self, hash):
|
39
|
-
match self.op_type:
|
40
|
-
case "RUN" | "ADD":
|
41
|
-
hash.update(self.op_type.encode())
|
42
|
-
for a in self.args:
|
43
|
-
hash.update(a.encode())
|
44
|
-
|
45
|
-
case "COPY":
|
46
|
-
hash.update("COPY".encode())
|
47
|
-
for root, dirs, files in os.walk(self.args[0]):
|
48
|
-
for file in files:
|
49
|
-
filename = pathlib.Path(root, file)
|
50
|
-
with open(filename, "rb") as fp:
|
51
|
-
data = fp.read(HASH_BUFF_SIZE)
|
52
|
-
while data:
|
53
|
-
hash.update(data)
|
54
|
-
data = fp.read(HASH_BUFF_SIZE)
|
55
|
-
|
56
|
-
case _:
|
57
|
-
raise ValueError(f"Unsupported build op type {self.op_type}")
|
58
|
-
|
59
|
-
def render(self):
|
60
|
-
match self.op_type:
|
61
|
-
case "RUN" | "ADD":
|
62
|
-
options = [f"--{k}={v}" for k, v in self.options.items()]
|
63
|
-
return f"{self.op_type} {' '.join(options)} {' '.join(self.args)}"
|
64
|
-
|
65
|
-
case "COPY":
|
66
|
-
return f"COPY {self.args[0]} {self.args[1]}"
|
67
|
-
case _:
|
68
|
-
raise ValueError(f"Unsupported build op type {self.op_type}")
|
69
|
-
|
70
|
-
|
71
|
-
class Build(BaseModel):
|
72
|
-
"""
|
73
|
-
Model for talking with the build service.
|
74
|
-
"""
|
75
|
-
|
76
|
-
id: int | None = None
|
77
|
-
namespace: str
|
78
|
-
image_name: str
|
79
|
-
image_hash: str
|
80
|
-
status: str | None
|
81
|
-
result: str | None
|
82
|
-
error_message: str | None = None # Only provided when result is "failed"
|
83
|
-
|
84
|
-
created_at: datetime.datetime | None
|
85
|
-
started_at: datetime.datetime | None = None
|
86
|
-
build_completed_at: datetime.datetime | None = None
|
87
|
-
push_completed_at: datetime.datetime | None = None
|
88
|
-
uri: str | None = None
|
89
|
-
|
90
|
-
|
91
|
-
class Image:
|
92
|
-
def __init__(self):
|
93
|
-
self._image_name = None
|
94
|
-
self._tag = "latest"
|
95
|
-
self._base_image = BASE_IMAGE_NAME
|
96
|
-
self._python_version = LOCAL_PYTHON_VERSION
|
97
|
-
self._build_ops = [] # List of ImageOperation
|
98
|
-
self._sdk_version = importlib.metadata.version("indexify")
|
99
|
-
|
100
|
-
def name(self, image_name):
|
101
|
-
self._image_name = image_name
|
102
|
-
return self
|
103
|
-
|
104
|
-
def tag(self, tag):
|
105
|
-
self._tag = tag
|
106
|
-
return self
|
107
|
-
|
108
|
-
def base_image(self, base_image):
|
109
|
-
self._base_image = base_image
|
110
|
-
return self
|
111
|
-
|
112
|
-
def add(self, source: str, dest: str, **kwargs):
|
113
|
-
self._build_ops.append(
|
114
|
-
BuildOp(op_type="ADD", args=[source, dest], options=kwargs)
|
115
|
-
)
|
116
|
-
return self
|
117
|
-
|
118
|
-
def run(self, run_str, **kwargs):
|
119
|
-
self._build_ops.append(BuildOp(op_type="RUN", args=[run_str], options=kwargs))
|
120
|
-
return self
|
121
|
-
|
122
|
-
def copy(self, source: str, dest: str, **kwargs):
|
123
|
-
self._build_ops.append(
|
124
|
-
BuildOp(op_type="COPY", args=[source, dest], options=kwargs)
|
125
|
-
)
|
126
|
-
return self
|
127
|
-
|
128
|
-
def to_image_information(self):
|
129
|
-
return ImageInformation(
|
130
|
-
image_name=self._image_name,
|
131
|
-
sdk_version=self._sdk_version,
|
132
|
-
image_hash=self.hash(),
|
133
|
-
)
|
134
|
-
|
135
|
-
def build_context(self, filename: str):
|
136
|
-
with tarfile.open(filename, "w:gz") as tf:
|
137
|
-
for op in self._build_ops:
|
138
|
-
if op.op_type == "COPY":
|
139
|
-
src = op.args[0]
|
140
|
-
logging.info(f"Adding {src}")
|
141
|
-
tf.add(src, src)
|
142
|
-
|
143
|
-
dockerfile = self._generate_dockerfile()
|
144
|
-
tarinfo = tarfile.TarInfo("Dockerfile")
|
145
|
-
tarinfo.size = len(dockerfile)
|
146
|
-
|
147
|
-
tf.addfile(tarinfo, BytesIO(dockerfile.encode()))
|
148
|
-
|
149
|
-
def _generate_dockerfile(self, python_sdk_path: Optional[str] = None):
|
150
|
-
docker_contents = [
|
151
|
-
f"FROM {self._base_image}",
|
152
|
-
"RUN mkdir -p ~/.indexify",
|
153
|
-
f"RUN echo {self._image_name} > ~/.indexify/image_name",
|
154
|
-
f"RUN echo {self.hash()} > ~/.indexify/image_hash",
|
155
|
-
"WORKDIR /app",
|
156
|
-
]
|
157
|
-
|
158
|
-
for build_op in self._build_ops:
|
159
|
-
docker_contents.append(build_op.render())
|
160
|
-
|
161
|
-
if python_sdk_path is not None:
|
162
|
-
logging.info(
|
163
|
-
f"Building image {self._image_name} with local version of the SDK"
|
164
|
-
)
|
165
|
-
if not os.path.exists(python_sdk_path):
|
166
|
-
print(f"error: {python_sdk_path} does not exist")
|
167
|
-
os.exit(1)
|
168
|
-
docker_contents.append(f"COPY {python_sdk_path} /app/python-sdk")
|
169
|
-
docker_contents.append("RUN (cd /app/python-sdk && pip install .)")
|
170
|
-
else:
|
171
|
-
docker_contents.append(f"RUN pip install indexify=={self._sdk_version}")
|
172
|
-
|
173
|
-
docker_file = "\n".join(docker_contents)
|
174
|
-
return docker_file
|
175
|
-
|
176
|
-
def build(self, python_sdk_path: Optional[str] = None, docker_client=None):
|
177
|
-
if docker_client is None:
|
178
|
-
docker_client = docker.from_env()
|
179
|
-
docker_client.ping()
|
180
|
-
|
181
|
-
docker_file = self._generate_dockerfile(python_sdk_path=python_sdk_path)
|
182
|
-
image_name = f"{self._image_name}:{self._tag}"
|
183
|
-
|
184
|
-
docker.api.build.process_dockerfile = lambda dockerfile, path: (
|
185
|
-
"Dockerfile",
|
186
|
-
dockerfile,
|
187
|
-
)
|
188
|
-
|
189
|
-
return docker_client.images.build(
|
190
|
-
path=".",
|
191
|
-
dockerfile=docker_file,
|
192
|
-
tag=image_name,
|
193
|
-
rm=True,
|
194
|
-
)
|
195
|
-
|
196
|
-
def hash(self) -> str:
|
197
|
-
hash = hashlib.sha256(
|
198
|
-
self._image_name.encode()
|
199
|
-
) # Make a hash of the image name
|
200
|
-
hash.update(self._base_image.encode())
|
201
|
-
for op in self._build_ops:
|
202
|
-
op.hash(hash)
|
203
|
-
|
204
|
-
hash.update(self._sdk_version.encode())
|
205
|
-
|
206
|
-
return hash.hexdigest()
|
207
|
-
|
208
|
-
|
209
|
-
LOCAL_PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"
|
210
|
-
BASE_IMAGE_NAME = f"python:{LOCAL_PYTHON_VERSION}-slim-bookworm"
|
211
|
-
|
212
|
-
|
213
|
-
def GetDefaultPythonImage(python_version: str):
|
214
|
-
return (
|
215
|
-
Image()
|
216
|
-
.name("tensorlake/indexify-executor-default")
|
217
|
-
.base_image(f"python:{python_version}-slim-bookworm")
|
218
|
-
.tag(python_version)
|
219
|
-
)
|
220
|
-
|
221
|
-
|
222
|
-
DEFAULT_IMAGE = GetDefaultPythonImage(LOCAL_PYTHON_VERSION)
|
@@ -1,354 +0,0 @@
|
|
1
|
-
import inspect
|
2
|
-
import traceback
|
3
|
-
from inspect import Parameter
|
4
|
-
from typing import (
|
5
|
-
Any,
|
6
|
-
Dict,
|
7
|
-
List,
|
8
|
-
Optional,
|
9
|
-
Tuple,
|
10
|
-
Type,
|
11
|
-
Union,
|
12
|
-
get_args,
|
13
|
-
get_origin,
|
14
|
-
)
|
15
|
-
|
16
|
-
from pydantic import BaseModel
|
17
|
-
from typing_extensions import get_type_hints
|
18
|
-
|
19
|
-
from .data_objects import IndexifyData
|
20
|
-
from .image import DEFAULT_IMAGE, Image
|
21
|
-
from .invocation_state.invocation_state import InvocationState
|
22
|
-
from .object_serializer import get_serializer
|
23
|
-
|
24
|
-
|
25
|
-
class GraphInvocationContext:
|
26
|
-
def __init__(
|
27
|
-
self,
|
28
|
-
invocation_id: str,
|
29
|
-
graph_name: str,
|
30
|
-
graph_version: str,
|
31
|
-
invocation_state: InvocationState,
|
32
|
-
):
|
33
|
-
self.invocation_id = invocation_id
|
34
|
-
self.graph_name = graph_name
|
35
|
-
self.graph_version = graph_version
|
36
|
-
self.invocation_state = invocation_state
|
37
|
-
|
38
|
-
|
39
|
-
def is_pydantic_model_from_annotation(type_annotation):
|
40
|
-
# If it's a string representation
|
41
|
-
if isinstance(type_annotation, str):
|
42
|
-
# Extract the class name from the string
|
43
|
-
class_name = type_annotation.split("'")[-2].split(".")[-1]
|
44
|
-
# This part is tricky and might require additional context or imports
|
45
|
-
# You might need to import the actual class or module where it's defined
|
46
|
-
# For example:
|
47
|
-
# from indexify.functions_sdk.data_objects import File
|
48
|
-
# return issubclass(eval(class_name), BaseModel)
|
49
|
-
return False # Default to False if we can't evaluate
|
50
|
-
|
51
|
-
# If it's a Type object
|
52
|
-
origin = get_origin(type_annotation)
|
53
|
-
if origin is not None:
|
54
|
-
# Handle generic types like List[File], Optional[File], etc.
|
55
|
-
args = get_args(type_annotation)
|
56
|
-
if args:
|
57
|
-
return is_pydantic_model_from_annotation(args[0])
|
58
|
-
|
59
|
-
# If it's a direct class reference
|
60
|
-
if isinstance(type_annotation, type):
|
61
|
-
return issubclass(type_annotation, BaseModel)
|
62
|
-
|
63
|
-
return False
|
64
|
-
|
65
|
-
|
66
|
-
class PlacementConstraints(BaseModel):
|
67
|
-
min_python_version: Optional[str] = "3.9"
|
68
|
-
max_python_version: Optional[str] = None
|
69
|
-
platform: Optional[str] = None
|
70
|
-
image_name: Optional[str] = None
|
71
|
-
|
72
|
-
|
73
|
-
class IndexifyFunction:
|
74
|
-
name: str = ""
|
75
|
-
description: str = ""
|
76
|
-
image: Optional[Image] = DEFAULT_IMAGE
|
77
|
-
placement_constraints: List[PlacementConstraints] = []
|
78
|
-
accumulate: Optional[Type[Any]] = None
|
79
|
-
input_encoder: Optional[str] = "cloudpickle"
|
80
|
-
output_encoder: Optional[str] = "cloudpickle"
|
81
|
-
|
82
|
-
def run(self, *args, **kwargs) -> Union[List[Any], Any]:
|
83
|
-
pass
|
84
|
-
|
85
|
-
def _call_run(self, *args, **kwargs) -> Union[List[Any], Any]:
|
86
|
-
# Process dictionary argument mapping it to args or to kwargs.
|
87
|
-
if self.accumulate and len(args) == 2 and isinstance(args[1], dict):
|
88
|
-
sig = inspect.signature(self.run)
|
89
|
-
new_args = [args[0]] # Keep the accumulate argument
|
90
|
-
dict_arg = args[1]
|
91
|
-
new_args_from_dict, new_kwargs = _process_dict_arg(dict_arg, sig)
|
92
|
-
new_args.extend(new_args_from_dict)
|
93
|
-
return self.run(*new_args, **new_kwargs)
|
94
|
-
elif len(args) == 1 and isinstance(args[0], dict):
|
95
|
-
sig = inspect.signature(self.run)
|
96
|
-
dict_arg = args[0]
|
97
|
-
new_args, new_kwargs = _process_dict_arg(dict_arg, sig)
|
98
|
-
return self.run(*new_args, **new_kwargs)
|
99
|
-
|
100
|
-
return self.run(*args, **kwargs)
|
101
|
-
|
102
|
-
@classmethod
|
103
|
-
def deserialize_output(cls, output: IndexifyData) -> Any:
|
104
|
-
serializer = get_serializer(cls.output_encoder)
|
105
|
-
return serializer.deserialize(output.payload)
|
106
|
-
|
107
|
-
|
108
|
-
class IndexifyRouter:
|
109
|
-
name: str = ""
|
110
|
-
description: str = ""
|
111
|
-
image: Optional[Image] = DEFAULT_IMAGE
|
112
|
-
placement_constraints: List[PlacementConstraints] = []
|
113
|
-
input_encoder: Optional[str] = "cloudpickle"
|
114
|
-
output_encoder: Optional[str] = "cloudpickle"
|
115
|
-
|
116
|
-
def run(self, *args, **kwargs) -> Optional[List[IndexifyFunction]]:
|
117
|
-
pass
|
118
|
-
|
119
|
-
# Create run method that preserves signature
|
120
|
-
def _call_run(self, *args, **kwargs):
|
121
|
-
# Process dictionary argument mapping it to args or to kwargs.
|
122
|
-
if len(args) == 1 and isinstance(args[0], dict):
|
123
|
-
sig = inspect.signature(self.run)
|
124
|
-
dict_arg = args[0]
|
125
|
-
new_args, new_kwargs = _process_dict_arg(dict_arg, sig)
|
126
|
-
return self.run(*new_args, **new_kwargs)
|
127
|
-
|
128
|
-
return self.run(*args, **kwargs)
|
129
|
-
|
130
|
-
|
131
|
-
def _process_dict_arg(dict_arg: dict, sig: inspect.Signature) -> Tuple[list, dict]:
|
132
|
-
new_args = []
|
133
|
-
new_kwargs = {}
|
134
|
-
remaining_kwargs = dict_arg.copy()
|
135
|
-
|
136
|
-
# Match dictionary keys to function parameters
|
137
|
-
for param_name, param in sig.parameters.items():
|
138
|
-
if param_name in dict_arg:
|
139
|
-
new_args.append(dict_arg[param_name])
|
140
|
-
remaining_kwargs.pop(param_name, None)
|
141
|
-
|
142
|
-
if any(v.kind == Parameter.VAR_KEYWORD for v in sig.parameters.values()):
|
143
|
-
# Combine remaining dict items with additional kwargs
|
144
|
-
new_kwargs.update(remaining_kwargs)
|
145
|
-
elif len(remaining_kwargs) > 0:
|
146
|
-
# If there are remaining kwargs, add them as a single dict argument
|
147
|
-
new_args.append(remaining_kwargs)
|
148
|
-
|
149
|
-
return new_args, new_kwargs
|
150
|
-
|
151
|
-
|
152
|
-
def indexify_router(
|
153
|
-
name: Optional[str] = None,
|
154
|
-
description: Optional[str] = "",
|
155
|
-
image: Optional[Image] = DEFAULT_IMAGE,
|
156
|
-
placement_constraints: List[PlacementConstraints] = [],
|
157
|
-
input_encoder: Optional[str] = "cloudpickle",
|
158
|
-
output_encoder: Optional[str] = "cloudpickle",
|
159
|
-
):
|
160
|
-
def construct(fn):
|
161
|
-
attrs = {
|
162
|
-
"name": name if name else fn.__name__,
|
163
|
-
"description": (
|
164
|
-
description
|
165
|
-
if description
|
166
|
-
else (fn.__doc__ or "").strip().replace("\n", "")
|
167
|
-
),
|
168
|
-
"image": image,
|
169
|
-
"placement_constraints": placement_constraints,
|
170
|
-
"input_encoder": input_encoder,
|
171
|
-
"output_encoder": output_encoder,
|
172
|
-
"run": staticmethod(fn),
|
173
|
-
}
|
174
|
-
|
175
|
-
return type("IndexifyRouter", (IndexifyRouter,), attrs)
|
176
|
-
|
177
|
-
return construct
|
178
|
-
|
179
|
-
|
180
|
-
def indexify_function(
|
181
|
-
name: Optional[str] = None,
|
182
|
-
description: Optional[str] = "",
|
183
|
-
image: Optional[Image] = DEFAULT_IMAGE,
|
184
|
-
accumulate: Optional[Type[BaseModel]] = None,
|
185
|
-
input_encoder: Optional[str] = "cloudpickle",
|
186
|
-
output_encoder: Optional[str] = "cloudpickle",
|
187
|
-
placement_constraints: List[PlacementConstraints] = [],
|
188
|
-
):
|
189
|
-
def construct(fn):
|
190
|
-
attrs = {
|
191
|
-
"name": name if name else fn.__name__,
|
192
|
-
"description": (
|
193
|
-
description
|
194
|
-
if description
|
195
|
-
else (fn.__doc__ or "").strip().replace("\n", "")
|
196
|
-
),
|
197
|
-
"image": image,
|
198
|
-
"placement_constraints": placement_constraints,
|
199
|
-
"accumulate": accumulate,
|
200
|
-
"input_encoder": input_encoder,
|
201
|
-
"output_encoder": output_encoder,
|
202
|
-
"run": staticmethod(fn),
|
203
|
-
}
|
204
|
-
|
205
|
-
return type("IndexifyFunction", (IndexifyFunction,), attrs)
|
206
|
-
|
207
|
-
return construct
|
208
|
-
|
209
|
-
|
210
|
-
class FunctionCallResult(BaseModel):
|
211
|
-
ser_outputs: List[IndexifyData]
|
212
|
-
traceback_msg: Optional[str] = None
|
213
|
-
|
214
|
-
|
215
|
-
class RouterCallResult(BaseModel):
|
216
|
-
edges: List[str]
|
217
|
-
traceback_msg: Optional[str] = None
|
218
|
-
|
219
|
-
|
220
|
-
class IndexifyFunctionWrapper:
|
221
|
-
def __init__(
|
222
|
-
self,
|
223
|
-
indexify_function: Union[IndexifyFunction, IndexifyRouter],
|
224
|
-
context: GraphInvocationContext,
|
225
|
-
):
|
226
|
-
self.indexify_function: Union[IndexifyFunction, IndexifyRouter] = (
|
227
|
-
indexify_function()
|
228
|
-
)
|
229
|
-
self.indexify_function._ctx = context
|
230
|
-
|
231
|
-
def get_output_model(self) -> Any:
|
232
|
-
if not isinstance(self.indexify_function, IndexifyFunction):
|
233
|
-
raise TypeError("Input must be an instance of IndexifyFunction")
|
234
|
-
|
235
|
-
extract_method = self.indexify_function.run
|
236
|
-
type_hints = get_type_hints(extract_method)
|
237
|
-
return_type = type_hints.get("return", Any)
|
238
|
-
if get_origin(return_type) is list:
|
239
|
-
return_type = get_args(return_type)[0]
|
240
|
-
elif get_origin(return_type) is Union:
|
241
|
-
inner_types = get_args(return_type)
|
242
|
-
if len(inner_types) == 2 and type(None) in inner_types:
|
243
|
-
return_type = (
|
244
|
-
inner_types[0] if inner_types[1] is type(None) else inner_types[1]
|
245
|
-
)
|
246
|
-
return return_type
|
247
|
-
|
248
|
-
def get_input_types(self) -> Dict[str, Any]:
|
249
|
-
if not isinstance(self.indexify_function, IndexifyFunction):
|
250
|
-
raise TypeError("Input must be an instance of IndexifyFunction")
|
251
|
-
|
252
|
-
extract_method = self.indexify_function.run
|
253
|
-
type_hints = get_type_hints(extract_method)
|
254
|
-
return {
|
255
|
-
k: v
|
256
|
-
for k, v in type_hints.items()
|
257
|
-
if k != "return" and not is_pydantic_model_from_annotation(v)
|
258
|
-
}
|
259
|
-
|
260
|
-
def run_router(
|
261
|
-
self, input: Union[Dict, Type[BaseModel]]
|
262
|
-
) -> Tuple[List[str], Optional[str]]:
|
263
|
-
args = []
|
264
|
-
kwargs = {}
|
265
|
-
try:
|
266
|
-
# tuple and list are considered positional arguments, list is used for compatibility
|
267
|
-
# with json encoding which won't deserialize in tuple.
|
268
|
-
if isinstance(input, tuple) or isinstance(input, list):
|
269
|
-
args += input
|
270
|
-
elif isinstance(input, dict):
|
271
|
-
kwargs.update(input)
|
272
|
-
else:
|
273
|
-
args.append(input)
|
274
|
-
extracted_data = self.indexify_function._call_run(*args, **kwargs)
|
275
|
-
except Exception as e:
|
276
|
-
return [], traceback.format_exc()
|
277
|
-
if not isinstance(extracted_data, list) and extracted_data is not None:
|
278
|
-
return [extracted_data.name], None
|
279
|
-
edges = []
|
280
|
-
for fn in extracted_data or []:
|
281
|
-
edges.append(fn.name)
|
282
|
-
return edges, None
|
283
|
-
|
284
|
-
def run_fn(
|
285
|
-
self, input: Union[Dict, Type[BaseModel], List, Tuple], acc: Type[Any] = None
|
286
|
-
) -> Tuple[List[Any], Optional[str]]:
|
287
|
-
args = []
|
288
|
-
kwargs = {}
|
289
|
-
|
290
|
-
if acc is not None:
|
291
|
-
args.append(acc)
|
292
|
-
|
293
|
-
# tuple and list are considered positional arguments, list is used for compatibility
|
294
|
-
# with json encoding which won't deserialize in tuple.
|
295
|
-
if isinstance(input, tuple) or isinstance(input, list):
|
296
|
-
args += input
|
297
|
-
elif isinstance(input, dict):
|
298
|
-
kwargs.update(input)
|
299
|
-
else:
|
300
|
-
args.append(input)
|
301
|
-
|
302
|
-
try:
|
303
|
-
extracted_data = self.indexify_function._call_run(*args, **kwargs)
|
304
|
-
except Exception as e:
|
305
|
-
return [], traceback.format_exc()
|
306
|
-
if extracted_data is None:
|
307
|
-
return [], None
|
308
|
-
|
309
|
-
output = (
|
310
|
-
extracted_data if isinstance(extracted_data, list) else [extracted_data]
|
311
|
-
)
|
312
|
-
return output, None
|
313
|
-
|
314
|
-
def invoke_fn_ser(
|
315
|
-
self, name: str, input: IndexifyData, acc: Optional[Any] = None
|
316
|
-
) -> FunctionCallResult:
|
317
|
-
input = self.deserialize_input(name, input)
|
318
|
-
input_serializer = get_serializer(self.indexify_function.input_encoder)
|
319
|
-
output_serializer = get_serializer(self.indexify_function.output_encoder)
|
320
|
-
if acc is not None:
|
321
|
-
acc = input_serializer.deserialize(acc.payload)
|
322
|
-
if acc is None and self.indexify_function.accumulate is not None:
|
323
|
-
acc = self.indexify_function.accumulate()
|
324
|
-
outputs, err = self.run_fn(input, acc=acc)
|
325
|
-
ser_outputs = [
|
326
|
-
IndexifyData(
|
327
|
-
payload=output_serializer.serialize(output),
|
328
|
-
encoder=self.indexify_function.output_encoder,
|
329
|
-
)
|
330
|
-
for output in outputs
|
331
|
-
]
|
332
|
-
return FunctionCallResult(ser_outputs=ser_outputs, traceback_msg=err)
|
333
|
-
|
334
|
-
def invoke_router(self, name: str, input: IndexifyData) -> RouterCallResult:
|
335
|
-
input = self.deserialize_input(name, input)
|
336
|
-
edges, err = self.run_router(input)
|
337
|
-
return RouterCallResult(edges=edges, traceback_msg=err)
|
338
|
-
|
339
|
-
def deserialize_input(self, compute_fn: str, indexify_data: IndexifyData) -> Any:
|
340
|
-
encoder = indexify_data.encoder
|
341
|
-
payload = indexify_data.payload
|
342
|
-
serializer = get_serializer(encoder)
|
343
|
-
return serializer.deserialize(payload)
|
344
|
-
|
345
|
-
|
346
|
-
def get_ctx() -> GraphInvocationContext:
|
347
|
-
frame = inspect.currentframe()
|
348
|
-
caller_frame = frame.f_back.f_back
|
349
|
-
function_instance = caller_frame.f_locals["self"]
|
350
|
-
del frame
|
351
|
-
del caller_frame
|
352
|
-
if isinstance(function_instance, IndexifyFunctionWrapper):
|
353
|
-
return function_instance.indexify_function._ctx
|
354
|
-
return function_instance._ctx
|
@@ -1,22 +0,0 @@
|
|
1
|
-
from typing import Any, Optional
|
2
|
-
|
3
|
-
|
4
|
-
class InvocationState:
|
5
|
-
"""Abstract interface for Graph invocation state key-value API.
|
6
|
-
|
7
|
-
The API allows to set and get key-value pairs from Indexify functions.
|
8
|
-
The key-value pairs are scoped per Graph invocation.
|
9
|
-
Each new invocation starts with an empty state (empty set of key-value pairs).
|
10
|
-
A value can be any CloudPickleSerializer serializable object."""
|
11
|
-
|
12
|
-
def set(self, key: str, value: Any) -> None:
|
13
|
-
"""Set a key-value pair.
|
14
|
-
|
15
|
-
Raises Exception if an error occured."""
|
16
|
-
raise NotImplementedError()
|
17
|
-
|
18
|
-
def get(self, key: str, default: Optional[Any] = None) -> Optional[Any]:
|
19
|
-
"""Get a value by key. If the key does not exist, return the default value.
|
20
|
-
|
21
|
-
Raises Exception if an error occured."""
|
22
|
-
raise NotImplementedError()
|
@@ -1,30 +0,0 @@
|
|
1
|
-
from typing import Any, Dict, Optional
|
2
|
-
|
3
|
-
from ..object_serializer import CloudPickleSerializer
|
4
|
-
from .invocation_state import InvocationState
|
5
|
-
|
6
|
-
|
7
|
-
class LocalInvocationState(InvocationState):
|
8
|
-
"""InvocationState that stores the key-value pairs in memory.
|
9
|
-
|
10
|
-
This is intended to be used with local graphs."""
|
11
|
-
|
12
|
-
def __init__(self):
|
13
|
-
"""Creates a new instance.
|
14
|
-
|
15
|
-
Caller needs to ensure that the returned instance is only used for a single invocation state.
|
16
|
-
"""
|
17
|
-
self._state: Dict[str, bytes] = {}
|
18
|
-
|
19
|
-
def set(self, key: str, value: Any) -> None:
|
20
|
-
# It's important to serialize the value even in the local implementation
|
21
|
-
# so there are no unexpected errors when running in remote graph mode.
|
22
|
-
self._state[key] = CloudPickleSerializer.serialize(value)
|
23
|
-
|
24
|
-
def get(self, key: str, default: Optional[Any] = None) -> Optional[Any]:
|
25
|
-
serialized_value: Optional[bytes] = self._state.get(key, None)
|
26
|
-
return (
|
27
|
-
default
|
28
|
-
if serialized_value is None
|
29
|
-
else CloudPickleSerializer.deserialize(serialized_value)
|
30
|
-
)
|
@@ -1,68 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
from typing import Any, List, Type
|
3
|
-
|
4
|
-
import cloudpickle
|
5
|
-
|
6
|
-
|
7
|
-
def get_serializer(serializer_type: str) -> Any:
|
8
|
-
if serializer_type == "cloudpickle":
|
9
|
-
return CloudPickleSerializer()
|
10
|
-
elif serializer_type == "json":
|
11
|
-
return JsonSerializer()
|
12
|
-
elif serializer_type == JsonSerializer.content_type:
|
13
|
-
return JsonSerializer()
|
14
|
-
elif serializer_type == CloudPickleSerializer.content_type:
|
15
|
-
return CloudPickleSerializer()
|
16
|
-
raise ValueError(f"Unknown serializer type: {serializer_type}")
|
17
|
-
|
18
|
-
|
19
|
-
class JsonSerializer:
|
20
|
-
content_type = "application/json"
|
21
|
-
encoding_type = "json"
|
22
|
-
|
23
|
-
@staticmethod
|
24
|
-
def serialize(data: Any) -> str:
|
25
|
-
try:
|
26
|
-
return json.dumps(data)
|
27
|
-
except Exception as e:
|
28
|
-
raise ValueError(f"failed to serialize data with json: {e}")
|
29
|
-
|
30
|
-
@staticmethod
|
31
|
-
def deserialize(data: str) -> Any:
|
32
|
-
try:
|
33
|
-
if isinstance(data, bytes):
|
34
|
-
data = data.decode("utf-8")
|
35
|
-
return json.loads(data)
|
36
|
-
except Exception as e:
|
37
|
-
raise ValueError(f"failed to deserialize data with json: {e}")
|
38
|
-
|
39
|
-
@staticmethod
|
40
|
-
def serialize_list(data: List[Any]) -> str:
|
41
|
-
return json.dumps(data)
|
42
|
-
|
43
|
-
@staticmethod
|
44
|
-
def deserialize_list(data: str, t: Type) -> List[Any]:
|
45
|
-
if isinstance(data, bytes):
|
46
|
-
data = data.decode("utf-8")
|
47
|
-
return json.loads(data)
|
48
|
-
|
49
|
-
|
50
|
-
class CloudPickleSerializer:
|
51
|
-
content_type = "application/octet-stream"
|
52
|
-
encoding_type = "cloudpickle"
|
53
|
-
|
54
|
-
@staticmethod
|
55
|
-
def serialize(data: Any) -> bytes:
|
56
|
-
return cloudpickle.dumps(data)
|
57
|
-
|
58
|
-
@staticmethod
|
59
|
-
def deserialize(data: bytes) -> Any:
|
60
|
-
return cloudpickle.loads(data)
|
61
|
-
|
62
|
-
@staticmethod
|
63
|
-
def serialize_list(data: List[Any]) -> bytes:
|
64
|
-
return cloudpickle.dumps(data)
|
65
|
-
|
66
|
-
@staticmethod
|
67
|
-
def deserialize_list(data: bytes) -> List[Any]:
|
68
|
-
return cloudpickle.loads(data)
|