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.
Files changed (42) hide show
  1. indexify/__init__.py +2 -0
  2. indexify/cli.py +41 -80
  3. indexify/executor/api_objects.py +2 -0
  4. indexify/executor/downloader.py +23 -25
  5. indexify/executor/executor.py +29 -35
  6. indexify/executor/function_executor/function_executor.py +120 -19
  7. indexify/executor/function_executor/function_executor_state.py +75 -0
  8. indexify/executor/function_executor/invocation_state_client.py +232 -0
  9. indexify/executor/function_executor/server/function_executor_server.py +24 -0
  10. indexify/executor/function_executor/server/function_executor_server_factory.py +43 -0
  11. indexify/executor/function_executor/server/subprocess_function_executor_server.py +25 -0
  12. indexify/executor/function_executor/{process_function_executor_factory.py → server/subprocess_function_executor_server_factory.py} +21 -21
  13. indexify/executor/function_executor/single_task_runner.py +160 -0
  14. indexify/executor/function_executor/task_input.py +23 -0
  15. indexify/executor/function_executor/task_output.py +36 -0
  16. indexify/executor/task_reporter.py +10 -17
  17. indexify/executor/task_runner.py +104 -0
  18. indexify/function_executor/function_executor_service.py +22 -7
  19. indexify/function_executor/handlers/run_function/handler.py +13 -12
  20. indexify/function_executor/invocation_state/invocation_state_proxy_server.py +170 -0
  21. indexify/function_executor/invocation_state/proxied_invocation_state.py +24 -0
  22. indexify/function_executor/invocation_state/response_validator.py +29 -0
  23. indexify/function_executor/proto/function_executor.proto +47 -0
  24. indexify/function_executor/proto/function_executor_pb2.py +23 -11
  25. indexify/function_executor/proto/function_executor_pb2.pyi +70 -0
  26. indexify/function_executor/proto/function_executor_pb2_grpc.py +50 -0
  27. indexify/functions_sdk/graph.py +3 -3
  28. indexify/functions_sdk/image.py +142 -9
  29. indexify/functions_sdk/indexify_functions.py +45 -79
  30. indexify/functions_sdk/invocation_state/invocation_state.py +22 -0
  31. indexify/functions_sdk/invocation_state/local_invocation_state.py +30 -0
  32. indexify/http_client.py +0 -17
  33. {indexify-0.2.44.dist-info → indexify-0.2.45.dist-info}/METADATA +1 -1
  34. indexify-0.2.45.dist-info/RECORD +60 -0
  35. indexify/executor/function_executor/function_executor_factory.py +0 -26
  36. indexify/executor/function_executor/function_executor_map.py +0 -91
  37. indexify/executor/function_executor/process_function_executor.py +0 -64
  38. indexify/executor/function_worker.py +0 -253
  39. indexify-0.2.44.dist-info/RECORD +0 -50
  40. {indexify-0.2.44.dist-info → indexify-0.2.45.dist-info}/LICENSE.txt +0 -0
  41. {indexify-0.2.44.dist-info → indexify-0.2.45.dist-info}/WHEEL +0 -0
  42. {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,
@@ -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
- ctx = GraphInvocationContext(
240
+ self._local_graph_ctx = GraphInvocationContext(
240
241
  invocation_id=input.id,
241
242
  graph_name=self.name,
242
243
  graph_version="1",
243
- indexify_client=None,
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
 
@@ -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
- tag: str
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._run_strs = []
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._run_strs.append(run_str)
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
- hash.update("".join(self._run_strs).encode())
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, Field, PrivateAttr
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(BaseModel):
24
- invocation_id: str
25
- graph_name: str
26
- graph_version: str
27
- indexify_client: Optional[Any] = Field(default=None) # avoids circular import
28
- _local_state: Dict[str, Any] = PrivateAttr(default_factory=dict)
29
-
30
- def set_state_key(self, key: str, value: Any) -> None:
31
- if self.indexify_client is None:
32
- self._local_state[key] = value
33
- return
34
- self.indexify_client.set_state_key(
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 partial(self, **kwargs) -> Callable:
93
- from functools import partial
94
-
95
- return partial(self.run, **kwargs)
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
- from inspect import Parameter, signature
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": 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": 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.run(*args, **kwargs)
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.run(*args, **kwargs)
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: indexify
3
- Version: 0.2.44
3
+ Version: 0.2.45
4
4
  Summary: Python Client for Indexify
5
5
  Home-page: https://github.com/tensorlakeai/indexify
6
6
  License: Apache 2.0