indexify 0.2.27__py3-none-any.whl → 0.2.28__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/common_util.py +88 -0
- indexify/executor/agent.py +18 -36
- indexify/executor/downloader.py +38 -9
- indexify/executor/function_worker.py +2 -2
- indexify/executor/task_reporter.py +26 -11
- indexify/functions_sdk/data_objects.py +1 -1
- indexify/functions_sdk/graph.py +2 -2
- indexify/functions_sdk/graph_definition.py +2 -2
- indexify/functions_sdk/indexify_functions.py +1 -6
- indexify/functions_sdk/object_serializer.py +10 -32
- indexify/http_client.py +47 -33
- indexify/remote_graph.py +10 -6
- indexify/remote_pipeline.py +1 -0
- {indexify-0.2.27.dist-info → indexify-0.2.28.dist-info}/METADATA +3 -4
- {indexify-0.2.27.dist-info → indexify-0.2.28.dist-info}/RECORD +18 -17
- {indexify-0.2.27.dist-info → indexify-0.2.28.dist-info}/LICENSE.txt +0 -0
- {indexify-0.2.27.dist-info → indexify-0.2.28.dist-info}/WHEEL +0 -0
- {indexify-0.2.27.dist-info → indexify-0.2.28.dist-info}/entry_points.txt +0 -0
indexify/common_util.py
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
from typing import Optional, Union
|
2
|
+
|
3
|
+
import httpx
|
4
|
+
import yaml
|
5
|
+
from httpx import AsyncClient, Client
|
6
|
+
|
7
|
+
|
8
|
+
def get_httpx_client(
|
9
|
+
config_path: Optional[str] = None, make_async: Optional[bool] = False
|
10
|
+
) -> AsyncClient | Client:
|
11
|
+
"""
|
12
|
+
Creates and returns an httpx.Client instance, optionally configured with TLS settings from a YAML config file.
|
13
|
+
|
14
|
+
The function creates a basic httpx.Client by default. If a config path is provided and the config specifies
|
15
|
+
'use_tls' as True, it creates a TLS-enabled client with HTTP/2 support using the provided certificate settings.
|
16
|
+
To get https.AsyncClient, provide make_async as True.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
config_path (Optional[str]): Path to a YAML configuration file. If provided, the file should contain TLS
|
20
|
+
configuration with the following structure:
|
21
|
+
{
|
22
|
+
"use_tls": bool,
|
23
|
+
"tls_config": {
|
24
|
+
"cert_path": str, # Path to client certificate
|
25
|
+
"key_path": str, # Path to client private key
|
26
|
+
"ca_bundle_path": str # Optional: Path to CA bundle for verification
|
27
|
+
}
|
28
|
+
}
|
29
|
+
make_async (Optional[bool]): Whether to make an asynchronous httpx client instance.
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
httpx.Client: An initialized httpx client instance, either with basic settings or TLS configuration
|
33
|
+
if specified in the config file.
|
34
|
+
|
35
|
+
Example:
|
36
|
+
# Basic client without TLS
|
37
|
+
client = get_httpx_client()
|
38
|
+
|
39
|
+
# Client with TLS configuration from config file
|
40
|
+
client = get_httpx_client("/path/to/config.yaml")
|
41
|
+
"""
|
42
|
+
if config_path:
|
43
|
+
with open(config_path, "r") as file:
|
44
|
+
config = yaml.safe_load(file)
|
45
|
+
if config.get("use_tls", False):
|
46
|
+
print(f"Configuring client with TLS config: {config}")
|
47
|
+
tls_config = config["tls_config"]
|
48
|
+
return get_sync_or_async_client(make_async, **tls_config)
|
49
|
+
return get_sync_or_async_client(make_async)
|
50
|
+
|
51
|
+
|
52
|
+
def get_sync_or_async_client(
|
53
|
+
make_async: Optional[bool] = False,
|
54
|
+
cert_path: Optional[str] = None,
|
55
|
+
key_path: Optional[str] = None,
|
56
|
+
ca_bundle_path: Optional[str] = None,
|
57
|
+
) -> AsyncClient | Client:
|
58
|
+
"""
|
59
|
+
Creates and returns either a synchronous or asynchronous httpx client with optional TLS configuration.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
make_async (Optional[bool]): If True, returns an AsyncClient; if False, returns a synchronous Client.
|
63
|
+
Defaults to False.
|
64
|
+
cert_path (Optional[str]): Path to the client certificate file. Required for TLS configuration
|
65
|
+
when key_path is also provided.
|
66
|
+
key_path (Optional[str]): Path to the client private key file. Required for TLS configuration
|
67
|
+
when cert_path is also provided.
|
68
|
+
ca_bundle_path (Optional[str]): Path to the CA bundle file for certificate verification.
|
69
|
+
If not provided, defaults to system CA certificates.
|
70
|
+
"""
|
71
|
+
if make_async:
|
72
|
+
if cert_path and key_path:
|
73
|
+
return httpx.AsyncClient(
|
74
|
+
http2=True,
|
75
|
+
cert=(cert_path, key_path),
|
76
|
+
verify=ca_bundle_path if ca_bundle_path else True,
|
77
|
+
)
|
78
|
+
else:
|
79
|
+
return httpx.AsyncClient()
|
80
|
+
else:
|
81
|
+
if cert_path and key_path:
|
82
|
+
return httpx.Client(
|
83
|
+
http2=True,
|
84
|
+
cert=(cert_path, key_path),
|
85
|
+
verify=ca_bundle_path if ca_bundle_path else True,
|
86
|
+
)
|
87
|
+
else:
|
88
|
+
return httpx.Client()
|
indexify/executor/agent.py
CHANGED
@@ -1,13 +1,11 @@
|
|
1
1
|
import asyncio
|
2
2
|
import json
|
3
|
-
import ssl
|
4
3
|
import traceback
|
5
4
|
from concurrent.futures.process import BrokenProcessPool
|
6
5
|
from importlib.metadata import version
|
6
|
+
from pathlib import Path
|
7
7
|
from typing import Dict, List, Optional
|
8
8
|
|
9
|
-
import httpx
|
10
|
-
import yaml
|
11
9
|
from httpx_sse import aconnect_sse
|
12
10
|
from pydantic import BaseModel
|
13
11
|
from rich.console import Console
|
@@ -15,6 +13,7 @@ from rich.panel import Panel
|
|
15
13
|
from rich.text import Text
|
16
14
|
from rich.theme import Theme
|
17
15
|
|
16
|
+
from indexify.common_util import get_httpx_client
|
18
17
|
from indexify.functions_sdk.data_objects import (
|
19
18
|
FunctionWorkerOutput,
|
20
19
|
IndexifyData,
|
@@ -58,7 +57,7 @@ class ExtractorAgent:
|
|
58
57
|
self,
|
59
58
|
executor_id: str,
|
60
59
|
num_workers,
|
61
|
-
code_path:
|
60
|
+
code_path: Path,
|
62
61
|
server_addr: str = "localhost:8900",
|
63
62
|
config_path: Optional[str] = None,
|
64
63
|
name_alias: Optional[str] = None,
|
@@ -66,7 +65,7 @@ class ExtractorAgent:
|
|
66
65
|
):
|
67
66
|
self.name_alias = name_alias
|
68
67
|
self.image_version = image_version
|
69
|
-
|
68
|
+
self._config_path = config_path
|
70
69
|
self._probe = RuntimeProbes()
|
71
70
|
|
72
71
|
runtime_probe: ProbeInfo = self._probe.probe()
|
@@ -82,39 +81,19 @@ class ExtractorAgent:
|
|
82
81
|
)
|
83
82
|
|
84
83
|
self.num_workers = num_workers
|
85
|
-
self._use_tls = False
|
86
84
|
if config_path:
|
87
|
-
with
|
88
|
-
|
89
|
-
self._config = config
|
90
|
-
if config.get("use_tls", False):
|
91
|
-
console.print(
|
92
|
-
"Running the extractor with TLS enabled", style="cyan bold"
|
93
|
-
)
|
94
|
-
self._use_tls = True
|
95
|
-
tls_config = config["tls_config"]
|
96
|
-
self._ssl_context = ssl.create_default_context(
|
97
|
-
ssl.Purpose.SERVER_AUTH, cafile=tls_config["ca_bundle_path"]
|
98
|
-
)
|
99
|
-
self._ssl_context.load_cert_chain(
|
100
|
-
certfile=tls_config["cert_path"], keyfile=tls_config["key_path"]
|
101
|
-
)
|
102
|
-
self._protocol = "wss"
|
103
|
-
self._tls_config = tls_config
|
104
|
-
else:
|
105
|
-
self._ssl_context = None
|
106
|
-
self._protocol = "ws"
|
85
|
+
console.print("Running the extractor with TLS enabled", style="cyan bold")
|
86
|
+
self._protocol = "https"
|
107
87
|
else:
|
108
|
-
self._ssl_context = None
|
109
88
|
self._protocol = "http"
|
110
|
-
self._config = {}
|
111
89
|
|
112
90
|
self._task_store: TaskStore = TaskStore()
|
113
91
|
self._executor_id = executor_id
|
114
92
|
self._function_worker = FunctionWorker(
|
115
93
|
workers=num_workers,
|
116
94
|
indexify_client=IndexifyClient(
|
117
|
-
service_url=f"{self._protocol}://{server_addr}"
|
95
|
+
service_url=f"{self._protocol}://{server_addr}",
|
96
|
+
config_path=config_path,
|
118
97
|
),
|
119
98
|
)
|
120
99
|
self._has_registered = False
|
@@ -124,7 +103,9 @@ class ExtractorAgent:
|
|
124
103
|
self._downloader = Downloader(code_path=code_path, base_url=self._base_url)
|
125
104
|
self._max_queued_tasks = 10
|
126
105
|
self._task_reporter = TaskReporter(
|
127
|
-
base_url=self._base_url,
|
106
|
+
base_url=self._base_url,
|
107
|
+
executor_id=self._executor_id,
|
108
|
+
config_path=self._config_path,
|
128
109
|
)
|
129
110
|
|
130
111
|
async def task_completion_reporter(self):
|
@@ -197,7 +178,7 @@ class ExtractorAgent:
|
|
197
178
|
if self._require_image_bootstrap:
|
198
179
|
try:
|
199
180
|
image_info = await _get_image_info_for_compute_graph(
|
200
|
-
task, self._protocol, self._server_addr
|
181
|
+
task, self._protocol, self._server_addr, self._config_path
|
201
182
|
)
|
202
183
|
image_dependency_installer.executor_image_builder(
|
203
184
|
image_info, self.name_alias, self.image_version
|
@@ -365,8 +346,8 @@ class ExtractorAgent:
|
|
365
346
|
asyncio.create_task(self.task_completion_reporter())
|
366
347
|
self._should_run = True
|
367
348
|
while self._should_run:
|
368
|
-
self._protocol = "http"
|
369
349
|
url = f"{self._protocol}://{self._server_addr}/internal/executors/{self._executor_id}/tasks"
|
350
|
+
print(f"calling url: {url}")
|
370
351
|
|
371
352
|
def to_sentence_case(snake_str):
|
372
353
|
words = snake_str.split("_")
|
@@ -407,9 +388,8 @@ class ExtractorAgent:
|
|
407
388
|
border_style="cyan",
|
408
389
|
)
|
409
390
|
)
|
410
|
-
|
411
391
|
try:
|
412
|
-
async with
|
392
|
+
async with get_httpx_client(self._config_path, True) as client:
|
413
393
|
async with aconnect_sse(
|
414
394
|
client,
|
415
395
|
"POST",
|
@@ -453,14 +433,16 @@ class ExtractorAgent:
|
|
453
433
|
|
454
434
|
|
455
435
|
async def _get_image_info_for_compute_graph(
|
456
|
-
task: Task, protocol, server_addr
|
436
|
+
task: Task, protocol, server_addr, config_path: str
|
457
437
|
) -> ImageInformation:
|
458
438
|
namespace = task.namespace
|
459
439
|
graph_name: str = task.compute_graph
|
460
440
|
compute_fn_name: str = task.compute_fn
|
461
441
|
|
462
442
|
http_client = IndexifyClient(
|
463
|
-
service_url=f"{protocol}://{server_addr}",
|
443
|
+
service_url=f"{protocol}://{server_addr}",
|
444
|
+
namespace=namespace,
|
445
|
+
config_path=config_path,
|
464
446
|
)
|
465
447
|
compute_graph: ComputeGraphMetadata = http_client.graph(graph_name)
|
466
448
|
|
indexify/executor/downloader.py
CHANGED
@@ -8,8 +8,9 @@ from rich.panel import Panel
|
|
8
8
|
from rich.theme import Theme
|
9
9
|
|
10
10
|
from indexify.functions_sdk.data_objects import IndexifyData
|
11
|
-
from indexify.functions_sdk.object_serializer import MsgPackSerializer
|
12
11
|
|
12
|
+
from ..common_util import get_httpx_client
|
13
|
+
from ..functions_sdk.object_serializer import JsonSerializer, get_serializer
|
13
14
|
from .api_objects import Task
|
14
15
|
|
15
16
|
custom_theme = Theme(
|
@@ -29,9 +30,12 @@ class DownloadedInputs(BaseModel):
|
|
29
30
|
|
30
31
|
|
31
32
|
class Downloader:
|
32
|
-
def __init__(
|
33
|
+
def __init__(
|
34
|
+
self, code_path: str, base_url: str, config_path: Optional[str] = None
|
35
|
+
):
|
33
36
|
self.code_path = code_path
|
34
37
|
self.base_url = base_url
|
38
|
+
self._client = get_httpx_client(config_path)
|
35
39
|
|
36
40
|
async def download_graph(self, namespace: str, name: str, version: int) -> str:
|
37
41
|
path = os.path.join(self.code_path, namespace, f"{name}.{version}")
|
@@ -46,7 +50,7 @@ class Downloader:
|
|
46
50
|
)
|
47
51
|
)
|
48
52
|
|
49
|
-
response =
|
53
|
+
response = self._client.get(
|
50
54
|
f"{self.base_url}/internal/namespaces/{namespace}/compute_graphs/{name}/code"
|
51
55
|
)
|
52
56
|
try:
|
@@ -66,7 +70,7 @@ class Downloader:
|
|
66
70
|
f.write(response.content)
|
67
71
|
return path
|
68
72
|
|
69
|
-
async def download_input(self, task: Task) ->
|
73
|
+
async def download_input(self, task: Task) -> DownloadedInputs:
|
70
74
|
input_id = task.input_key.split("|")[-1]
|
71
75
|
if task.invocation_id == input_id:
|
72
76
|
url = f"{self.base_url}/namespaces/{task.namespace}/compute_graphs/{task.compute_graph}/invocations/{task.invocation_id}/payload"
|
@@ -85,7 +89,8 @@ class Downloader:
|
|
85
89
|
)
|
86
90
|
)
|
87
91
|
|
88
|
-
response =
|
92
|
+
response = self._client.get(url)
|
93
|
+
|
89
94
|
try:
|
90
95
|
response.raise_for_status()
|
91
96
|
except httpx.HTTPStatusError as e:
|
@@ -98,12 +103,22 @@ class Downloader:
|
|
98
103
|
)
|
99
104
|
raise
|
100
105
|
|
106
|
+
encoder = (
|
107
|
+
"json"
|
108
|
+
if response.headers["content-type"] == JsonSerializer.content_type
|
109
|
+
else "cloudpickle"
|
110
|
+
)
|
111
|
+
serializer = get_serializer(encoder)
|
112
|
+
|
101
113
|
if task.invocation_id == input_id:
|
102
114
|
return DownloadedInputs(
|
103
|
-
input=IndexifyData(
|
115
|
+
input=IndexifyData(
|
116
|
+
payload=response.content, id=input_id, encoder=encoder
|
117
|
+
),
|
104
118
|
)
|
105
119
|
|
106
|
-
|
120
|
+
deserialized_content = serializer.deserialize(response.content)
|
121
|
+
|
107
122
|
if reducer_url:
|
108
123
|
init_value = httpx.get(reducer_url)
|
109
124
|
try:
|
@@ -117,8 +132,22 @@ class Downloader:
|
|
117
132
|
)
|
118
133
|
)
|
119
134
|
raise
|
120
|
-
init_value =
|
135
|
+
init_value = serializer.deserialize(init_value.content)
|
136
|
+
return DownloadedInputs(
|
137
|
+
input=IndexifyData(
|
138
|
+
input_id=task.invocation_id,
|
139
|
+
payload=deserialized_content,
|
140
|
+
encoder=encoder,
|
141
|
+
),
|
142
|
+
init_value=IndexifyData(
|
143
|
+
input_id=task.invocation_id, payload=init_value, encoder=encoder
|
144
|
+
),
|
145
|
+
)
|
121
146
|
|
122
147
|
return DownloadedInputs(
|
123
|
-
input=
|
148
|
+
input=IndexifyData(
|
149
|
+
input_id=task.invocation_id,
|
150
|
+
payload=deserialized_content,
|
151
|
+
encoder=encoder,
|
152
|
+
)
|
124
153
|
)
|
@@ -15,10 +15,10 @@ from indexify.functions_sdk.data_objects import (
|
|
15
15
|
from indexify.functions_sdk.indexify_functions import (
|
16
16
|
FunctionCallResult,
|
17
17
|
GraphInvocationContext,
|
18
|
+
IndexifyFunction,
|
18
19
|
IndexifyFunctionWrapper,
|
19
|
-
RouterCallResult,
|
20
20
|
IndexifyRouter,
|
21
|
-
|
21
|
+
RouterCallResult,
|
22
22
|
)
|
23
23
|
|
24
24
|
function_wrapper_map: Dict[str, IndexifyFunctionWrapper] = {}
|
@@ -1,15 +1,14 @@
|
|
1
1
|
import io
|
2
|
-
from typing import
|
2
|
+
from typing import Optional
|
3
3
|
|
4
|
-
import httpx
|
5
4
|
import nanoid
|
6
5
|
from rich import print
|
7
6
|
|
7
|
+
from indexify.common_util import get_httpx_client
|
8
8
|
from indexify.executor.api_objects import RouterOutput as ApiRouterOutput
|
9
|
-
from indexify.executor.api_objects import
|
9
|
+
from indexify.executor.api_objects import TaskResult
|
10
10
|
from indexify.executor.task_store import CompletedTask
|
11
|
-
from indexify.functions_sdk.
|
12
|
-
from indexify.functions_sdk.object_serializer import MsgPackSerializer
|
11
|
+
from indexify.functions_sdk.object_serializer import get_serializer
|
13
12
|
|
14
13
|
|
15
14
|
# https://github.com/psf/requests/issues/1081#issuecomment-428504128
|
@@ -19,12 +18,16 @@ class ForceMultipartDict(dict):
|
|
19
18
|
|
20
19
|
|
21
20
|
FORCE_MULTIPART = ForceMultipartDict()
|
21
|
+
UTF_8_CONTENT_TYPE = "application/octet-stream"
|
22
22
|
|
23
23
|
|
24
24
|
class TaskReporter:
|
25
|
-
def __init__(
|
25
|
+
def __init__(
|
26
|
+
self, base_url: str, executor_id: str, config_path: Optional[str] = None
|
27
|
+
):
|
26
28
|
self._base_url = base_url
|
27
29
|
self._executor_id = executor_id
|
30
|
+
self._client = get_httpx_client(config_path)
|
28
31
|
|
29
32
|
def report_task_outcome(self, completed_task: CompletedTask):
|
30
33
|
fn_outputs = []
|
@@ -32,9 +35,13 @@ class TaskReporter:
|
|
32
35
|
print(
|
33
36
|
f"[bold]task-reporter[/bold] uploading output of size: {len(output.payload)} bytes"
|
34
37
|
)
|
35
|
-
|
38
|
+
serializer = get_serializer(output.encoder)
|
39
|
+
serialized_output = serializer.serialize(output.payload)
|
36
40
|
fn_outputs.append(
|
37
|
-
(
|
41
|
+
(
|
42
|
+
"node_outputs",
|
43
|
+
(nanoid.generate(), serialized_output, serializer.content_type),
|
44
|
+
)
|
38
45
|
)
|
39
46
|
|
40
47
|
if completed_task.stdout:
|
@@ -44,7 +51,11 @@ class TaskReporter:
|
|
44
51
|
fn_outputs.append(
|
45
52
|
(
|
46
53
|
"stdout",
|
47
|
-
(
|
54
|
+
(
|
55
|
+
nanoid.generate(),
|
56
|
+
completed_task.stdout.encode(),
|
57
|
+
UTF_8_CONTENT_TYPE,
|
58
|
+
),
|
48
59
|
)
|
49
60
|
)
|
50
61
|
|
@@ -55,7 +66,11 @@ class TaskReporter:
|
|
55
66
|
fn_outputs.append(
|
56
67
|
(
|
57
68
|
"stderr",
|
58
|
-
(
|
69
|
+
(
|
70
|
+
nanoid.generate(),
|
71
|
+
completed_task.stderr.encode(),
|
72
|
+
UTF_8_CONTENT_TYPE,
|
73
|
+
),
|
59
74
|
)
|
60
75
|
)
|
61
76
|
|
@@ -84,7 +99,7 @@ class TaskReporter:
|
|
84
99
|
else:
|
85
100
|
kwargs["files"] = FORCE_MULTIPART
|
86
101
|
try:
|
87
|
-
response =
|
102
|
+
response = self._client.post(
|
88
103
|
url=f"{self._base_url}/internal/ingest_files",
|
89
104
|
**kwargs,
|
90
105
|
)
|
indexify/functions_sdk/graph.py
CHANGED
@@ -167,7 +167,7 @@ class Graph:
|
|
167
167
|
reducer=is_reducer,
|
168
168
|
image_name=start_node.image._image_name,
|
169
169
|
image_information=start_node.image.to_image_information(),
|
170
|
-
|
170
|
+
encoder=start_node.encoder,
|
171
171
|
)
|
172
172
|
metadata_edges = self.edges.copy()
|
173
173
|
metadata_nodes = {}
|
@@ -179,7 +179,7 @@ class Graph:
|
|
179
179
|
description=node.description or "",
|
180
180
|
source_fn=node_name,
|
181
181
|
target_fns=self.routers[node_name],
|
182
|
-
|
182
|
+
encoder=node.encoder,
|
183
183
|
image_name=node.image._image_name,
|
184
184
|
image_information=node.image.to_image_information(),
|
185
185
|
)
|
@@ -14,7 +14,7 @@ class FunctionMetadata(BaseModel):
|
|
14
14
|
reducer: bool = False
|
15
15
|
image_name: str
|
16
16
|
image_information: ImageInformation
|
17
|
-
|
17
|
+
encoder: str = "cloudpickle"
|
18
18
|
|
19
19
|
|
20
20
|
class RouterMetadata(BaseModel):
|
@@ -24,7 +24,7 @@ class RouterMetadata(BaseModel):
|
|
24
24
|
target_fns: List[str]
|
25
25
|
image_name: str
|
26
26
|
image_information: ImageInformation
|
27
|
-
|
27
|
+
encoder: str = "cloudpickle"
|
28
28
|
|
29
29
|
|
30
30
|
class NodeMetadata(BaseModel):
|
@@ -1,8 +1,5 @@
|
|
1
1
|
import inspect
|
2
|
-
import re
|
3
|
-
import sys
|
4
2
|
import traceback
|
5
|
-
from functools import update_wrapper
|
6
3
|
from typing import (
|
7
4
|
Any,
|
8
5
|
Callable,
|
@@ -16,9 +13,7 @@ from typing import (
|
|
16
13
|
get_origin,
|
17
14
|
)
|
18
15
|
|
19
|
-
import
|
20
|
-
from pydantic import BaseModel, Field, PrivateAttr, model_validator
|
21
|
-
from typing_extensions import get_type_hints
|
16
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
22
17
|
|
23
18
|
from .data_objects import IndexifyData
|
24
19
|
from .image import DEFAULT_IMAGE_3_10, Image
|
@@ -2,23 +2,24 @@ from typing import Any, List
|
|
2
2
|
|
3
3
|
import cloudpickle
|
4
4
|
import jsonpickle
|
5
|
-
import msgpack
|
6
|
-
from pydantic import BaseModel
|
7
|
-
|
8
|
-
from .data_objects import IndexifyData
|
9
5
|
|
10
6
|
|
11
7
|
def get_serializer(serializer_type: str) -> Any:
|
12
8
|
if serializer_type == "cloudpickle":
|
13
9
|
return CloudPickleSerializer()
|
14
|
-
elif serializer_type == "msgpack":
|
15
|
-
return MsgPackSerializer()
|
16
10
|
elif serializer_type == "json":
|
17
11
|
return JsonSerializer()
|
12
|
+
elif serializer_type == JsonSerializer.content_type:
|
13
|
+
return JsonSerializer()
|
14
|
+
elif serializer_type == CloudPickleSerializer.content_type:
|
15
|
+
return CloudPickleSerializer()
|
18
16
|
raise ValueError(f"Unknown serializer type: {serializer_type}")
|
19
17
|
|
20
18
|
|
21
19
|
class JsonSerializer:
|
20
|
+
content_type = "application/json"
|
21
|
+
encoding_type = "json"
|
22
|
+
|
22
23
|
@staticmethod
|
23
24
|
def serialize(data: Any) -> str:
|
24
25
|
return jsonpickle.encode(data)
|
@@ -37,6 +38,9 @@ class JsonSerializer:
|
|
37
38
|
|
38
39
|
|
39
40
|
class CloudPickleSerializer:
|
41
|
+
content_type = "application/octet-stream"
|
42
|
+
encoding_type = "cloudpickle"
|
43
|
+
|
40
44
|
@staticmethod
|
41
45
|
def serialize(data: Any) -> bytes:
|
42
46
|
return cloudpickle.dumps(data)
|
@@ -52,29 +56,3 @@ class CloudPickleSerializer:
|
|
52
56
|
@staticmethod
|
53
57
|
def deserialize_list(data: bytes) -> List[Any]:
|
54
58
|
return cloudpickle.loads(data)
|
55
|
-
|
56
|
-
|
57
|
-
class MsgPackSerializer:
|
58
|
-
@staticmethod
|
59
|
-
def serialize(data: Any) -> bytes:
|
60
|
-
if (
|
61
|
-
isinstance(data, type)
|
62
|
-
and issubclass(data, BaseModel)
|
63
|
-
or isinstance(data, BaseModel)
|
64
|
-
):
|
65
|
-
return msgpack.packb(data.model_dump())
|
66
|
-
return msgpack.packb(data)
|
67
|
-
|
68
|
-
@staticmethod
|
69
|
-
def deserialize(data: bytes) -> IndexifyData:
|
70
|
-
cached_output = msgpack.unpackb(data)
|
71
|
-
return IndexifyData(**cached_output)
|
72
|
-
|
73
|
-
@staticmethod
|
74
|
-
def serialize_list(data: List[IndexifyData]) -> bytes:
|
75
|
-
data = [item.model_dump() for item in data]
|
76
|
-
return msgpack.packb(data)
|
77
|
-
|
78
|
-
@staticmethod
|
79
|
-
def deserialize_list(data: bytes) -> List[IndexifyData]:
|
80
|
-
return [IndexifyData(**item) for item in msgpack.unpackb(data)]
|
indexify/http_client.py
CHANGED
@@ -1,19 +1,23 @@
|
|
1
1
|
import json
|
2
2
|
import os
|
3
|
-
from typing import Any, Dict, List, Optional
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
4
4
|
|
5
5
|
import cloudpickle
|
6
6
|
import httpx
|
7
|
-
import msgpack
|
8
|
-
import yaml
|
9
7
|
from httpx_sse import connect_sse
|
10
8
|
from pydantic import BaseModel, Json
|
11
9
|
from rich import print
|
12
10
|
|
11
|
+
from indexify.common_util import get_httpx_client, get_sync_or_async_client
|
13
12
|
from indexify.error import ApiException, GraphStillProcessing
|
14
13
|
from indexify.functions_sdk.data_objects import IndexifyData
|
15
14
|
from indexify.functions_sdk.graph import ComputeGraphMetadata, Graph
|
16
15
|
from indexify.functions_sdk.indexify_functions import IndexifyFunction
|
16
|
+
from indexify.functions_sdk.object_serializer import (
|
17
|
+
CloudPickleSerializer,
|
18
|
+
JsonSerializer,
|
19
|
+
get_serializer,
|
20
|
+
)
|
17
21
|
from indexify.settings import DEFAULT_SERVICE_URL
|
18
22
|
|
19
23
|
|
@@ -55,18 +59,8 @@ class IndexifyClient:
|
|
55
59
|
service_url = os.environ["INDEXIFY_URL"]
|
56
60
|
|
57
61
|
self.service_url = service_url
|
58
|
-
self.
|
59
|
-
|
60
|
-
with open(config_path, "r") as file:
|
61
|
-
config = yaml.safe_load(file)
|
62
|
-
|
63
|
-
if config.get("use_tls", False):
|
64
|
-
tls_config = config["tls_config"]
|
65
|
-
self._client = httpx.Client(
|
66
|
-
http2=True,
|
67
|
-
cert=(tls_config["cert_path"], tls_config["key_path"]),
|
68
|
-
verify=tls_config.get("ca_bundle_path", True),
|
69
|
-
)
|
62
|
+
self._config_path = config_path
|
63
|
+
self._client = get_httpx_client(config_path)
|
70
64
|
|
71
65
|
self.namespace: str = namespace
|
72
66
|
self.compute_graphs: List[Graph] = []
|
@@ -135,17 +129,13 @@ class IndexifyClient:
|
|
135
129
|
if not (cert_path and key_path):
|
136
130
|
raise ValueError("Both cert and key must be provided for mTLS")
|
137
131
|
|
138
|
-
|
139
|
-
|
140
|
-
client = IndexifyClient(
|
141
|
-
*args,
|
142
|
-
**kwargs,
|
143
|
-
service_url=service_url,
|
144
|
-
http2=True,
|
145
|
-
cert=client_certs,
|
146
|
-
verify=verify_option,
|
132
|
+
client = get_sync_or_async_client(
|
133
|
+
cert_path=cert_path, key_path=key_path, ca_bundle_path=ca_bundle_path
|
147
134
|
)
|
148
|
-
|
135
|
+
|
136
|
+
indexify_client = IndexifyClient(service_url, *args, **kwargs)
|
137
|
+
indexify_client._client = client
|
138
|
+
return indexify_client
|
149
139
|
|
150
140
|
def _add_api_key(self, kwargs):
|
151
141
|
if self._api_key:
|
@@ -189,6 +179,20 @@ class IndexifyClient:
|
|
189
179
|
for fn_name, fn in graph.nodes.items():
|
190
180
|
self._fns[f"{graph.name}/{fn_name}"] = fn
|
191
181
|
|
182
|
+
def delete_compute_graph(
|
183
|
+
self,
|
184
|
+
graph_name: str,
|
185
|
+
) -> None:
|
186
|
+
"""
|
187
|
+
Deletes a graph and all of its invocations from the namespace.
|
188
|
+
:param graph_name The name of the graph to delete.
|
189
|
+
WARNING: This operation is irreversible.
|
190
|
+
"""
|
191
|
+
response = self._delete(
|
192
|
+
f"namespaces/{self.namespace}/compute_graphs/{graph_name}",
|
193
|
+
)
|
194
|
+
response.raise_for_status()
|
195
|
+
|
192
196
|
def graphs(self) -> List[str]:
|
193
197
|
response = self._get(f"graphs")
|
194
198
|
return response.json()["graphs"]
|
@@ -265,21 +269,27 @@ class IndexifyClient:
|
|
265
269
|
print(f"failed to fetch logs: {e}")
|
266
270
|
return None
|
267
271
|
|
268
|
-
def
|
269
|
-
self._post(f"namespaces/{self.namespace}/compute_graphs/{graph}/
|
272
|
+
def replay_invocations(self, graph: str):
|
273
|
+
self._post(f"namespaces/{self.namespace}/compute_graphs/{graph}/replay")
|
270
274
|
|
271
275
|
def invoke_graph_with_object(
|
272
|
-
self,
|
276
|
+
self,
|
277
|
+
graph: str,
|
278
|
+
block_until_done: bool = False,
|
279
|
+
serializer: Union[
|
280
|
+
CloudPickleSerializer, JsonSerializer
|
281
|
+
] = CloudPickleSerializer,
|
282
|
+
**kwargs,
|
273
283
|
) -> str:
|
274
|
-
ser_input =
|
284
|
+
ser_input = serializer.serialize(kwargs)
|
275
285
|
params = {"block_until_finish": block_until_done}
|
276
286
|
kwargs = {
|
277
|
-
"headers": {"Content-Type":
|
287
|
+
"headers": {"Content-Type": serializer.content_type},
|
278
288
|
"data": ser_input,
|
279
289
|
"params": params,
|
280
290
|
}
|
281
291
|
self._add_api_key(kwargs)
|
282
|
-
with
|
292
|
+
with get_httpx_client(self._config_path) as client:
|
283
293
|
with connect_sse(
|
284
294
|
client,
|
285
295
|
"POST",
|
@@ -343,8 +353,12 @@ class IndexifyClient:
|
|
343
353
|
f"namespaces/{namespace}/compute_graphs/{graph}/invocations/{invocation_id}/fn/{fn_name}/output/{output_id}",
|
344
354
|
)
|
345
355
|
response.raise_for_status()
|
346
|
-
|
347
|
-
|
356
|
+
content_type = response.headers.get("Content-Type")
|
357
|
+
serializer = get_serializer(content_type)
|
358
|
+
decoded_response = serializer.deserialize(response.content)
|
359
|
+
return IndexifyData(
|
360
|
+
id=output_id, payload=decoded_response, encoder=serializer.encoding_type
|
361
|
+
)
|
348
362
|
|
349
363
|
def graph_outputs(
|
350
364
|
self,
|
indexify/remote_graph.py
CHANGED
@@ -39,7 +39,6 @@ class RemoteGraph:
|
|
39
39
|
:return: The invocation ID of the graph execution.
|
40
40
|
|
41
41
|
Example:
|
42
|
-
|
43
42
|
@indexify_function()
|
44
43
|
def foo(x: int) -> int:
|
45
44
|
return x + 1
|
@@ -48,16 +47,20 @@ class RemoteGraph:
|
|
48
47
|
invocation_id = remote_graph.run(x=1)
|
49
48
|
"""
|
50
49
|
return self._client.invoke_graph_with_object(
|
51
|
-
self._name,
|
50
|
+
self._name,
|
51
|
+
block_until_done,
|
52
|
+
self.graph.definition().get_input_payload_serializer(),
|
53
|
+
**kwargs
|
52
54
|
)
|
53
55
|
|
54
|
-
def
|
56
|
+
def replay_invocations(self):
|
55
57
|
"""
|
56
|
-
|
58
|
+
Replay all the graph previous runs/invocations on the latest version of the graph.
|
57
59
|
|
58
|
-
|
60
|
+
This is useful to make all the previous invocations go through
|
61
|
+
an updated graph to take advantage of graph improvements.
|
59
62
|
"""
|
60
|
-
self._client.
|
63
|
+
self._client.replay_invocations(self._name)
|
61
64
|
|
62
65
|
@classmethod
|
63
66
|
def deploy(
|
@@ -78,6 +81,7 @@ class RemoteGraph:
|
|
78
81
|
:param client: The IndexifyClient used to communicate with the server.
|
79
82
|
Prefered over server_url.
|
80
83
|
"""
|
84
|
+
cls.graph = g
|
81
85
|
if not client:
|
82
86
|
client = IndexifyClient(service_url=server_url)
|
83
87
|
client.register_compute_graph(g, additional_modules)
|
indexify/remote_pipeline.py
CHANGED
@@ -19,6 +19,7 @@ class RemotePipeline(RemoteGraph):
|
|
19
19
|
:param g: The local Graph object.
|
20
20
|
:param server_url: The URL of the server where the graph will be registered.
|
21
21
|
"""
|
22
|
+
cls.graph = p._graph
|
22
23
|
client = IndexifyClient(service_url=server_url)
|
23
24
|
client.register_compute_graph(p._graph, additional_modules)
|
24
25
|
return cls(name=p._graph.name, server_url=server_url)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: indexify
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.28
|
4
4
|
Summary: Python Client for Indexify
|
5
5
|
Home-page: https://github.com/tensorlakeai/indexify
|
6
6
|
License: Apache 2.0
|
@@ -18,13 +18,12 @@ Requires-Dist: cloudpickle (>=3.1.0,<4.0.0)
|
|
18
18
|
Requires-Dist: docker (>=7.1.0,<8.0.0)
|
19
19
|
Requires-Dist: httpx-sse (>=0.4.0,<0.5.0)
|
20
20
|
Requires-Dist: httpx[http2] (>=0,<1)
|
21
|
-
Requires-Dist: jsonpickle (>=
|
22
|
-
Requires-Dist: msgpack (>=1.1.0,<2.0.0)
|
21
|
+
Requires-Dist: jsonpickle (>=4.0.0,<5.0.0)
|
23
22
|
Requires-Dist: nanoid (>=2.0.0,<3.0.0)
|
24
23
|
Requires-Dist: pydantic (>=2.9.2,<3.0.0)
|
25
24
|
Requires-Dist: pyyaml (>=6,<7)
|
26
25
|
Requires-Dist: rich (>=13.9.2,<14.0.0)
|
27
|
-
Requires-Dist: typer (>=0.
|
26
|
+
Requires-Dist: typer (>=0.13.0,<0.14.0)
|
28
27
|
Project-URL: Repository, https://github.com/tensorlakeai/indexify
|
29
28
|
Description-Content-Type: text/markdown
|
30
29
|
|
@@ -1,34 +1,35 @@
|
|
1
1
|
indexify/__init__.py,sha256=P0mvM8sbkeS2CjYzRYyzb42CnXGhyJXdz4FdmTBMSWM,697
|
2
2
|
indexify/cli.py,sha256=TBycogrXcTK7q8IIcJxcpPPoZYve_rp6FYipWbkiTUI,8528
|
3
|
+
indexify/common_util.py,sha256=LKS6yZ3yv8nF2J-KzisGIjqjTvCn7tLFifQJLT4gHRg,3529
|
3
4
|
indexify/data_loaders/__init__.py,sha256=Y5NEuseTcYAICRiweYw5wBQ2m2YplbsY21I7df-rdi4,1339
|
4
5
|
indexify/data_loaders/local_directory_loader.py,sha256=fCrgj5drnW71ZUdDDvcB1-VJjIs1w6Q8sEW0HSGSAiA,1247
|
5
6
|
indexify/data_loaders/url_loader.py,sha256=32SERljcq1Xsi4RdLz2dgyk2TER5pQPTtXl3gUzwHbY,1533
|
6
7
|
indexify/error.py,sha256=qAWr8R6AxPkjsxHSzXTc8zqYnNO_AjOqqYEPsQvF1Zs,238
|
7
|
-
indexify/executor/agent.py,sha256=
|
8
|
+
indexify/executor/agent.py,sha256=mqEDcRdDfoUjw5XoFfVlOlJ-C_ODzyCQQMW90vDbWk4,18114
|
8
9
|
indexify/executor/api_objects.py,sha256=mvmwGbK4paJNQGFvbtNHMPpiH_LpVhrlRnCcrqS6HOQ,859
|
9
|
-
indexify/executor/downloader.py,sha256=
|
10
|
+
indexify/executor/downloader.py,sha256=_SQ-6-0Kj3Zg0Dqp_pMwTYxvBP3xvNUOREyDucCJp8M,4944
|
10
11
|
indexify/executor/executor_tasks.py,sha256=A0UIEZ5VaB6zSkFQG81UmTW0E57MTYhGlaXuAbRV8lQ,1884
|
11
|
-
indexify/executor/function_worker.py,sha256=
|
12
|
+
indexify/executor/function_worker.py,sha256=wRW2-X9dNI80KhwTD1vD-pcyetsVKVs6vVdg7L7JjcQ,6462
|
12
13
|
indexify/executor/image_dependency_installer.py,sha256=ct8GmzgkaPi6NAblk68IJJWo5MecIUubELotmSrgoRQ,1759
|
13
14
|
indexify/executor/indexify_executor.py,sha256=2Ut_VX-Su_lm4b4aEROyRJ3gXx-uFHA-V7EN0sWiARE,771
|
14
15
|
indexify/executor/runtime_probes.py,sha256=mjw2_mGQ622wRT_39WPGGgPEZQTgtrf3-ICcUUZOeyg,2126
|
15
|
-
indexify/executor/task_reporter.py,sha256=
|
16
|
+
indexify/executor/task_reporter.py,sha256=Wr8caDtjHBHvK5C-wwghJ5TNxIK_moxjQokGRcEs2rw,3798
|
16
17
|
indexify/executor/task_store.py,sha256=u48FdRKAh_KH7WOMQOArdOY5CawlyW5MJx8V0W79JM0,3951
|
17
|
-
indexify/functions_sdk/data_objects.py,sha256=
|
18
|
-
indexify/functions_sdk/graph.py,sha256=
|
19
|
-
indexify/functions_sdk/graph_definition.py,sha256=
|
18
|
+
indexify/functions_sdk/data_objects.py,sha256=wXbUa9hjU6rsXmmk19vQ5Kixf3FsI59VBWPNmHasAX0,854
|
19
|
+
indexify/functions_sdk/graph.py,sha256=TbHtIcAzRcEn3BWewVhNsUGMNfRduI1aSAvK6Vyx-fk,11801
|
20
|
+
indexify/functions_sdk/graph_definition.py,sha256=5M2I1FaWxmDucdirnaCYR4F_-8WHPBxyNLqKl_tennk,1293
|
20
21
|
indexify/functions_sdk/graph_validation.py,sha256=mN2Fcp91GIwFZEQP6z_qGqt4LkLM70SnI7AWBi4CmKQ,2509
|
21
22
|
indexify/functions_sdk/image.py,sha256=QK0H6KxLWriB_z4M0kunKzzHdHxYLWL670RPYgYuf_8,1762
|
22
|
-
indexify/functions_sdk/indexify_functions.py,sha256=
|
23
|
+
indexify/functions_sdk/indexify_functions.py,sha256=EbNC5gZktRc-G9ribQrpMG2X_G-8cujsxTgot734iGQ,10340
|
23
24
|
indexify/functions_sdk/local_cache.py,sha256=cNWF67zbhbTJe3g86hyLBy3Rqzs6dNvp2SjLazGZWvw,1348
|
24
|
-
indexify/functions_sdk/object_serializer.py,sha256=
|
25
|
+
indexify/functions_sdk/object_serializer.py,sha256=pOgUOWbRNRix9uZT0aQn0LTCnJCeMNGO1nAE0jAybmg,1546
|
25
26
|
indexify/functions_sdk/pipeline.py,sha256=KmxZE8eBFAQ4bbEcYURXXR26HSyoAT3O6iu9H38-OXE,974
|
26
|
-
indexify/http_client.py,sha256=
|
27
|
-
indexify/remote_graph.py,sha256=
|
28
|
-
indexify/remote_pipeline.py,sha256=
|
27
|
+
indexify/http_client.py,sha256=Gi67Z1_hGY9N-so_6YY-PobP6JparH5vWq1PDQp2SEw,15924
|
28
|
+
indexify/remote_graph.py,sha256=Qzc-xOYY8upwjX92FozIyx040HRr2aeuJMUiUkK3tDk,4670
|
29
|
+
indexify/remote_pipeline.py,sha256=oqx57rSPszNS3DToXO_nf-CKqkCZWptm1u_p3orV_gQ,790
|
29
30
|
indexify/settings.py,sha256=Ny59mzYI4gbXoK8hjx66a_men6ndbd1J1zCTcKOoyzg,50
|
30
|
-
indexify-0.2.
|
31
|
-
indexify-0.2.
|
32
|
-
indexify-0.2.
|
33
|
-
indexify-0.2.
|
34
|
-
indexify-0.2.
|
31
|
+
indexify-0.2.28.dist-info/LICENSE.txt,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
32
|
+
indexify-0.2.28.dist-info/METADATA,sha256=4L3k1UBoIMJUzmSlnPfM4yqiQbGhRG15J95HNkg-r4s,6202
|
33
|
+
indexify-0.2.28.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
34
|
+
indexify-0.2.28.dist-info/entry_points.txt,sha256=Pih7WV-XMpAzI5dEvROcpLr-ybVhd9Y-AtuzBKUdcDs,49
|
35
|
+
indexify-0.2.28.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|