indexify 0.2.44__py3-none-any.whl → 0.2.46__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.46.dist-info}/METADATA +2 -2
  34. indexify-0.2.46.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.46.dist-info}/LICENSE.txt +0 -0
  41. {indexify-0.2.44.dist-info → indexify-0.2.46.dist-info}/WHEEL +0 -0
  42. {indexify-0.2.44.dist-info → indexify-0.2.46.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,232 @@
1
+ import asyncio
2
+ from typing import Any, AsyncGenerator, Optional, Union
3
+
4
+ import grpc
5
+ import httpx
6
+
7
+ from indexify.executor.downloader import serialized_object_from_http_response
8
+ from indexify.function_executor.proto.function_executor_pb2 import (
9
+ GetInvocationStateResponse,
10
+ InvocationStateRequest,
11
+ InvocationStateResponse,
12
+ SerializedObject,
13
+ SetInvocationStateResponse,
14
+ )
15
+ from indexify.function_executor.proto.function_executor_pb2_grpc import (
16
+ FunctionExecutorStub,
17
+ )
18
+ from indexify.function_executor.proto.message_validator import MessageValidator
19
+
20
+
21
+ class InvocationStateClient:
22
+ """InvocationStateClient is a client for the invocation state server of a Function Executor.
23
+
24
+ The client initializes the Function Executor's invocation state server and executes requests
25
+ it sends to the client.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ stub: FunctionExecutorStub,
31
+ base_url: str,
32
+ http_client: httpx.AsyncClient,
33
+ graph: str,
34
+ namespace: str,
35
+ logger: Any,
36
+ ):
37
+ self._stub: FunctionExecutorStub = stub
38
+ self._base_url: str = base_url
39
+ self._http_client: httpx.AsyncClient = http_client
40
+ self._graph: str = graph
41
+ self._namespace: str = namespace
42
+ self._logger: Any = logger.bind(
43
+ module=__name__, graph=graph, namespace=namespace
44
+ )
45
+ self._client_response_queue: asyncio.Queue[
46
+ Union[InvocationStateResponse, str]
47
+ ] = asyncio.Queue()
48
+ self._task_id_to_invocation_id: dict[str, str] = {}
49
+ self._request_loop_task: Optional[asyncio.Task] = None
50
+
51
+ async def start(self) -> None:
52
+ """Starts the invocation state client.
53
+
54
+ This method initializes the Function Executor's invocation state server first.
55
+ This is why this method needs to be awaited before executing any tasks on the Function Executor
56
+ that might use invocation state feature."""
57
+ server_requests = self._stub.initialize_invocation_state_server(
58
+ self._response_generator()
59
+ )
60
+ self._request_loop_task = asyncio.create_task(
61
+ self._request_loop(server_requests)
62
+ )
63
+
64
+ def add_task_to_invocation_id_entry(self, task_id: str, invocation_id: str) -> None:
65
+ """Adds a task ID to invocation ID entry to the client's internal state.
66
+
67
+ This allows to authorize requests to the invocation state server.
68
+ If a request is not comming from the task ID that was added here then it will
69
+ be rejected. It's caller's responsibility to only add task IDs that are being
70
+ executed by the Function Executor so the Function Executor can't get access to
71
+ invocation state of tasks it doesn't run."""
72
+ self._task_id_to_invocation_id[task_id] = invocation_id
73
+
74
+ def remove_task_to_invocation_id_entry(self, task_id: str) -> None:
75
+ del self._task_id_to_invocation_id[task_id]
76
+
77
+ async def destroy(self) -> None:
78
+ if self._request_loop_task is not None:
79
+ self._request_loop_task.cancel()
80
+ await self._client_response_queue.put("shutdown")
81
+
82
+ async def _request_loop(
83
+ self, server_requests: AsyncGenerator[InvocationStateRequest, None]
84
+ ) -> None:
85
+ try:
86
+ async for request in server_requests:
87
+ await self._process_request_no_raise(request)
88
+ except grpc.aio.AioRpcError:
89
+ # Reading from the stream failed.
90
+ # This is a normal situation when the server is shutting down.
91
+ pass
92
+ except asyncio.CancelledError:
93
+ # This async task was cancelled by destroy(). Normal situation too.
94
+ pass
95
+
96
+ async def _process_request_no_raise(self, request: InvocationStateRequest) -> None:
97
+ try:
98
+ await self._process_request(request)
99
+ except Exception as e:
100
+ try:
101
+ await self._client_response_queue.put(
102
+ InvocationStateResponse(
103
+ request_id=request.request_id,
104
+ success=False,
105
+ )
106
+ )
107
+ except Exception as ee:
108
+ self._logger.error("failed to send error response", exc_info=ee)
109
+
110
+ self._logger.error(
111
+ "failed to process request",
112
+ exc_info=e,
113
+ request_id=request.request_id,
114
+ )
115
+
116
+ async def _process_request(
117
+ self, request: InvocationStateRequest
118
+ ) -> InvocationStateResponse:
119
+ self._validate_request(request)
120
+ # This is a very important check. We don't trust invocation ID and task ID
121
+ # supplied by Function Executor. If a task ID entry doesn't exist then it's
122
+ # a privelege escalation attempt.
123
+ invocation_id: str = self._task_id_to_invocation_id[request.task_id]
124
+ if request.HasField("get"):
125
+ value: Optional[SerializedObject] = await self._get_server_state(
126
+ invocation_id, request.get.key
127
+ )
128
+ await self._client_response_queue.put(
129
+ InvocationStateResponse(
130
+ request_id=request.request_id,
131
+ success=True,
132
+ get=GetInvocationStateResponse(
133
+ key=request.get.key,
134
+ value=value,
135
+ ),
136
+ )
137
+ )
138
+ elif request.HasField("set"):
139
+ await self._set_server_state(
140
+ invocation_id, request.set.key, request.set.value
141
+ )
142
+ await self._client_response_queue.put(
143
+ InvocationStateResponse(
144
+ request_id=request.request_id,
145
+ success=True,
146
+ set=SetInvocationStateResponse(),
147
+ )
148
+ )
149
+
150
+ async def _response_generator(
151
+ self,
152
+ ) -> AsyncGenerator[InvocationStateResponse, None]:
153
+ while True:
154
+ response = await self._client_response_queue.get()
155
+ # Hacky cancellation of the generator.
156
+ if response == "shutdown":
157
+ break
158
+ yield response
159
+
160
+ async def _set_server_state(
161
+ self, invocation_id: str, key: str, value: SerializedObject
162
+ ) -> None:
163
+ url: str = (
164
+ f"{self._base_url}/internal/namespaces/{self._namespace}/compute_graphs/{self._graph}/invocations/{invocation_id}/ctx/{key}"
165
+ )
166
+ payload = value.bytes if value.HasField("bytes") else value.string
167
+
168
+ response = await self._http_client.post(
169
+ url=url,
170
+ files=[
171
+ (
172
+ "value",
173
+ ("value", payload, value.content_type),
174
+ ),
175
+ ],
176
+ )
177
+
178
+ try:
179
+ response.raise_for_status()
180
+ except Exception as e:
181
+ self._logger.error(
182
+ "failed to set graph invocation state",
183
+ invocation_id=invocation_id,
184
+ key=key,
185
+ status_code=response.status_code,
186
+ error=response.text,
187
+ exc_info=e,
188
+ )
189
+ raise
190
+
191
+ async def _get_server_state(
192
+ self, invocation_id: str, key: str
193
+ ) -> Optional[SerializedObject]:
194
+ url: str = (
195
+ f"{self._base_url}/internal/namespaces/{self._namespace}/compute_graphs/{self._graph}/invocations/{invocation_id}/ctx/{key}"
196
+ )
197
+
198
+ response: httpx.Response = await self._http_client.get(url)
199
+ if response.status_code == 404:
200
+ return None
201
+
202
+ try:
203
+ response.raise_for_status()
204
+ except httpx.HTTPStatusError as e:
205
+ self._logger.error(
206
+ f"failed to download graph invocation state value",
207
+ invocation_id=invocation_id,
208
+ key=key,
209
+ status_code=response.status_code,
210
+ error=response.text,
211
+ exc_info=e,
212
+ )
213
+ raise
214
+
215
+ return serialized_object_from_http_response(response)
216
+
217
+ def _validate_request(self, request: InvocationStateRequest) -> None:
218
+ (
219
+ MessageValidator(request)
220
+ .required_field("request_id")
221
+ .required_field("task_id")
222
+ )
223
+ if request.HasField("get"):
224
+ (MessageValidator(request.get).required_field("key"))
225
+ elif request.HasField("set"):
226
+ (
227
+ MessageValidator(request.set)
228
+ .required_field("key")
229
+ .required_serialized_object("value")
230
+ )
231
+ else:
232
+ raise ValueError("unknown request type")
@@ -0,0 +1,24 @@
1
+ from typing import Any
2
+
3
+ import grpc
4
+
5
+ # Timeout for Function Executor Server startup in seconds. The timeout is counted from
6
+ # the moment when a server just started.
7
+ FUNCTION_EXECUTOR_SERVER_READY_TIMEOUT_SEC = 5
8
+
9
+
10
+ class FunctionExecutorServer:
11
+ """Abstract interface for a Function Executor Server.
12
+
13
+ FunctionExecutorServer is a class that executes tasks for a particular function.
14
+ The communication with FunctionExecutorServer is typicall done via gRPC.
15
+ """
16
+
17
+ async def create_channel(self, logger: Any) -> grpc.aio.Channel:
18
+ """Creates a new async gRPC channel to the Function Executor Server.
19
+
20
+ The channel is in ready state. It can only be used in the same thread where the
21
+ function was called. Caller should close the channel when it's no longer needed.
22
+
23
+ Raises Exception if an error occurred."""
24
+ raise NotImplementedError
@@ -0,0 +1,43 @@
1
+ from typing import Any, Optional
2
+
3
+ from .function_executor_server import FunctionExecutorServer
4
+
5
+
6
+ class FunctionExecutorServerConfiguration:
7
+ """Configuration for creating a FunctionExecutorServer.
8
+
9
+ This configuration only includes data that must be known
10
+ during creation of the FunctionExecutorServer. If some data
11
+ is not required during the creation then it shouldn't be here.
12
+
13
+ A particular factory implementation might ignore certain
14
+ configuration parameters or raise an exception if it can't implement
15
+ them."""
16
+
17
+ def __init__(self, image_uri: Optional[str]):
18
+ # Container image URI of the Function Executor Server.
19
+ self.image_uri: Optional[str] = image_uri
20
+
21
+
22
+ class FunctionExecutorServerFactory:
23
+ """Abstract class for creating FunctionExecutorServers."""
24
+
25
+ async def create(
26
+ self, config: FunctionExecutorServerConfiguration, logger: Any
27
+ ) -> FunctionExecutorServer:
28
+ """Creates a new FunctionExecutorServer.
29
+
30
+ Raises an exception if the creation failed or the configuration is not supported.
31
+ Args:
32
+ config: configuration of the FunctionExecutorServer.
33
+ logger: logger to be used during the function call."""
34
+ raise NotImplementedError()
35
+
36
+ async def destroy(self, server: FunctionExecutorServer, logger: Any) -> None:
37
+ """Destroys the FunctionExecutorServer and release all its resources.
38
+
39
+ Args:
40
+ logger: logger to be used during the function call.
41
+ FunctionExecutorServer and customer code that it's running are not notified about the destruction.
42
+ Never raises any Exceptions."""
43
+ raise NotImplementedError
@@ -0,0 +1,25 @@
1
+ import asyncio
2
+ from typing import Any
3
+
4
+ import grpc
5
+
6
+ from indexify.function_executor.proto.configuration import GRPC_CHANNEL_OPTIONS
7
+
8
+ from .function_executor_server import FunctionExecutorServer
9
+
10
+
11
+ class SubprocessFunctionExecutorServer(FunctionExecutorServer):
12
+ """A FunctionExecutorServer that runs in a child process."""
13
+
14
+ def __init__(
15
+ self,
16
+ process: asyncio.subprocess.Process,
17
+ port: int,
18
+ address: str,
19
+ ):
20
+ self._proc = process
21
+ self._port = port
22
+ self._address = address
23
+
24
+ async def create_channel(self, logger: Any) -> grpc.aio.Channel:
25
+ return grpc.aio.insecure_channel(self._address, options=GRPC_CHANNEL_OPTIONS)
@@ -1,26 +1,32 @@
1
1
  import asyncio
2
2
  from typing import Any, Optional
3
3
 
4
- from .function_executor_factory import FunctionExecutorFactory
5
- from .process_function_executor import ProcessFunctionExecutor
4
+ from .function_executor_server_factory import (
5
+ FunctionExecutorServerConfiguration,
6
+ FunctionExecutorServerFactory,
7
+ )
8
+ from .subprocess_function_executor_server import (
9
+ SubprocessFunctionExecutorServer,
10
+ )
6
11
 
7
12
 
8
- class ProcessFunctionExecutorFactory(FunctionExecutorFactory):
13
+ class SubprocessFunctionExecutorServerFactory(FunctionExecutorServerFactory):
9
14
  def __init__(
10
15
  self,
11
- indexify_server_address: str,
12
16
  development_mode: bool,
13
- config_path: Optional[str],
14
17
  ):
15
- self._indexify_server_address: str = indexify_server_address
16
18
  self._development_mode: bool = development_mode
17
- self._config_path: Optional[str] = config_path
18
19
  # Registred ports range end at 49151. We start from 50000 to hopefully avoid conflicts.
19
20
  self._free_ports = set(range(50000, 51000))
20
21
 
21
22
  async def create(
22
- self, logger: Any, state: Optional[Any] = None
23
- ) -> ProcessFunctionExecutor:
23
+ self, config: FunctionExecutorServerConfiguration, logger: Any
24
+ ) -> SubprocessFunctionExecutorServer:
25
+ if config.image_uri is not None:
26
+ raise ValueError(
27
+ "SubprocessFunctionExecutorServerFactory doesn't support container images"
28
+ )
29
+
24
30
  logger = logger.bind(module=__name__)
25
31
  port: Optional[int] = None
26
32
 
@@ -30,13 +36,9 @@ class ProcessFunctionExecutorFactory(FunctionExecutorFactory):
30
36
  "function-executor",
31
37
  "--function-executor-server-address",
32
38
  _server_address(port),
33
- "--indexify-server-address",
34
- self._indexify_server_address,
35
39
  ]
36
40
  if self._development_mode:
37
41
  args.append("--dev")
38
- if self._config_path is not None:
39
- args.extend(["--config-path", self._config_path])
40
42
  # Run the process with our stdout, stderr. We want to see process logs and exceptions in our process output.
41
43
  # This is useful for dubugging. Customer function stdout and stderr is captured and returned in the response
42
44
  # so we won't see it in our process outputs. This is the right behavior as customer function stdout and stderr
@@ -45,12 +47,10 @@ class ProcessFunctionExecutorFactory(FunctionExecutorFactory):
45
47
  "indexify-cli",
46
48
  *args,
47
49
  )
48
- return ProcessFunctionExecutor(
50
+ return SubprocessFunctionExecutorServer(
49
51
  process=proc,
50
52
  port=port,
51
53
  address=_server_address(port),
52
- logger=logger,
53
- state=state,
54
54
  )
55
55
  except Exception as e:
56
56
  if port is not None:
@@ -61,9 +61,11 @@ class ProcessFunctionExecutorFactory(FunctionExecutorFactory):
61
61
  )
62
62
  raise
63
63
 
64
- async def destroy(self, executor: ProcessFunctionExecutor, logger: Any) -> None:
65
- proc: asyncio.subprocess.Process = executor._proc
66
- port: int = executor._port
64
+ async def destroy(
65
+ self, server: SubprocessFunctionExecutorServer, logger: Any
66
+ ) -> None:
67
+ proc: asyncio.subprocess.Process = server._proc
68
+ port: int = server._port
67
69
  logger = logger.bind(
68
70
  module=__name__,
69
71
  pid=proc.pid,
@@ -84,8 +86,6 @@ class ProcessFunctionExecutorFactory(FunctionExecutorFactory):
84
86
  )
85
87
  finally:
86
88
  self._release_port(port)
87
- if executor._channel is not None:
88
- await executor._channel.close()
89
89
 
90
90
  def _allocate_port(self) -> int:
91
91
  # No asyncio.Lock is required here because this operation never awaits
@@ -0,0 +1,160 @@
1
+ from typing import Any, Optional
2
+
3
+ import grpc
4
+
5
+ from indexify.function_executor.proto.function_executor_pb2 import (
6
+ InitializeRequest,
7
+ RunTaskRequest,
8
+ RunTaskResponse,
9
+ )
10
+ from indexify.function_executor.proto.function_executor_pb2_grpc import (
11
+ FunctionExecutorStub,
12
+ )
13
+
14
+ from ..api_objects import Task
15
+ from .function_executor import FunctionExecutor
16
+ from .function_executor_state import FunctionExecutorState
17
+ from .server.function_executor_server_factory import (
18
+ FunctionExecutorServerConfiguration,
19
+ FunctionExecutorServerFactory,
20
+ )
21
+ from .task_input import TaskInput
22
+ from .task_output import TaskOutput
23
+
24
+
25
+ class SingleTaskRunner:
26
+ def __init__(
27
+ self,
28
+ function_executor_state: FunctionExecutorState,
29
+ task_input: TaskInput,
30
+ function_executor_server_factory: FunctionExecutorServerFactory,
31
+ base_url: str,
32
+ config_path: Optional[str],
33
+ logger: Any,
34
+ ):
35
+ self._state: FunctionExecutorState = function_executor_state
36
+ self._task_input: TaskInput = task_input
37
+ self._factory: FunctionExecutorServerFactory = function_executor_server_factory
38
+ self._base_url: str = base_url
39
+ self._config_path: Optional[str] = config_path
40
+ self._logger = logger.bind(module=__name__)
41
+
42
+ async def run(self) -> TaskOutput:
43
+ """Runs the task in the Function Executor.
44
+
45
+ The FunctionExecutorState must be locked by the caller.
46
+ The lock is released during actual task run in the server.
47
+ The lock is relocked on return.
48
+
49
+ Raises an exception if an error occured."""
50
+ self._state.check_locked()
51
+
52
+ if self._state.function_executor is None:
53
+ self._state.function_executor = await self._create_function_executor()
54
+
55
+ return await self._run()
56
+
57
+ async def _create_function_executor(self) -> FunctionExecutor:
58
+ function_executor: FunctionExecutor = FunctionExecutor(
59
+ server_factory=self._factory, logger=self._logger
60
+ )
61
+ try:
62
+ config: FunctionExecutorServerConfiguration = (
63
+ FunctionExecutorServerConfiguration(
64
+ image_uri=self._task_input.task.image_uri,
65
+ )
66
+ )
67
+ initialize_request: InitializeRequest = InitializeRequest(
68
+ namespace=self._task_input.task.namespace,
69
+ graph_name=self._task_input.task.compute_graph,
70
+ graph_version=self._task_input.task.graph_version,
71
+ function_name=self._task_input.task.compute_fn,
72
+ graph=self._task_input.graph,
73
+ )
74
+ await function_executor.initialize(
75
+ config=config,
76
+ initialize_request=initialize_request,
77
+ base_url=self._base_url,
78
+ config_path=self._config_path,
79
+ )
80
+ return function_executor
81
+ except Exception as e:
82
+ self._logger.error(
83
+ "failed to initialize function executor",
84
+ exc_info=e,
85
+ )
86
+ await function_executor.destroy()
87
+ raise
88
+
89
+ async def _run(self) -> TaskOutput:
90
+ request: RunTaskRequest = RunTaskRequest(
91
+ graph_invocation_id=self._task_input.task.invocation_id,
92
+ task_id=self._task_input.task.id,
93
+ function_input=self._task_input.input,
94
+ )
95
+ if self._task_input.init_value is not None:
96
+ request.function_init_value.CopyFrom(self._task_input.init_value)
97
+ channel: grpc.aio.Channel = self._state.function_executor.channel()
98
+
99
+ async with _RunningTaskContextManager(
100
+ task_input=self._task_input, function_executor_state=self._state
101
+ ):
102
+ response: RunTaskResponse = await FunctionExecutorStub(channel).run_task(
103
+ request
104
+ )
105
+ return _task_output(task=self._task_input.task, response=response)
106
+
107
+
108
+ class _RunningTaskContextManager:
109
+ """Performs all the actions required before and after running a task."""
110
+
111
+ def __init__(
112
+ self, task_input: TaskInput, function_executor_state: FunctionExecutorState
113
+ ):
114
+ self._task_input: TaskInput = task_input
115
+ self._state: FunctionExecutorState = function_executor_state
116
+
117
+ async def __aenter__(self):
118
+ self._state.increment_running_tasks()
119
+ self._state.function_executor.invocation_state_client().add_task_to_invocation_id_entry(
120
+ task_id=self._task_input.task.id,
121
+ invocation_id=self._task_input.task.invocation_id,
122
+ )
123
+ # Unlock the state so other tasks can act depending on it.
124
+ self._state.lock.release()
125
+ return self
126
+
127
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
128
+ await self._state.lock.acquire()
129
+ self._state.decrement_running_tasks()
130
+ self._state.function_executor.invocation_state_client().remove_task_to_invocation_id_entry(
131
+ task_id=self._task_input.task.id
132
+ )
133
+
134
+
135
+ def _task_output(task: Task, response: RunTaskResponse) -> TaskOutput:
136
+ required_fields = [
137
+ "stdout",
138
+ "stderr",
139
+ "is_reducer",
140
+ "success",
141
+ ]
142
+
143
+ for field in required_fields:
144
+ if not response.HasField(field):
145
+ raise ValueError(f"Response is missing required field: {field}")
146
+
147
+ output = TaskOutput(
148
+ task=task,
149
+ stdout=response.stdout,
150
+ stderr=response.stderr,
151
+ reducer=response.is_reducer,
152
+ success=response.success,
153
+ )
154
+
155
+ if response.HasField("function_output"):
156
+ output.function_output = response.function_output
157
+ if response.HasField("router_output"):
158
+ output.router_output = response.router_output
159
+
160
+ return output
@@ -0,0 +1,23 @@
1
+ from typing import Optional
2
+
3
+ from indexify.function_executor.proto.function_executor_pb2 import (
4
+ SerializedObject,
5
+ )
6
+
7
+ from ..api_objects import Task
8
+
9
+
10
+ class TaskInput:
11
+ """Task with all the resources required to run it."""
12
+
13
+ def __init__(
14
+ self,
15
+ task: Task,
16
+ graph: SerializedObject,
17
+ input: SerializedObject,
18
+ init_value: Optional[SerializedObject],
19
+ ):
20
+ self.task: Task = task
21
+ self.graph: SerializedObject = graph
22
+ self.input: SerializedObject = input
23
+ self.init_value: Optional[SerializedObject] = init_value
@@ -0,0 +1,36 @@
1
+ from typing import Optional
2
+
3
+ from indexify.function_executor.proto.function_executor_pb2 import (
4
+ FunctionOutput,
5
+ RouterOutput,
6
+ )
7
+
8
+ from ..api_objects import Task
9
+
10
+
11
+ class TaskOutput:
12
+ """Result of running a task."""
13
+
14
+ def __init__(
15
+ self,
16
+ task: Task,
17
+ function_output: Optional[FunctionOutput] = None,
18
+ router_output: Optional[RouterOutput] = None,
19
+ stdout: Optional[str] = None,
20
+ stderr: Optional[str] = None,
21
+ reducer: bool = False,
22
+ success: bool = False,
23
+ ):
24
+ self.task = task
25
+ self.function_output = function_output
26
+ self.router_output = router_output
27
+ self.stdout = stdout
28
+ self.stderr = stderr
29
+ self.reducer = reducer
30
+ self.success = success
31
+
32
+ @classmethod
33
+ def internal_error(cls, task: Task) -> "TaskOutput":
34
+ """Creates a TaskOutput for an internal error."""
35
+ # We are not sharing internal error messages with the customer.
36
+ return TaskOutput(task=task, stderr="Platform failed to execute the function.")