indexify 0.2.27__py3-none-any.whl → 0.2.29__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.
@@ -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()
@@ -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: str,
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 open(config_path, "r") as f:
88
- config = yaml.safe_load(f)
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, executor_id=self._executor_id
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 httpx.AsyncClient() as client:
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}", namespace=namespace
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
 
@@ -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__(self, code_path: str, base_url: str):
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 = httpx.get(
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) -> IndexifyData:
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 = httpx.get(url)
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(payload=response.content, id=input_id)
115
+ input=IndexifyData(
116
+ payload=response.content, id=input_id, encoder=encoder
117
+ ),
104
118
  )
105
119
 
106
- init_value = None
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 = MsgPackSerializer.deserialize(init_value.content)
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=MsgPackSerializer.deserialize(response.content), init_value=init_value
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
- IndexifyFunction,
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 List, Optional
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 Task, TaskResult
9
+ from indexify.executor.api_objects import TaskResult
10
10
  from indexify.executor.task_store import CompletedTask
11
- from indexify.functions_sdk.data_objects import IndexifyData, RouterOutput
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__(self, base_url: str, executor_id: str):
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
- output_bytes = MsgPackSerializer.serialize(output)
38
+ serializer = get_serializer(output.encoder)
39
+ serialized_output = serializer.serialize(output.payload)
36
40
  fn_outputs.append(
37
- ("node_outputs", (nanoid.generate(), io.BytesIO(output_bytes)))
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
- (nanoid.generate(), io.BytesIO(completed_task.stdout.encode())),
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
- (nanoid.generate(), io.BytesIO(completed_task.stderr.encode())),
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 = httpx.post(
102
+ response = self._client.post(
88
103
  url=f"{self._base_url}/internal/ingest_files",
89
104
  **kwargs,
90
105
  )
@@ -16,7 +16,7 @@ class RouterOutput(BaseModel):
16
16
 
17
17
  class IndexifyData(BaseModel):
18
18
  id: Optional[str] = None
19
- payload: bytes
19
+ payload: Union[bytes, str]
20
20
  encoder: Literal["cloudpickle", "json"] = "cloudpickle"
21
21
 
22
22
 
@@ -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
- payload_encoder=start_node.encoder,
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
- payload_encoder=node.encoder,
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
- payload_encoder: str = "cloudpickle"
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
- payload_encoder: str = "cloudpickle"
27
+ encoder: str = "cloudpickle"
28
28
 
29
29
 
30
30
  class NodeMetadata(BaseModel):
@@ -49,3 +49,11 @@ class ComputeGraphMetadata(BaseModel):
49
49
 
50
50
  def get_input_payload_serializer(self):
51
51
  return get_serializer(self.start_node.compute_fn.encoder)
52
+
53
+ def get_input_encoder(self) -> str:
54
+ if self.start_node.compute_fn:
55
+ return self.start_node.compute_fn.encoder
56
+ elif self.start_node.dynamic_router:
57
+ return self.start_node.dynamic_router.encoder
58
+
59
+ raise ValueError("start node is not set on the graph")
@@ -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 msgpack
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
@@ -4,16 +4,16 @@ from typing import Any, Dict, List, Optional
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 get_serializer
17
17
  from indexify.settings import DEFAULT_SERVICE_URL
18
18
 
19
19
 
@@ -55,18 +55,8 @@ class IndexifyClient:
55
55
  service_url = os.environ["INDEXIFY_URL"]
56
56
 
57
57
  self.service_url = service_url
58
- self._client = httpx.Client()
59
- if config_path:
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
- )
58
+ self._config_path = config_path
59
+ self._client = get_httpx_client(config_path)
70
60
 
71
61
  self.namespace: str = namespace
72
62
  self.compute_graphs: List[Graph] = []
@@ -135,17 +125,13 @@ class IndexifyClient:
135
125
  if not (cert_path and key_path):
136
126
  raise ValueError("Both cert and key must be provided for mTLS")
137
127
 
138
- client_certs = (cert_path, key_path)
139
- verify_option = ca_bundle_path if ca_bundle_path else True
140
- client = IndexifyClient(
141
- *args,
142
- **kwargs,
143
- service_url=service_url,
144
- http2=True,
145
- cert=client_certs,
146
- verify=verify_option,
128
+ client = get_sync_or_async_client(
129
+ cert_path=cert_path, key_path=key_path, ca_bundle_path=ca_bundle_path
147
130
  )
148
- return client
131
+
132
+ indexify_client = IndexifyClient(service_url, *args, **kwargs)
133
+ indexify_client._client = client
134
+ return indexify_client
149
135
 
150
136
  def _add_api_key(self, kwargs):
151
137
  if self._api_key:
@@ -189,6 +175,20 @@ class IndexifyClient:
189
175
  for fn_name, fn in graph.nodes.items():
190
176
  self._fns[f"{graph.name}/{fn_name}"] = fn
191
177
 
178
+ def delete_compute_graph(
179
+ self,
180
+ graph_name: str,
181
+ ) -> None:
182
+ """
183
+ Deletes a graph and all of its invocations from the namespace.
184
+ :param graph_name The name of the graph to delete.
185
+ WARNING: This operation is irreversible.
186
+ """
187
+ response = self._delete(
188
+ f"namespaces/{self.namespace}/compute_graphs/{graph_name}",
189
+ )
190
+ response.raise_for_status()
191
+
192
192
  def graphs(self) -> List[str]:
193
193
  response = self._get(f"graphs")
194
194
  return response.json()["graphs"]
@@ -265,21 +265,26 @@ class IndexifyClient:
265
265
  print(f"failed to fetch logs: {e}")
266
266
  return None
267
267
 
268
- def rerun_graph(self, graph: str):
269
- self._post(f"namespaces/{self.namespace}/compute_graphs/{graph}/rerun")
268
+ def replay_invocations(self, graph: str):
269
+ self._post(f"namespaces/{self.namespace}/compute_graphs/{graph}/replay")
270
270
 
271
271
  def invoke_graph_with_object(
272
- self, graph: str, block_until_done: bool = False, **kwargs
272
+ self,
273
+ graph: str,
274
+ block_until_done: bool = False,
275
+ serializer: str = "cloudpickle",
276
+ **kwargs,
273
277
  ) -> str:
274
- ser_input = cloudpickle.dumps(kwargs)
278
+ serializer = get_serializer(serializer)
279
+ ser_input = serializer.serialize(kwargs)
275
280
  params = {"block_until_finish": block_until_done}
276
281
  kwargs = {
277
- "headers": {"Content-Type": "application/cbor"},
282
+ "headers": {"Content-Type": serializer.content_type},
278
283
  "data": ser_input,
279
284
  "params": params,
280
285
  }
281
286
  self._add_api_key(kwargs)
282
- with httpx.Client() as client:
287
+ with get_httpx_client(self._config_path) as client:
283
288
  with connect_sse(
284
289
  client,
285
290
  "POST",
@@ -343,8 +348,12 @@ class IndexifyClient:
343
348
  f"namespaces/{namespace}/compute_graphs/{graph}/invocations/{invocation_id}/fn/{fn_name}/output/{output_id}",
344
349
  )
345
350
  response.raise_for_status()
346
- data_dict = msgpack.unpackb(response.content)
347
- return IndexifyData.model_validate(data_dict)
351
+ content_type = response.headers.get("Content-Type")
352
+ serializer = get_serializer(content_type)
353
+ decoded_response = serializer.deserialize(response.content)
354
+ return IndexifyData(
355
+ id=output_id, payload=decoded_response, encoder=serializer.encoding_type
356
+ )
348
357
 
349
358
  def graph_outputs(
350
359
  self,
indexify/remote_graph.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from typing import Any, List, Optional
2
2
 
3
- from indexify.functions_sdk.graph import Graph
3
+ from indexify.functions_sdk.graph import ComputeGraphMetadata, Graph
4
4
 
5
5
  from .http_client import IndexifyClient
6
6
  from .settings import DEFAULT_SERVICE_URL
@@ -30,6 +30,8 @@ class RemoteGraph:
30
30
  else:
31
31
  self._client = IndexifyClient(service_url=server_url)
32
32
 
33
+ self._graph_definition: ComputeGraphMetadata = self._client.graph(self._name)
34
+
33
35
  def run(self, block_until_done: bool = False, **kwargs) -> str:
34
36
  """
35
37
  Run the graph with the given inputs. The input is for the start function of the graph.
@@ -39,7 +41,6 @@ class RemoteGraph:
39
41
  :return: The invocation ID of the graph execution.
40
42
 
41
43
  Example:
42
-
43
44
  @indexify_function()
44
45
  def foo(x: int) -> int:
45
46
  return x + 1
@@ -48,16 +49,20 @@ class RemoteGraph:
48
49
  invocation_id = remote_graph.run(x=1)
49
50
  """
50
51
  return self._client.invoke_graph_with_object(
51
- self._name, block_until_done, **kwargs
52
+ self._name,
53
+ block_until_done,
54
+ self._graph_definition.get_input_encoder(),
55
+ **kwargs
52
56
  )
53
57
 
54
- def rerun(self):
58
+ def replay_invocations(self):
55
59
  """
56
- Rerun the graph with the given invocation ID.
60
+ Replay all the graph previous runs/invocations on the latest version of the graph.
57
61
 
58
- :param invocation_id: The invocation ID of the graph execution.
62
+ This is useful to make all the previous invocations go through
63
+ an updated graph to take advantage of graph improvements.
59
64
  """
60
- self._client.rerun_graph(self._name)
65
+ self._client.replay_invocations(self._name)
61
66
 
62
67
  @classmethod
63
68
  def deploy(
@@ -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.27
3
+ Version: 0.2.29
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 (>=3.3.0,<4.0.0)
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.12.5,<0.13.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=5TPCbeqjGg_3_EsBY8o3HmtZl8GSNEKYDiUA4SKJg0Y,18692
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=3mEDdluTzspsLGAZtFHZOVuyKOzT3CSema2kIK6Z1yU,4005
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=mFvDwbPiX6QHTkZALqBOAYix4kHSNSAF8KDImB8OTWM,6462
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=7M2fDzLDkSNOSr42EBtkIbGvBzeZlTCqxQuDfdoFT4Y,3349
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=G08Nrfe2nMUXEhMSedIZwNcZLEsEnj0NykmLPaMZmFs,842
18
- indexify/functions_sdk/graph.py,sha256=a28o07g0MJoBq2rQWfMGK_dtiSyryoADeF7tEuhI8gc,11817
19
- indexify/functions_sdk/graph_definition.py,sha256=s8NbqZRaVw1mhGvaaUZ2cJ0NitdlbLj1tnkKjsc71U4,1309
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=5jUzjfk4gxxQCmQeOlNS9VndZK7KQBsZzABcDpQKgrs,1594
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=UspEi8wYWf4lZ0f2Ln19RbQigocSPr23lgMvD9E-PS0,10475
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=Nw7PqhSpkw1nCw5jo0XDBumzO2crLMIohSuIMKLaEOk,2161
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=mGSSZSMJOShoZGDq42f0kiCQ_aIg2ZjWKeaOW35_r8g,15400
27
- indexify/remote_graph.py,sha256=_04bdraDaeEmkvEcsxIZ8M37BCAjfbfQccZpAgnZJ2c,4435
28
- indexify/remote_pipeline.py,sha256=FW7IAv3r24OOpiqlprw3kuFrpdkqi6Ic4_tT26FThjA,761
27
+ indexify/http_client.py,sha256=rFYjv0rRMo4sXp-TjWoflQeJaxNZK-Di6Gk3G78xKQI,15838
28
+ indexify/remote_graph.py,sha256=1JogFFcWCKc_Dq4Rh1s6IwAnxB-qrFnKuDQIbpJso1Y,4745
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.27.dist-info/LICENSE.txt,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
31
- indexify-0.2.27.dist-info/METADATA,sha256=Ok21hFzjW-T6uEn4fNrZC1ioUeDvmbQdUvlZcYNg7KU,6242
32
- indexify-0.2.27.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
33
- indexify-0.2.27.dist-info/entry_points.txt,sha256=Pih7WV-XMpAzI5dEvROcpLr-ybVhd9Y-AtuzBKUdcDs,49
34
- indexify-0.2.27.dist-info/RECORD,,
31
+ indexify-0.2.29.dist-info/LICENSE.txt,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
32
+ indexify-0.2.29.dist-info/METADATA,sha256=3lC5Cqd9Uym3Bk0AvQVTT8JTlHsodWXyktGjEAU9Y7s,6202
33
+ indexify-0.2.29.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
34
+ indexify-0.2.29.dist-info/entry_points.txt,sha256=Pih7WV-XMpAzI5dEvROcpLr-ybVhd9Y-AtuzBKUdcDs,49
35
+ indexify-0.2.29.dist-info/RECORD,,