indexify 0.2.44__py3-none-any.whl → 0.2.45__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/__init__.py +2 -0
- indexify/cli.py +41 -80
- indexify/executor/api_objects.py +2 -0
- indexify/executor/downloader.py +23 -25
- indexify/executor/executor.py +29 -35
- indexify/executor/function_executor/function_executor.py +120 -19
- indexify/executor/function_executor/function_executor_state.py +75 -0
- indexify/executor/function_executor/invocation_state_client.py +232 -0
- indexify/executor/function_executor/server/function_executor_server.py +24 -0
- indexify/executor/function_executor/server/function_executor_server_factory.py +43 -0
- indexify/executor/function_executor/server/subprocess_function_executor_server.py +25 -0
- indexify/executor/function_executor/{process_function_executor_factory.py → server/subprocess_function_executor_server_factory.py} +21 -21
- indexify/executor/function_executor/single_task_runner.py +160 -0
- indexify/executor/function_executor/task_input.py +23 -0
- indexify/executor/function_executor/task_output.py +36 -0
- indexify/executor/task_reporter.py +10 -17
- indexify/executor/task_runner.py +104 -0
- indexify/function_executor/function_executor_service.py +22 -7
- indexify/function_executor/handlers/run_function/handler.py +13 -12
- indexify/function_executor/invocation_state/invocation_state_proxy_server.py +170 -0
- indexify/function_executor/invocation_state/proxied_invocation_state.py +24 -0
- indexify/function_executor/invocation_state/response_validator.py +29 -0
- indexify/function_executor/proto/function_executor.proto +47 -0
- indexify/function_executor/proto/function_executor_pb2.py +23 -11
- indexify/function_executor/proto/function_executor_pb2.pyi +70 -0
- indexify/function_executor/proto/function_executor_pb2_grpc.py +50 -0
- indexify/functions_sdk/graph.py +3 -3
- indexify/functions_sdk/image.py +142 -9
- indexify/functions_sdk/indexify_functions.py +45 -79
- indexify/functions_sdk/invocation_state/invocation_state.py +22 -0
- indexify/functions_sdk/invocation_state/local_invocation_state.py +30 -0
- indexify/http_client.py +0 -17
- {indexify-0.2.44.dist-info → indexify-0.2.45.dist-info}/METADATA +1 -1
- indexify-0.2.45.dist-info/RECORD +60 -0
- indexify/executor/function_executor/function_executor_factory.py +0 -26
- indexify/executor/function_executor/function_executor_map.py +0 -91
- indexify/executor/function_executor/process_function_executor.py +0 -64
- indexify/executor/function_worker.py +0 -253
- indexify-0.2.44.dist-info/RECORD +0 -50
- {indexify-0.2.44.dist-info → indexify-0.2.45.dist-info}/LICENSE.txt +0 -0
- {indexify-0.2.44.dist-info → indexify-0.2.45.dist-info}/WHEEL +0 -0
- {indexify-0.2.44.dist-info → indexify-0.2.45.dist-info}/entry_points.txt +0 -0
@@ -52,6 +52,76 @@ class InitializeResponse(_message.Message):
|
|
52
52
|
success: bool
|
53
53
|
def __init__(self, success: bool = ...) -> None: ...
|
54
54
|
|
55
|
+
class SetInvocationStateRequest(_message.Message):
|
56
|
+
__slots__ = ("key", "value")
|
57
|
+
KEY_FIELD_NUMBER: _ClassVar[int]
|
58
|
+
VALUE_FIELD_NUMBER: _ClassVar[int]
|
59
|
+
key: str
|
60
|
+
value: SerializedObject
|
61
|
+
def __init__(
|
62
|
+
self,
|
63
|
+
key: _Optional[str] = ...,
|
64
|
+
value: _Optional[_Union[SerializedObject, _Mapping]] = ...,
|
65
|
+
) -> None: ...
|
66
|
+
|
67
|
+
class SetInvocationStateResponse(_message.Message):
|
68
|
+
__slots__ = ()
|
69
|
+
def __init__(self) -> None: ...
|
70
|
+
|
71
|
+
class GetInvocationStateRequest(_message.Message):
|
72
|
+
__slots__ = ("key",)
|
73
|
+
KEY_FIELD_NUMBER: _ClassVar[int]
|
74
|
+
key: str
|
75
|
+
def __init__(self, key: _Optional[str] = ...) -> None: ...
|
76
|
+
|
77
|
+
class GetInvocationStateResponse(_message.Message):
|
78
|
+
__slots__ = ("key", "value")
|
79
|
+
KEY_FIELD_NUMBER: _ClassVar[int]
|
80
|
+
VALUE_FIELD_NUMBER: _ClassVar[int]
|
81
|
+
key: str
|
82
|
+
value: SerializedObject
|
83
|
+
def __init__(
|
84
|
+
self,
|
85
|
+
key: _Optional[str] = ...,
|
86
|
+
value: _Optional[_Union[SerializedObject, _Mapping]] = ...,
|
87
|
+
) -> None: ...
|
88
|
+
|
89
|
+
class InvocationStateRequest(_message.Message):
|
90
|
+
__slots__ = ("request_id", "task_id", "set", "get")
|
91
|
+
REQUEST_ID_FIELD_NUMBER: _ClassVar[int]
|
92
|
+
TASK_ID_FIELD_NUMBER: _ClassVar[int]
|
93
|
+
SET_FIELD_NUMBER: _ClassVar[int]
|
94
|
+
GET_FIELD_NUMBER: _ClassVar[int]
|
95
|
+
request_id: str
|
96
|
+
task_id: str
|
97
|
+
set: SetInvocationStateRequest
|
98
|
+
get: GetInvocationStateRequest
|
99
|
+
def __init__(
|
100
|
+
self,
|
101
|
+
request_id: _Optional[str] = ...,
|
102
|
+
task_id: _Optional[str] = ...,
|
103
|
+
set: _Optional[_Union[SetInvocationStateRequest, _Mapping]] = ...,
|
104
|
+
get: _Optional[_Union[GetInvocationStateRequest, _Mapping]] = ...,
|
105
|
+
) -> None: ...
|
106
|
+
|
107
|
+
class InvocationStateResponse(_message.Message):
|
108
|
+
__slots__ = ("request_id", "success", "set", "get")
|
109
|
+
REQUEST_ID_FIELD_NUMBER: _ClassVar[int]
|
110
|
+
SUCCESS_FIELD_NUMBER: _ClassVar[int]
|
111
|
+
SET_FIELD_NUMBER: _ClassVar[int]
|
112
|
+
GET_FIELD_NUMBER: _ClassVar[int]
|
113
|
+
request_id: str
|
114
|
+
success: bool
|
115
|
+
set: SetInvocationStateResponse
|
116
|
+
get: GetInvocationStateResponse
|
117
|
+
def __init__(
|
118
|
+
self,
|
119
|
+
request_id: _Optional[str] = ...,
|
120
|
+
success: bool = ...,
|
121
|
+
set: _Optional[_Union[SetInvocationStateResponse, _Mapping]] = ...,
|
122
|
+
get: _Optional[_Union[GetInvocationStateResponse, _Mapping]] = ...,
|
123
|
+
) -> None: ...
|
124
|
+
|
55
125
|
class FunctionOutput(_message.Message):
|
56
126
|
__slots__ = ("outputs",)
|
57
127
|
OUTPUTS_FIELD_NUMBER: _ClassVar[int]
|
@@ -46,6 +46,12 @@ class FunctionExecutorStub(object):
|
|
46
46
|
response_deserializer=indexify_dot_function__executor_dot_proto_dot_function__executor__pb2.InitializeResponse.FromString,
|
47
47
|
_registered_method=True,
|
48
48
|
)
|
49
|
+
self.initialize_invocation_state_server = channel.stream_stream(
|
50
|
+
"/function_executor_service.FunctionExecutor/initialize_invocation_state_server",
|
51
|
+
request_serializer=indexify_dot_function__executor_dot_proto_dot_function__executor__pb2.InvocationStateResponse.SerializeToString,
|
52
|
+
response_deserializer=indexify_dot_function__executor_dot_proto_dot_function__executor__pb2.InvocationStateRequest.FromString,
|
53
|
+
_registered_method=True,
|
54
|
+
)
|
49
55
|
self.run_task = channel.unary_unary(
|
50
56
|
"/function_executor_service.FunctionExecutor/run_task",
|
51
57
|
request_serializer=indexify_dot_function__executor_dot_proto_dot_function__executor__pb2.RunTaskRequest.SerializeToString,
|
@@ -67,6 +73,15 @@ class FunctionExecutorServicer(object):
|
|
67
73
|
context.set_details("Method not implemented!")
|
68
74
|
raise NotImplementedError("Method not implemented!")
|
69
75
|
|
76
|
+
def initialize_invocation_state_server(self, request_iterator, context):
|
77
|
+
"""Initializes a server that sends requests to the client to perform actions on
|
78
|
+
a task's graph invocation state. This method is called only once per Function Executor
|
79
|
+
It should be called before calling RunTask for the function.
|
80
|
+
"""
|
81
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
82
|
+
context.set_details("Method not implemented!")
|
83
|
+
raise NotImplementedError("Method not implemented!")
|
84
|
+
|
70
85
|
def run_task(self, request, context):
|
71
86
|
"""Executes the task defined in the request.
|
72
87
|
Multiple tasks can be running in parallel.
|
@@ -83,6 +98,11 @@ def add_FunctionExecutorServicer_to_server(servicer, server):
|
|
83
98
|
request_deserializer=indexify_dot_function__executor_dot_proto_dot_function__executor__pb2.InitializeRequest.FromString,
|
84
99
|
response_serializer=indexify_dot_function__executor_dot_proto_dot_function__executor__pb2.InitializeResponse.SerializeToString,
|
85
100
|
),
|
101
|
+
"initialize_invocation_state_server": grpc.stream_stream_rpc_method_handler(
|
102
|
+
servicer.initialize_invocation_state_server,
|
103
|
+
request_deserializer=indexify_dot_function__executor_dot_proto_dot_function__executor__pb2.InvocationStateResponse.FromString,
|
104
|
+
response_serializer=indexify_dot_function__executor_dot_proto_dot_function__executor__pb2.InvocationStateRequest.SerializeToString,
|
105
|
+
),
|
86
106
|
"run_task": grpc.unary_unary_rpc_method_handler(
|
87
107
|
servicer.run_task,
|
88
108
|
request_deserializer=indexify_dot_function__executor_dot_proto_dot_function__executor__pb2.RunTaskRequest.FromString,
|
@@ -132,6 +152,36 @@ class FunctionExecutor(object):
|
|
132
152
|
_registered_method=True,
|
133
153
|
)
|
134
154
|
|
155
|
+
@staticmethod
|
156
|
+
def initialize_invocation_state_server(
|
157
|
+
request_iterator,
|
158
|
+
target,
|
159
|
+
options=(),
|
160
|
+
channel_credentials=None,
|
161
|
+
call_credentials=None,
|
162
|
+
insecure=False,
|
163
|
+
compression=None,
|
164
|
+
wait_for_ready=None,
|
165
|
+
timeout=None,
|
166
|
+
metadata=None,
|
167
|
+
):
|
168
|
+
return grpc.experimental.stream_stream(
|
169
|
+
request_iterator,
|
170
|
+
target,
|
171
|
+
"/function_executor_service.FunctionExecutor/initialize_invocation_state_server",
|
172
|
+
indexify_dot_function__executor_dot_proto_dot_function__executor__pb2.InvocationStateResponse.SerializeToString,
|
173
|
+
indexify_dot_function__executor_dot_proto_dot_function__executor__pb2.InvocationStateRequest.FromString,
|
174
|
+
options,
|
175
|
+
channel_credentials,
|
176
|
+
insecure,
|
177
|
+
call_credentials,
|
178
|
+
compression,
|
179
|
+
wait_for_ready,
|
180
|
+
timeout,
|
181
|
+
metadata,
|
182
|
+
_registered_method=True,
|
183
|
+
)
|
184
|
+
|
135
185
|
@staticmethod
|
136
186
|
def run_task(
|
137
187
|
request,
|
indexify/functions_sdk/graph.py
CHANGED
@@ -37,6 +37,7 @@ from .indexify_functions import (
|
|
37
37
|
IndexifyRouter,
|
38
38
|
RouterCallResult,
|
39
39
|
)
|
40
|
+
from .invocation_state.local_invocation_state import LocalInvocationState
|
40
41
|
from .object_serializer import get_serializer
|
41
42
|
|
42
43
|
RouterFn = Annotated[
|
@@ -236,13 +237,12 @@ class Graph:
|
|
236
237
|
payload=serializer.serialize(v), encoder=node.input_encoder
|
237
238
|
)
|
238
239
|
self._results[input.id] = outputs
|
239
|
-
|
240
|
+
self._local_graph_ctx = GraphInvocationContext(
|
240
241
|
invocation_id=input.id,
|
241
242
|
graph_name=self.name,
|
242
243
|
graph_version="1",
|
243
|
-
|
244
|
+
invocation_state=LocalInvocationState(),
|
244
245
|
)
|
245
|
-
self._local_graph_ctx = ctx
|
246
246
|
self._run(input, outputs)
|
247
247
|
return input.id
|
248
248
|
|
indexify/functions_sdk/image.py
CHANGED
@@ -1,20 +1,88 @@
|
|
1
|
+
import datetime
|
1
2
|
import hashlib
|
2
3
|
import importlib
|
4
|
+
import logging
|
5
|
+
import os
|
6
|
+
import pathlib
|
3
7
|
import sys
|
8
|
+
import tarfile
|
9
|
+
from io import BytesIO
|
4
10
|
from typing import List, Optional
|
5
11
|
|
12
|
+
import docker
|
13
|
+
import docker.api.build
|
6
14
|
from pydantic import BaseModel
|
7
15
|
|
8
16
|
|
9
17
|
# Pydantic object for API
|
10
18
|
class ImageInformation(BaseModel):
|
11
19
|
image_name: str
|
12
|
-
|
13
|
-
base_image: str
|
14
|
-
run_strs: List[str]
|
20
|
+
image_hash: str
|
15
21
|
image_url: Optional[str] = ""
|
16
22
|
sdk_version: str
|
17
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
|
+
args: List[str]
|
36
|
+
|
37
|
+
def hash(self, hash):
|
38
|
+
match self.op_type:
|
39
|
+
case "RUN":
|
40
|
+
hash.update("RUN".encode())
|
41
|
+
for a in self.args:
|
42
|
+
hash.update(a.encode())
|
43
|
+
|
44
|
+
case "COPY":
|
45
|
+
hash.update("COPY".encode())
|
46
|
+
for root, dirs, files in os.walk(self.args[0]):
|
47
|
+
for file in files:
|
48
|
+
filename = pathlib.Path(root, file)
|
49
|
+
with open(filename, "rb") as fp:
|
50
|
+
data = fp.read(HASH_BUFF_SIZE)
|
51
|
+
while data:
|
52
|
+
hash.update(data)
|
53
|
+
data = fp.read(HASH_BUFF_SIZE)
|
54
|
+
|
55
|
+
case _:
|
56
|
+
raise ValueError(f"Unsupported build op type {self.op_type}")
|
57
|
+
|
58
|
+
def render(self):
|
59
|
+
match self.op_type:
|
60
|
+
case "RUN":
|
61
|
+
return f"RUN {''.join(self.args)}"
|
62
|
+
case "COPY":
|
63
|
+
return f"COPY {self.args[0]} {self.args[1]}"
|
64
|
+
case _:
|
65
|
+
raise ValueError(f"Unsupported build op type {self.op_type}")
|
66
|
+
|
67
|
+
|
68
|
+
class Build(BaseModel):
|
69
|
+
"""
|
70
|
+
Model for talking with the build service.
|
71
|
+
"""
|
72
|
+
|
73
|
+
id: int | None = None
|
74
|
+
namespace: str
|
75
|
+
image_name: str
|
76
|
+
image_hash: str
|
77
|
+
status: str | None
|
78
|
+
result: str | None
|
79
|
+
|
80
|
+
created_at: datetime.datetime | None
|
81
|
+
started_at: datetime.datetime | None = None
|
82
|
+
build_completed_at: datetime.datetime | None = None
|
83
|
+
push_completed_at: datetime.datetime | None = None
|
84
|
+
uri: str | None = None
|
85
|
+
|
18
86
|
|
19
87
|
class Image:
|
20
88
|
def __init__(self):
|
@@ -22,7 +90,7 @@ class Image:
|
|
22
90
|
self._tag = "latest"
|
23
91
|
self._base_image = BASE_IMAGE_NAME
|
24
92
|
self._python_version = LOCAL_PYTHON_VERSION
|
25
|
-
self.
|
93
|
+
self._build_ops = [] # List of ImageOperation
|
26
94
|
self._sdk_version = importlib.metadata.version("indexify")
|
27
95
|
|
28
96
|
def name(self, image_name):
|
@@ -38,16 +106,79 @@ class Image:
|
|
38
106
|
return self
|
39
107
|
|
40
108
|
def run(self, run_str):
|
41
|
-
self.
|
109
|
+
self._build_ops.append(BuildOp(op_type="RUN", args=[run_str]))
|
110
|
+
return self
|
111
|
+
|
112
|
+
def copy(self, source: str, dest: str):
|
113
|
+
self._build_ops.append(BuildOp(op_type="COPY", args=[source, dest]))
|
42
114
|
return self
|
43
115
|
|
44
116
|
def to_image_information(self):
|
45
117
|
return ImageInformation(
|
46
118
|
image_name=self._image_name,
|
47
|
-
tag=self._tag,
|
48
|
-
base_image=self._base_image,
|
49
|
-
run_strs=self._run_strs,
|
50
119
|
sdk_version=self._sdk_version,
|
120
|
+
image_hash=self.hash(),
|
121
|
+
)
|
122
|
+
|
123
|
+
def build_context(self, filename: str):
|
124
|
+
with tarfile.open(filename, "w:gz") as tf:
|
125
|
+
for op in self._build_ops:
|
126
|
+
if op.op_type == "COPY":
|
127
|
+
src = op.args[0]
|
128
|
+
logging.info(f"Adding {src}")
|
129
|
+
tf.add(src, src)
|
130
|
+
|
131
|
+
dockerfile = self._generate_dockerfile()
|
132
|
+
tarinfo = tarfile.TarInfo("Dockerfile")
|
133
|
+
tarinfo.size = len(dockerfile)
|
134
|
+
|
135
|
+
tf.addfile(tarinfo, BytesIO(dockerfile.encode()))
|
136
|
+
|
137
|
+
def _generate_dockerfile(self, python_sdk_path: Optional[str] = None):
|
138
|
+
docker_contents = [
|
139
|
+
f"FROM {self._base_image}",
|
140
|
+
"RUN mkdir -p ~/.indexify",
|
141
|
+
f"RUN echo {self._image_name} > ~/.indexify/image_name",
|
142
|
+
f"RUN echo {self.hash()} > ~/.indexify/image_hash",
|
143
|
+
"WORKDIR /app",
|
144
|
+
]
|
145
|
+
|
146
|
+
for build_op in self._build_ops:
|
147
|
+
docker_contents.append(build_op.render())
|
148
|
+
|
149
|
+
if python_sdk_path is not None:
|
150
|
+
logging.info(
|
151
|
+
f"Building image {self._image_name} with local version of the SDK"
|
152
|
+
)
|
153
|
+
if not os.path.exists(python_sdk_path):
|
154
|
+
print(f"error: {python_sdk_path} does not exist")
|
155
|
+
os.exit(1)
|
156
|
+
docker_contents.append(f"COPY {python_sdk_path} /app/python-sdk")
|
157
|
+
docker_contents.append("RUN (cd /app/python-sdk && pip install .)")
|
158
|
+
else:
|
159
|
+
docker_contents.append(f"RUN pip install indexify=={self._sdk_version}")
|
160
|
+
|
161
|
+
docker_file = "\n".join(docker_contents)
|
162
|
+
return docker_file
|
163
|
+
|
164
|
+
def build(self, python_sdk_path: Optional[str] = None, docker_client=None):
|
165
|
+
if docker_client is None:
|
166
|
+
docker_client = docker.from_env()
|
167
|
+
docker_client.ping()
|
168
|
+
|
169
|
+
docker_file = self._generate_dockerfile(python_sdk_path=python_sdk_path)
|
170
|
+
image_name = f"{self._image_name}:{self._tag}"
|
171
|
+
|
172
|
+
docker.api.build.process_dockerfile = lambda dockerfile, path: (
|
173
|
+
"Dockerfile",
|
174
|
+
dockerfile,
|
175
|
+
)
|
176
|
+
|
177
|
+
return docker_client.images.build(
|
178
|
+
path=".",
|
179
|
+
dockerfile=docker_file,
|
180
|
+
tag=image_name,
|
181
|
+
rm=True,
|
51
182
|
)
|
52
183
|
|
53
184
|
def hash(self) -> str:
|
@@ -55,7 +186,9 @@ class Image:
|
|
55
186
|
self._image_name.encode()
|
56
187
|
) # Make a hash of the image name
|
57
188
|
hash.update(self._base_image.encode())
|
58
|
-
|
189
|
+
for op in self._build_ops:
|
190
|
+
op.hash(hash)
|
191
|
+
|
59
192
|
hash.update(self._sdk_version.encode())
|
60
193
|
|
61
194
|
return hash.hexdigest()
|
@@ -1,8 +1,8 @@
|
|
1
1
|
import inspect
|
2
2
|
import traceback
|
3
|
+
from inspect import Parameter
|
3
4
|
from typing import (
|
4
5
|
Any,
|
5
|
-
Callable,
|
6
6
|
Dict,
|
7
7
|
List,
|
8
8
|
Optional,
|
@@ -13,34 +13,27 @@ from typing import (
|
|
13
13
|
get_origin,
|
14
14
|
)
|
15
15
|
|
16
|
-
from pydantic import BaseModel
|
16
|
+
from pydantic import BaseModel
|
17
|
+
from typing_extensions import get_type_hints
|
17
18
|
|
18
19
|
from .data_objects import IndexifyData
|
19
20
|
from .image import DEFAULT_IMAGE, Image
|
21
|
+
from .invocation_state.invocation_state import InvocationState
|
20
22
|
from .object_serializer import get_serializer
|
21
23
|
|
22
24
|
|
23
|
-
class GraphInvocationContext
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
self.
|
35
|
-
self.graph_name, self.invocation_id, key, value
|
36
|
-
)
|
37
|
-
|
38
|
-
def get_state_key(self, key: str) -> Any:
|
39
|
-
if self.indexify_client is None:
|
40
|
-
return self._local_state.get(key)
|
41
|
-
return self.indexify_client.get_state_key(
|
42
|
-
self.graph_name, self.invocation_id, key
|
43
|
-
)
|
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
|
44
37
|
|
45
38
|
|
46
39
|
def is_pydantic_model_from_annotation(type_annotation):
|
@@ -89,10 +82,22 @@ class IndexifyFunction:
|
|
89
82
|
def run(self, *args, **kwargs) -> Union[List[Any], Any]:
|
90
83
|
pass
|
91
84
|
|
92
|
-
def
|
93
|
-
|
94
|
-
|
95
|
-
|
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)
|
96
101
|
|
97
102
|
@classmethod
|
98
103
|
def deserialize_output(cls, output: IndexifyData) -> Any:
|
@@ -111,10 +116,16 @@ class IndexifyRouter:
|
|
111
116
|
def run(self, *args, **kwargs) -> Optional[List[IndexifyFunction]]:
|
112
117
|
pass
|
113
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)
|
114
127
|
|
115
|
-
|
116
|
-
|
117
|
-
from typing_extensions import get_type_hints
|
128
|
+
return self.run(*args, **kwargs)
|
118
129
|
|
119
130
|
|
120
131
|
def _process_dict_arg(dict_arg: dict, sig: inspect.Signature) -> Tuple[list, dict]:
|
@@ -147,25 +158,6 @@ def indexify_router(
|
|
147
158
|
output_encoder: Optional[str] = "cloudpickle",
|
148
159
|
):
|
149
160
|
def construct(fn):
|
150
|
-
# Get function signature using inspect.signature
|
151
|
-
fn_sig = signature(fn)
|
152
|
-
fn_hints = get_type_hints(fn)
|
153
|
-
|
154
|
-
# Create run method that preserves signature
|
155
|
-
def run(self, *args, **kwargs):
|
156
|
-
# Process dictionary argument mapping it to args or to kwargs.
|
157
|
-
if len(args) == 1 and isinstance(args[0], dict):
|
158
|
-
sig = inspect.signature(fn)
|
159
|
-
dict_arg = args[0]
|
160
|
-
new_args, new_kwargs = _process_dict_arg(dict_arg, sig)
|
161
|
-
return fn(*new_args, **new_kwargs)
|
162
|
-
|
163
|
-
return fn(*args, **kwargs)
|
164
|
-
|
165
|
-
# Apply original signature and annotations to run method
|
166
|
-
run.__signature__ = fn_sig
|
167
|
-
run.__annotations__ = fn_hints
|
168
|
-
|
169
161
|
attrs = {
|
170
162
|
"name": name if name else fn.__name__,
|
171
163
|
"description": (
|
@@ -177,7 +169,7 @@ def indexify_router(
|
|
177
169
|
"placement_constraints": placement_constraints,
|
178
170
|
"input_encoder": input_encoder,
|
179
171
|
"output_encoder": output_encoder,
|
180
|
-
"run":
|
172
|
+
"run": staticmethod(fn),
|
181
173
|
}
|
182
174
|
|
183
175
|
return type("IndexifyRouter", (IndexifyRouter,), attrs)
|
@@ -195,32 +187,6 @@ def indexify_function(
|
|
195
187
|
placement_constraints: List[PlacementConstraints] = [],
|
196
188
|
):
|
197
189
|
def construct(fn):
|
198
|
-
# Get function signature using inspect.signature
|
199
|
-
fn_sig = signature(fn)
|
200
|
-
fn_hints = get_type_hints(fn)
|
201
|
-
|
202
|
-
# Create run method that preserves signature
|
203
|
-
def run(self, *args, **kwargs):
|
204
|
-
# Process dictionary argument mapping it to args or to kwargs.
|
205
|
-
if self.accumulate and len(args) == 2 and isinstance(args[1], dict):
|
206
|
-
sig = inspect.signature(fn)
|
207
|
-
new_args = [args[0]] # Keep the accumulate argument
|
208
|
-
dict_arg = args[1]
|
209
|
-
new_args_from_dict, new_kwargs = _process_dict_arg(dict_arg, sig)
|
210
|
-
new_args.extend(new_args_from_dict)
|
211
|
-
return fn(*new_args, **new_kwargs)
|
212
|
-
elif len(args) == 1 and isinstance(args[0], dict):
|
213
|
-
sig = inspect.signature(fn)
|
214
|
-
dict_arg = args[0]
|
215
|
-
new_args, new_kwargs = _process_dict_arg(dict_arg, sig)
|
216
|
-
return fn(*new_args, **new_kwargs)
|
217
|
-
|
218
|
-
return fn(*args, **kwargs)
|
219
|
-
|
220
|
-
# Apply original signature and annotations to run method
|
221
|
-
run.__signature__ = fn_sig
|
222
|
-
run.__annotations__ = fn_hints
|
223
|
-
|
224
190
|
attrs = {
|
225
191
|
"name": name if name else fn.__name__,
|
226
192
|
"description": (
|
@@ -233,7 +199,7 @@ def indexify_function(
|
|
233
199
|
"accumulate": accumulate,
|
234
200
|
"input_encoder": input_encoder,
|
235
201
|
"output_encoder": output_encoder,
|
236
|
-
"run":
|
202
|
+
"run": staticmethod(fn),
|
237
203
|
}
|
238
204
|
|
239
205
|
return type("IndexifyFunction", (IndexifyFunction,), attrs)
|
@@ -303,7 +269,7 @@ class IndexifyFunctionWrapper:
|
|
303
269
|
args += input
|
304
270
|
else:
|
305
271
|
args.append(input)
|
306
|
-
extracted_data = self.indexify_function.
|
272
|
+
extracted_data = self.indexify_function._call_run(*args, **kwargs)
|
307
273
|
except Exception as e:
|
308
274
|
return [], traceback.format_exc()
|
309
275
|
if not isinstance(extracted_data, list) and extracted_data is not None:
|
@@ -330,7 +296,7 @@ class IndexifyFunctionWrapper:
|
|
330
296
|
args.append(input)
|
331
297
|
|
332
298
|
try:
|
333
|
-
extracted_data = self.indexify_function.
|
299
|
+
extracted_data = self.indexify_function._call_run(*args, **kwargs)
|
334
300
|
except Exception as e:
|
335
301
|
return [], traceback.format_exc()
|
336
302
|
if extracted_data is None:
|
@@ -0,0 +1,22 @@
|
|
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()
|
@@ -0,0 +1,30 @@
|
|
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
|
+
)
|
indexify/http_client.py
CHANGED
@@ -208,23 +208,6 @@ class IndexifyClient:
|
|
208
208
|
namespaces.append(item["name"])
|
209
209
|
return namespaces
|
210
210
|
|
211
|
-
def set_state_key(
|
212
|
-
self, compute_graph: str, invocation_id: str, key: str, value: Json
|
213
|
-
) -> None:
|
214
|
-
response = self._post(
|
215
|
-
f"internal/namespaces/{self.namespace}/compute_graphs/{compute_graph}/invocations/{invocation_id}/ctx",
|
216
|
-
json={"key": key, "value": value},
|
217
|
-
)
|
218
|
-
response.raise_for_status()
|
219
|
-
|
220
|
-
def get_state_key(self, compute_graph: str, invocation_id: str, key: str) -> Json:
|
221
|
-
response = self._get(
|
222
|
-
f"internal/namespaces/{self.namespace}/compute_graphs/{compute_graph}/invocations/{invocation_id}/ctx",
|
223
|
-
json={"key": key},
|
224
|
-
)
|
225
|
-
response.raise_for_status()
|
226
|
-
return response.json().get("value")
|
227
|
-
|
228
211
|
@classmethod
|
229
212
|
def new_namespace(
|
230
213
|
cls, namespace: str, server_addr: Optional[str] = "http://localhost:8900"
|