nv-ingest-api 26.1.0rc4__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.

Potentially problematic release.


This version of nv-ingest-api might be problematic. Click here for more details.

Files changed (177) hide show
  1. nv_ingest_api/__init__.py +3 -0
  2. nv_ingest_api/interface/__init__.py +218 -0
  3. nv_ingest_api/interface/extract.py +977 -0
  4. nv_ingest_api/interface/mutate.py +154 -0
  5. nv_ingest_api/interface/store.py +200 -0
  6. nv_ingest_api/interface/transform.py +382 -0
  7. nv_ingest_api/interface/utility.py +186 -0
  8. nv_ingest_api/internal/__init__.py +0 -0
  9. nv_ingest_api/internal/enums/__init__.py +3 -0
  10. nv_ingest_api/internal/enums/common.py +550 -0
  11. nv_ingest_api/internal/extract/__init__.py +3 -0
  12. nv_ingest_api/internal/extract/audio/__init__.py +3 -0
  13. nv_ingest_api/internal/extract/audio/audio_extraction.py +202 -0
  14. nv_ingest_api/internal/extract/docx/__init__.py +5 -0
  15. nv_ingest_api/internal/extract/docx/docx_extractor.py +232 -0
  16. nv_ingest_api/internal/extract/docx/engines/__init__.py +0 -0
  17. nv_ingest_api/internal/extract/docx/engines/docxreader_helpers/__init__.py +3 -0
  18. nv_ingest_api/internal/extract/docx/engines/docxreader_helpers/docx_helper.py +127 -0
  19. nv_ingest_api/internal/extract/docx/engines/docxreader_helpers/docxreader.py +971 -0
  20. nv_ingest_api/internal/extract/html/__init__.py +3 -0
  21. nv_ingest_api/internal/extract/html/html_extractor.py +84 -0
  22. nv_ingest_api/internal/extract/image/__init__.py +3 -0
  23. nv_ingest_api/internal/extract/image/chart_extractor.py +375 -0
  24. nv_ingest_api/internal/extract/image/image_extractor.py +208 -0
  25. nv_ingest_api/internal/extract/image/image_helpers/__init__.py +3 -0
  26. nv_ingest_api/internal/extract/image/image_helpers/common.py +433 -0
  27. nv_ingest_api/internal/extract/image/infographic_extractor.py +290 -0
  28. nv_ingest_api/internal/extract/image/ocr_extractor.py +407 -0
  29. nv_ingest_api/internal/extract/image/table_extractor.py +391 -0
  30. nv_ingest_api/internal/extract/pdf/__init__.py +3 -0
  31. nv_ingest_api/internal/extract/pdf/engines/__init__.py +19 -0
  32. nv_ingest_api/internal/extract/pdf/engines/adobe.py +484 -0
  33. nv_ingest_api/internal/extract/pdf/engines/llama.py +246 -0
  34. nv_ingest_api/internal/extract/pdf/engines/nemotron_parse.py +598 -0
  35. nv_ingest_api/internal/extract/pdf/engines/pdf_helpers/__init__.py +166 -0
  36. nv_ingest_api/internal/extract/pdf/engines/pdfium.py +652 -0
  37. nv_ingest_api/internal/extract/pdf/engines/tika.py +96 -0
  38. nv_ingest_api/internal/extract/pdf/engines/unstructured_io.py +426 -0
  39. nv_ingest_api/internal/extract/pdf/pdf_extractor.py +74 -0
  40. nv_ingest_api/internal/extract/pptx/__init__.py +5 -0
  41. nv_ingest_api/internal/extract/pptx/engines/__init__.py +0 -0
  42. nv_ingest_api/internal/extract/pptx/engines/pptx_helper.py +968 -0
  43. nv_ingest_api/internal/extract/pptx/pptx_extractor.py +210 -0
  44. nv_ingest_api/internal/meta/__init__.py +3 -0
  45. nv_ingest_api/internal/meta/udf.py +232 -0
  46. nv_ingest_api/internal/mutate/__init__.py +3 -0
  47. nv_ingest_api/internal/mutate/deduplicate.py +110 -0
  48. nv_ingest_api/internal/mutate/filter.py +133 -0
  49. nv_ingest_api/internal/primitives/__init__.py +0 -0
  50. nv_ingest_api/internal/primitives/control_message_task.py +16 -0
  51. nv_ingest_api/internal/primitives/ingest_control_message.py +307 -0
  52. nv_ingest_api/internal/primitives/nim/__init__.py +9 -0
  53. nv_ingest_api/internal/primitives/nim/default_values.py +14 -0
  54. nv_ingest_api/internal/primitives/nim/model_interface/__init__.py +3 -0
  55. nv_ingest_api/internal/primitives/nim/model_interface/cached.py +274 -0
  56. nv_ingest_api/internal/primitives/nim/model_interface/decorators.py +56 -0
  57. nv_ingest_api/internal/primitives/nim/model_interface/deplot.py +270 -0
  58. nv_ingest_api/internal/primitives/nim/model_interface/helpers.py +338 -0
  59. nv_ingest_api/internal/primitives/nim/model_interface/nemotron_parse.py +239 -0
  60. nv_ingest_api/internal/primitives/nim/model_interface/ocr.py +776 -0
  61. nv_ingest_api/internal/primitives/nim/model_interface/parakeet.py +367 -0
  62. nv_ingest_api/internal/primitives/nim/model_interface/text_embedding.py +129 -0
  63. nv_ingest_api/internal/primitives/nim/model_interface/vlm.py +177 -0
  64. nv_ingest_api/internal/primitives/nim/model_interface/yolox.py +1681 -0
  65. nv_ingest_api/internal/primitives/nim/nim_client.py +801 -0
  66. nv_ingest_api/internal/primitives/nim/nim_model_interface.py +126 -0
  67. nv_ingest_api/internal/primitives/tracing/__init__.py +0 -0
  68. nv_ingest_api/internal/primitives/tracing/latency.py +69 -0
  69. nv_ingest_api/internal/primitives/tracing/logging.py +96 -0
  70. nv_ingest_api/internal/primitives/tracing/tagging.py +288 -0
  71. nv_ingest_api/internal/schemas/__init__.py +3 -0
  72. nv_ingest_api/internal/schemas/extract/__init__.py +3 -0
  73. nv_ingest_api/internal/schemas/extract/extract_audio_schema.py +133 -0
  74. nv_ingest_api/internal/schemas/extract/extract_chart_schema.py +144 -0
  75. nv_ingest_api/internal/schemas/extract/extract_docx_schema.py +129 -0
  76. nv_ingest_api/internal/schemas/extract/extract_html_schema.py +34 -0
  77. nv_ingest_api/internal/schemas/extract/extract_image_schema.py +126 -0
  78. nv_ingest_api/internal/schemas/extract/extract_infographic_schema.py +137 -0
  79. nv_ingest_api/internal/schemas/extract/extract_ocr_schema.py +137 -0
  80. nv_ingest_api/internal/schemas/extract/extract_pdf_schema.py +220 -0
  81. nv_ingest_api/internal/schemas/extract/extract_pptx_schema.py +128 -0
  82. nv_ingest_api/internal/schemas/extract/extract_table_schema.py +137 -0
  83. nv_ingest_api/internal/schemas/message_brokers/__init__.py +3 -0
  84. nv_ingest_api/internal/schemas/message_brokers/message_broker_client_schema.py +37 -0
  85. nv_ingest_api/internal/schemas/message_brokers/request_schema.py +34 -0
  86. nv_ingest_api/internal/schemas/message_brokers/response_schema.py +19 -0
  87. nv_ingest_api/internal/schemas/meta/__init__.py +3 -0
  88. nv_ingest_api/internal/schemas/meta/base_model_noext.py +11 -0
  89. nv_ingest_api/internal/schemas/meta/ingest_job_schema.py +355 -0
  90. nv_ingest_api/internal/schemas/meta/metadata_schema.py +394 -0
  91. nv_ingest_api/internal/schemas/meta/udf.py +23 -0
  92. nv_ingest_api/internal/schemas/mixins.py +39 -0
  93. nv_ingest_api/internal/schemas/mutate/__init__.py +3 -0
  94. nv_ingest_api/internal/schemas/mutate/mutate_image_dedup_schema.py +16 -0
  95. nv_ingest_api/internal/schemas/store/__init__.py +3 -0
  96. nv_ingest_api/internal/schemas/store/store_embedding_schema.py +28 -0
  97. nv_ingest_api/internal/schemas/store/store_image_schema.py +45 -0
  98. nv_ingest_api/internal/schemas/transform/__init__.py +3 -0
  99. nv_ingest_api/internal/schemas/transform/transform_image_caption_schema.py +36 -0
  100. nv_ingest_api/internal/schemas/transform/transform_image_filter_schema.py +17 -0
  101. nv_ingest_api/internal/schemas/transform/transform_text_embedding_schema.py +48 -0
  102. nv_ingest_api/internal/schemas/transform/transform_text_splitter_schema.py +24 -0
  103. nv_ingest_api/internal/store/__init__.py +3 -0
  104. nv_ingest_api/internal/store/embed_text_upload.py +236 -0
  105. nv_ingest_api/internal/store/image_upload.py +251 -0
  106. nv_ingest_api/internal/transform/__init__.py +3 -0
  107. nv_ingest_api/internal/transform/caption_image.py +219 -0
  108. nv_ingest_api/internal/transform/embed_text.py +702 -0
  109. nv_ingest_api/internal/transform/split_text.py +182 -0
  110. nv_ingest_api/util/__init__.py +3 -0
  111. nv_ingest_api/util/control_message/__init__.py +0 -0
  112. nv_ingest_api/util/control_message/validators.py +47 -0
  113. nv_ingest_api/util/converters/__init__.py +0 -0
  114. nv_ingest_api/util/converters/bytetools.py +78 -0
  115. nv_ingest_api/util/converters/containers.py +65 -0
  116. nv_ingest_api/util/converters/datetools.py +90 -0
  117. nv_ingest_api/util/converters/dftools.py +127 -0
  118. nv_ingest_api/util/converters/formats.py +64 -0
  119. nv_ingest_api/util/converters/type_mappings.py +27 -0
  120. nv_ingest_api/util/dataloader/__init__.py +9 -0
  121. nv_ingest_api/util/dataloader/dataloader.py +409 -0
  122. nv_ingest_api/util/detectors/__init__.py +5 -0
  123. nv_ingest_api/util/detectors/language.py +38 -0
  124. nv_ingest_api/util/exception_handlers/__init__.py +0 -0
  125. nv_ingest_api/util/exception_handlers/converters.py +72 -0
  126. nv_ingest_api/util/exception_handlers/decorators.py +429 -0
  127. nv_ingest_api/util/exception_handlers/detectors.py +74 -0
  128. nv_ingest_api/util/exception_handlers/pdf.py +116 -0
  129. nv_ingest_api/util/exception_handlers/schemas.py +68 -0
  130. nv_ingest_api/util/image_processing/__init__.py +5 -0
  131. nv_ingest_api/util/image_processing/clustering.py +260 -0
  132. nv_ingest_api/util/image_processing/processing.py +177 -0
  133. nv_ingest_api/util/image_processing/table_and_chart.py +504 -0
  134. nv_ingest_api/util/image_processing/transforms.py +850 -0
  135. nv_ingest_api/util/imports/__init__.py +3 -0
  136. nv_ingest_api/util/imports/callable_signatures.py +108 -0
  137. nv_ingest_api/util/imports/dynamic_resolvers.py +158 -0
  138. nv_ingest_api/util/introspection/__init__.py +3 -0
  139. nv_ingest_api/util/introspection/class_inspect.py +145 -0
  140. nv_ingest_api/util/introspection/function_inspect.py +65 -0
  141. nv_ingest_api/util/logging/__init__.py +0 -0
  142. nv_ingest_api/util/logging/configuration.py +102 -0
  143. nv_ingest_api/util/logging/sanitize.py +84 -0
  144. nv_ingest_api/util/message_brokers/__init__.py +3 -0
  145. nv_ingest_api/util/message_brokers/qos_scheduler.py +283 -0
  146. nv_ingest_api/util/message_brokers/simple_message_broker/__init__.py +9 -0
  147. nv_ingest_api/util/message_brokers/simple_message_broker/broker.py +465 -0
  148. nv_ingest_api/util/message_brokers/simple_message_broker/ordered_message_queue.py +71 -0
  149. nv_ingest_api/util/message_brokers/simple_message_broker/simple_client.py +455 -0
  150. nv_ingest_api/util/metadata/__init__.py +5 -0
  151. nv_ingest_api/util/metadata/aggregators.py +516 -0
  152. nv_ingest_api/util/multi_processing/__init__.py +8 -0
  153. nv_ingest_api/util/multi_processing/mp_pool_singleton.py +200 -0
  154. nv_ingest_api/util/nim/__init__.py +161 -0
  155. nv_ingest_api/util/pdf/__init__.py +3 -0
  156. nv_ingest_api/util/pdf/pdfium.py +428 -0
  157. nv_ingest_api/util/schema/__init__.py +3 -0
  158. nv_ingest_api/util/schema/schema_validator.py +10 -0
  159. nv_ingest_api/util/service_clients/__init__.py +3 -0
  160. nv_ingest_api/util/service_clients/client_base.py +86 -0
  161. nv_ingest_api/util/service_clients/kafka/__init__.py +3 -0
  162. nv_ingest_api/util/service_clients/redis/__init__.py +3 -0
  163. nv_ingest_api/util/service_clients/redis/redis_client.py +983 -0
  164. nv_ingest_api/util/service_clients/rest/__init__.py +0 -0
  165. nv_ingest_api/util/service_clients/rest/rest_client.py +595 -0
  166. nv_ingest_api/util/string_processing/__init__.py +51 -0
  167. nv_ingest_api/util/string_processing/configuration.py +682 -0
  168. nv_ingest_api/util/string_processing/yaml.py +109 -0
  169. nv_ingest_api/util/system/__init__.py +0 -0
  170. nv_ingest_api/util/system/hardware_info.py +594 -0
  171. nv_ingest_api-26.1.0rc4.dist-info/METADATA +237 -0
  172. nv_ingest_api-26.1.0rc4.dist-info/RECORD +177 -0
  173. nv_ingest_api-26.1.0rc4.dist-info/WHEEL +5 -0
  174. nv_ingest_api-26.1.0rc4.dist-info/licenses/LICENSE +201 -0
  175. nv_ingest_api-26.1.0rc4.dist-info/top_level.txt +2 -0
  176. udfs/__init__.py +5 -0
  177. udfs/llm_summarizer_udf.py +259 -0
File without changes
@@ -0,0 +1,595 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2024-25, NVIDIA CORPORATION & AFFILIATES.
2
+ # All rights reserved.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import logging
6
+ import re
7
+ import time
8
+ from typing import Any, Union, Tuple, Optional, Dict, Callable
9
+ from urllib.parse import urlparse
10
+
11
+ import requests
12
+
13
+ from nv_ingest_api.internal.schemas.message_brokers.response_schema import (
14
+ ResponseSchema,
15
+ )
16
+ from nv_ingest_api.util.service_clients.client_base import MessageBrokerClientBase
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # HTTP Response Statuses that result in marking submission as failed
21
+ # 4XX - Any 4XX status is considered a client derived error and will result in failure
22
+ # 5XX - Not all 500's are terminal but most are. Those which are listed below
23
+ _TERMINAL_RESPONSE_STATUSES = [
24
+ 400,
25
+ 401,
26
+ 402,
27
+ 403,
28
+ 404,
29
+ 405,
30
+ 406,
31
+ 407,
32
+ 408,
33
+ 409,
34
+ 410,
35
+ 411,
36
+ 412,
37
+ 413,
38
+ 414,
39
+ 415,
40
+ 416,
41
+ 417,
42
+ 418,
43
+ 421,
44
+ 422,
45
+ 423,
46
+ 424,
47
+ 425,
48
+ 426,
49
+ 428,
50
+ 429,
51
+ 431,
52
+ 451,
53
+ 500,
54
+ 501,
55
+ 503,
56
+ 505,
57
+ 506,
58
+ 507,
59
+ 508,
60
+ 510,
61
+ 511,
62
+ ]
63
+
64
+
65
+ class RestClient(MessageBrokerClientBase):
66
+ """
67
+ A client for interfacing with an HTTP endpoint (e.g., nv-ingest), providing mechanisms for sending
68
+ and receiving messages with retry logic using the `requests` library by default, but allowing a custom
69
+ HTTP client allocator.
70
+
71
+ Extends MessageBrokerClientBase for interface compatibility.
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ host: str,
77
+ port: int,
78
+ max_retries: int = 0,
79
+ max_backoff: int = 32,
80
+ default_connect_timeout: float = 300.0,
81
+ default_read_timeout: Optional[float] = None,
82
+ http_allocator: Optional[Callable[[], Any]] = None,
83
+ **kwargs,
84
+ ) -> None:
85
+ """
86
+ Initializes the RestClient.
87
+
88
+ By default, uses `requests.Session`. If `http_allocator` is provided, it will be called to instantiate
89
+ the client. If a custom allocator is used, the internal methods (`fetch_message`, `submit_message`)
90
+ might need adjustments if the allocated client's API differs significantly from `requests.Session`.
91
+
92
+ Parameters
93
+ ----------
94
+ host : str
95
+ The hostname or IP address of the HTTP server.
96
+ port : int
97
+ The port number of the HTTP server.
98
+ max_retries : int, optional
99
+ Maximum number of retry attempts for connection errors or specific retryable HTTP statuses. Default is 0.
100
+ max_backoff : int, optional
101
+ Maximum backoff delay between retries, in seconds. Default is 32.
102
+ default_connect_timeout : float, optional
103
+ Default timeout in seconds for establishing a connection. Default is 300.0.
104
+ default_read_timeout : float, optional
105
+ Default timeout in seconds for waiting for data after connection. Default is None.
106
+ http_allocator : Optional[Callable[[], Any]], optional
107
+ A callable that returns an HTTP client instance. If None, `requests.Session()` is used.
108
+ **kwargs : dict
109
+ Additional keyword arguments. Supported keys:
110
+ - api_version : str, optional
111
+ API version to use ('v1' or 'v2'). Defaults to 'v1' if not specified.
112
+ Invalid versions will log a warning and fall back to 'v1'.
113
+ - base_url : str, optional
114
+ Override the generated base URL.
115
+ - headers : dict, optional
116
+ Additional headers to include in requests.
117
+ - auth : optional
118
+ Authentication configuration for requests.
119
+
120
+ Returns
121
+ -------
122
+ None
123
+ """
124
+ self._host: str = host
125
+ self._port: int = port
126
+ self._max_retries: int = max_retries
127
+ self._max_backoff: int = max_backoff
128
+ self._default_connect_timeout: float = default_connect_timeout
129
+ self._default_read_timeout: Optional[float] = default_read_timeout
130
+ self._http_allocator: Optional[Callable[[], Any]] = http_allocator
131
+
132
+ self._timeout: Tuple[float, Optional[float]] = (
133
+ self._default_connect_timeout,
134
+ default_read_timeout,
135
+ )
136
+
137
+ if self._http_allocator is None:
138
+ self._client: Any = requests.Session()
139
+ logger.debug("RestClient initialized using default requests.Session.")
140
+ else:
141
+ try:
142
+ self._client = self._http_allocator()
143
+ logger.debug(f"RestClient initialized using provided http_allocator: {self._http_allocator.__name__}")
144
+ if not isinstance(self._client, requests.Session):
145
+ logger.warning(
146
+ "Provided http_allocator does not create a requests.Session. "
147
+ "Internal HTTP calls may fail if the client API is incompatible."
148
+ )
149
+ except Exception as e:
150
+ logger.exception(
151
+ f"Failed to instantiate client using provided http_allocator: {e}. "
152
+ f"Falling back to requests.Session."
153
+ )
154
+ self._client = requests.Session()
155
+
156
+ # Validate and normalize API version to prevent misconfiguration
157
+ # Default to v1 for backwards compatibility if not explicitly provided
158
+ VALID_API_VERSIONS = {"v1", "v2"}
159
+ raw_api_version = kwargs.get("api_version", "v2")
160
+ api_version = str(raw_api_version).strip().lower()
161
+
162
+ if api_version not in VALID_API_VERSIONS:
163
+ logger.warning(
164
+ f"Invalid API version '{raw_api_version}' specified. "
165
+ f"Valid versions are: {VALID_API_VERSIONS}. Falling back to 'v1'."
166
+ )
167
+ api_version = "v1"
168
+
169
+ self._api_version = api_version
170
+ self._submit_endpoint: str = f"/{api_version}/submit_job"
171
+ self._fetch_endpoint: str = f"/{api_version}/fetch_job"
172
+ self._base_url: str = kwargs.get("base_url") or self._generate_url(self._host, self._port)
173
+ self._headers = kwargs.get("headers", {})
174
+ self._auth = kwargs.get("auth", None)
175
+
176
+ logger.debug(f"RestClient base URL set to: {self._base_url}")
177
+ logger.info(
178
+ f"RestClient using API version: {api_version} (endpoints: {self._submit_endpoint}, {self._fetch_endpoint})"
179
+ )
180
+
181
+ @staticmethod
182
+ def _generate_url(host: str, port: int) -> str:
183
+ """
184
+ Constructs a base URL from host and port, intelligently handling schemes and existing ports.
185
+
186
+ Parameters
187
+ ----------
188
+ host : str
189
+ Hostname, IP address, or full URL (e.g., "localhost", "192.168.1.100",
190
+ "http://example.com", "https://api.example.com:8443/v1").
191
+ port : int
192
+ The default port number to use if the host string does not explicitly specify one.
193
+
194
+ Returns
195
+ -------
196
+ str
197
+ A fully constructed base URL string, including scheme, hostname, port,
198
+ and any original path, without a trailing slash.
199
+
200
+ Raises
201
+ ------
202
+ ValueError
203
+ If the host string appears to be a URL but lacks a valid hostname.
204
+ """
205
+ url_str: str = str(host).strip()
206
+ scheme: str = "http"
207
+ parsed_path: Optional[str] = None
208
+ effective_port: int = port
209
+ hostname: Optional[str] = None
210
+
211
+ if re.match(r"^https?://", url_str, re.IGNORECASE):
212
+ parsed_url = urlparse(url_str)
213
+ hostname = parsed_url.hostname
214
+ if hostname is None:
215
+ raise ValueError(f"Invalid URL provided in host string: '{url_str}'. Could not parse a valid hostname.")
216
+ scheme = parsed_url.scheme
217
+ if parsed_url.port is not None:
218
+ effective_port = parsed_url.port
219
+ else:
220
+ effective_port = port
221
+ if parsed_url.path and parsed_url.path.strip("/"):
222
+ parsed_path = parsed_url.path
223
+ else:
224
+ hostname = url_str
225
+ effective_port = port
226
+
227
+ if not hostname:
228
+ raise ValueError(f"Could not determine a valid hostname from input: '{host}'")
229
+
230
+ base_url: str = f"{scheme}://{hostname}:{effective_port}"
231
+ if parsed_path:
232
+ if not parsed_path.startswith("/"):
233
+ parsed_path = "/" + parsed_path
234
+ base_url += parsed_path
235
+
236
+ final_url: str = base_url.rstrip("/")
237
+ logger.debug(f"Generated base URL: {final_url}")
238
+ return final_url
239
+
240
+ @property
241
+ def max_retries(self) -> int:
242
+ """
243
+ Maximum number of retry attempts configured for operations.
244
+
245
+ Returns
246
+ -------
247
+ int
248
+ The maximum number of retries.
249
+ """
250
+ return self._max_retries
251
+
252
+ @max_retries.setter
253
+ def max_retries(self, value: int) -> None:
254
+ """
255
+ Sets the maximum number of retry attempts.
256
+
257
+ Parameters
258
+ ----------
259
+ value : int
260
+ The new maximum number of retries. Must be a non-negative integer.
261
+
262
+ Raises
263
+ ------
264
+ ValueError
265
+ If value is not a non-negative integer.
266
+ """
267
+ if not isinstance(value, int) or value < 0:
268
+ raise ValueError("max_retries must be a non-negative integer.")
269
+ self._max_retries = value
270
+
271
+ def get_client(self) -> Any:
272
+ """
273
+ Returns the underlying HTTP client instance.
274
+
275
+ Returns
276
+ -------
277
+ Any
278
+ The active HTTP client instance.
279
+ """
280
+ return self._client
281
+
282
+ def ping(self) -> "ResponseSchema":
283
+ """
284
+ Checks if the HTTP server endpoint is responsive using an HTTP GET request.
285
+
286
+ Returns
287
+ -------
288
+ ResponseSchema
289
+ An object encapsulating the outcome:
290
+ - response_code = 0 indicates success (HTTP status code < 400).
291
+ - response_code = 1 indicates failure, with details in response_reason.
292
+ """
293
+ ping_timeout: Tuple[float, float] = (
294
+ min(self._default_connect_timeout, 5.0),
295
+ 10.0,
296
+ )
297
+ logger.debug(f"Attempting to ping server at {self._base_url} with timeout {ping_timeout}")
298
+ try:
299
+ if isinstance(self._client, requests.Session):
300
+ response: requests.Response = self._client.get(self._base_url, timeout=ping_timeout)
301
+ response.raise_for_status()
302
+ logger.debug(f"Ping successful to {self._base_url} (Status: {response.status_code})")
303
+ return ResponseSchema(response_code=0, response_reason="Ping OK")
304
+ except requests.exceptions.RequestException as e:
305
+ error_reason: str = f"Ping failed due to RequestException for {self._base_url}: {e}"
306
+ logger.warning(error_reason)
307
+ return ResponseSchema(response_code=1, response_reason=error_reason)
308
+ except Exception as e:
309
+ error_reason: str = f"Unexpected error during ping to {self._base_url}: {e}"
310
+ logger.exception(error_reason)
311
+ return ResponseSchema(response_code=1, response_reason=error_reason)
312
+
313
+ def fetch_message(
314
+ self, job_id: str, timeout: Optional[Union[float, Tuple[float, float]]] = None
315
+ ) -> "ResponseSchema":
316
+ """
317
+ Fetches a job result message from the server's fetch endpoint.
318
+
319
+ Handles retries for connection errors and non-terminal HTTP errors based on the max_retries configuration.
320
+ Specific HTTP statuses are treated as immediate failures (terminal) or as job not ready (HTTP 202).
321
+
322
+ Parameters
323
+ ----------
324
+ job_id : str
325
+ The server-assigned identifier of the job to fetch.
326
+ timeout : float or tuple of float, optional
327
+ Specific timeout override for this request.
328
+
329
+ Returns
330
+ -------
331
+ ResponseSchema
332
+ - response_code = 0: Success (HTTP 200) with the job result.
333
+ - response_code = 1: Terminal failure (e.g., 404, 400, 5xx, or max retries exceeded).
334
+ - response_code = 2: Job not ready (HTTP 202).
335
+
336
+ Raises
337
+ ------
338
+ TypeError
339
+ If the configured client does not support the required HTTP GET method.
340
+ """
341
+ # Ensure headers are included
342
+ headers = {"Content-Type": "application/json"}
343
+ headers.update(self._headers)
344
+
345
+ retries: int = 0
346
+ url: str = f"{self._base_url}{self._fetch_endpoint}/{job_id}"
347
+ # Derive per-call timeout if provided; otherwise use default
348
+ if timeout is None:
349
+ req_timeout: Tuple[float, Optional[float]] = self._timeout
350
+ else:
351
+ if isinstance(timeout, tuple):
352
+ # Expect (connect, read)
353
+ connect_t = float(timeout[0])
354
+ read_t = None if (len(timeout) < 2 or timeout[1] is None) else float(timeout[1])
355
+ req_timeout = (connect_t, read_t)
356
+ else:
357
+ # Single float means override read timeout, keep a small connect timeout
358
+ req_timeout = (min(self._default_connect_timeout, 5.0), float(timeout))
359
+
360
+ while True:
361
+ result: Optional[Any] = None
362
+ trace_id: Optional[str] = job_id
363
+ response_code: int = -1
364
+
365
+ try:
366
+ if isinstance(self._client, requests.Session):
367
+ with self._client.get(
368
+ url,
369
+ timeout=req_timeout,
370
+ headers=headers,
371
+ stream=True,
372
+ auth=self._auth,
373
+ ) as result:
374
+ response_code = result.status_code
375
+ response_text = result.text
376
+
377
+ if response_code in _TERMINAL_RESPONSE_STATUSES:
378
+ error_reason: str = f"Terminal response code {response_code} fetching {job_id}."
379
+ logger.error(f"{error_reason} Response: {response_text[:200]}")
380
+ return ResponseSchema(
381
+ response_code=1,
382
+ response_reason=error_reason,
383
+ response=response_text,
384
+ trace_id=trace_id,
385
+ )
386
+ elif response_code == 200:
387
+ try:
388
+ full_response: str = b"".join(c for c in result.iter_content(1024 * 1024) if c).decode(
389
+ "utf-8"
390
+ )
391
+ return ResponseSchema(
392
+ response_code=0,
393
+ response_reason="OK",
394
+ response=full_response,
395
+ trace_id=trace_id,
396
+ )
397
+ except Exception as e:
398
+ logger.error(f"Stream processing error for {job_id}: {e}")
399
+ return ResponseSchema(
400
+ response_code=1,
401
+ response_reason=f"Stream processing error: {e}",
402
+ trace_id=trace_id,
403
+ )
404
+ elif response_code == 202:
405
+ logger.debug(f"Job {job_id} not ready (202)")
406
+ return ResponseSchema(
407
+ response_code=2,
408
+ response_reason="Job not ready yet. Retry later.",
409
+ trace_id=trace_id,
410
+ )
411
+ else:
412
+ logger.warning(f"Unexpected status {response_code} for {job_id}. Retrying if possible.")
413
+ else:
414
+ raise TypeError(
415
+ f"Unsupported client type for fetch_message: {type(self._client)}. "
416
+ f"Requires a requests.Session compatible API."
417
+ )
418
+ except requests.exceptions.RequestException as err:
419
+ logger.debug(
420
+ f"RequestException fetching {job_id}: {err}. "
421
+ f"Attempting retry ({retries + 1}/{self._max_retries})..."
422
+ )
423
+ try:
424
+ retries = self.perform_retry_backoff(retries)
425
+ continue
426
+ except RuntimeError as rte:
427
+ logger.error(f"Max retries hit fetching {job_id} after RequestException: {rte}")
428
+ return ResponseSchema(response_code=1, response_reason=str(rte), response=str(err))
429
+ except Exception as e:
430
+ logger.exception(f"Unexpected error fetching {job_id}: {e}")
431
+ return ResponseSchema(response_code=1, response_reason=f"Unexpected fetch error: {e}")
432
+
433
+ try:
434
+ retries = self.perform_retry_backoff(retries)
435
+ continue
436
+ except RuntimeError as rte:
437
+ logger.error(f"Max retries hit fetching {job_id} after HTTP {response_code}: {rte}")
438
+ resp_text_snippet: Optional[str] = response_text[:500] if "response_text" in locals() else None
439
+ return ResponseSchema(
440
+ response_code=1,
441
+ response_reason=f"Max retries after HTTP {response_code}: {rte}",
442
+ response=resp_text_snippet,
443
+ trace_id=trace_id,
444
+ )
445
+
446
+ def submit_message(
447
+ self,
448
+ channel_name: str,
449
+ message: str,
450
+ for_nv_ingest: bool = False,
451
+ timeout: Optional[Union[float, Tuple[float, float]]] = None,
452
+ ) -> "ResponseSchema":
453
+ """
454
+ Submits a job message payload to the server's submit endpoint.
455
+
456
+ Handles retries for connection errors and non-terminal HTTP errors based on the max_retries configuration.
457
+ Specific HTTP statuses are treated as immediate failures.
458
+
459
+ Parameters
460
+ ----------
461
+ channel_name : str
462
+ Not used by RestClient; included for interface compatibility.
463
+ message : str
464
+ The JSON string representing the job specification payload.
465
+ for_nv_ingest : bool, optional
466
+ Not used by RestClient. Default is False.
467
+ timeout : float or tuple of float, optional
468
+ Specific timeout override for this request.
469
+
470
+ Returns
471
+ -------
472
+ ResponseSchema
473
+ - response_code = 0: Success (HTTP 200) with a successful job submission.
474
+ - response_code = 1: Terminal failure (e.g., 422, 400, 5xx, or max retries exceeded).
475
+
476
+ Raises
477
+ ------
478
+ TypeError
479
+ If the configured client does not support the required HTTP POST method.
480
+ """
481
+ retries: int = 0
482
+ url: str = f"{self._base_url}{self._submit_endpoint}"
483
+ headers: Dict[str, str] = {"Content-Type": "application/json"}
484
+ request_payload: Dict[str, str] = {"payload": message}
485
+ req_timeout: Tuple[float, Optional[float]] = self._timeout
486
+
487
+ # Ensure content-type is present
488
+ headers = {"Content-Type": "application/json"}
489
+ headers.update(self._headers)
490
+
491
+ while True:
492
+ result: Optional[Any] = None
493
+ trace_id: Optional[str] = None
494
+ response_code: int = -1
495
+
496
+ try:
497
+ if isinstance(self._client, requests.Session):
498
+ result = self._client.post(
499
+ url,
500
+ json=request_payload,
501
+ headers=headers,
502
+ auth=self._auth,
503
+ timeout=req_timeout,
504
+ )
505
+ response_code = result.status_code
506
+ trace_id = result.headers.get("x-trace-id")
507
+ response_text: str = result.text
508
+
509
+ if response_code in _TERMINAL_RESPONSE_STATUSES:
510
+ error_reason: str = f"Terminal response code {response_code} submitting job."
511
+ logger.error(f"{error_reason} Response: {response_text[:200]}")
512
+ return ResponseSchema(
513
+ response_code=1,
514
+ response_reason=error_reason,
515
+ response=response_text,
516
+ trace_id=trace_id,
517
+ )
518
+ elif response_code == 200:
519
+ server_job_id_raw: str = response_text
520
+ cleaned_job_id: str = server_job_id_raw.strip('"')
521
+ logger.debug(f"Submit successful. Server Job ID: {cleaned_job_id}, Trace: {trace_id}")
522
+ return ResponseSchema(
523
+ response_code=0,
524
+ response_reason="OK",
525
+ response=server_job_id_raw,
526
+ transaction_id=cleaned_job_id,
527
+ trace_id=trace_id,
528
+ )
529
+ else:
530
+ logger.warning(f"Unexpected status {response_code} on submit. Retrying if possible.")
531
+ else:
532
+ raise TypeError(
533
+ f"Unsupported client type for submit_message: {type(self._client)}. "
534
+ f"Requires a requests.Session compatible API."
535
+ )
536
+ except requests.exceptions.RequestException as err:
537
+ logger.debug(
538
+ f"RequestException submitting job: {err}. Attempting retry ({retries + 1}/{self._max_retries})..."
539
+ )
540
+ try:
541
+ retries = self.perform_retry_backoff(retries)
542
+ continue
543
+ except RuntimeError as rte:
544
+ logger.error(f"Max retries hit submitting job after RequestException: {rte}")
545
+ return ResponseSchema(response_code=1, response_reason=str(rte), response=str(err))
546
+ except Exception as e:
547
+ logger.exception(f"Unexpected error submitting job: {e}")
548
+ return ResponseSchema(response_code=1, response_reason=f"Unexpected submit error: {e}")
549
+
550
+ try:
551
+ retries = self.perform_retry_backoff(retries)
552
+ continue
553
+ except RuntimeError as rte:
554
+ logger.error(f"Max retries hit submitting job after HTTP {response_code}: {rte}")
555
+ resp_text_snippet: Optional[str] = response_text[:500] if "response_text" in locals() else None
556
+ return ResponseSchema(
557
+ response_code=1,
558
+ response_reason=f"Max retries after HTTP {response_code}: {rte}",
559
+ response=resp_text_snippet,
560
+ trace_id=trace_id,
561
+ )
562
+
563
+ def perform_retry_backoff(self, existing_retries: int) -> int:
564
+ """
565
+ Performs exponential backoff sleep if retries are permitted.
566
+
567
+ Calculates the delay using exponential backoff (2^existing_retries) capped by self._max_backoff.
568
+ Sleeps for the calculated delay if the number of existing_retries is less than max_retries.
569
+
570
+ Parameters
571
+ ----------
572
+ existing_retries : int
573
+ The number of retries already attempted for the current operation.
574
+
575
+ Returns
576
+ -------
577
+ int
578
+ The incremented retry count (existing_retries + 1).
579
+
580
+ Raises
581
+ ------
582
+ RuntimeError
583
+ If existing_retries is greater than or equal to max_retries (when max_retries > 0).
584
+ """
585
+ if self._max_retries > 0 and existing_retries >= self._max_retries:
586
+ raise RuntimeError(f"Max retry attempts ({self._max_retries}) reached")
587
+ backoff_delay: int = min(2**existing_retries, self._max_backoff)
588
+ retry_attempt_num: int = existing_retries + 1
589
+ logger.debug(
590
+ f"Operation failed. Retrying attempt "
591
+ f"{retry_attempt_num}/{self._max_retries if self._max_retries > 0 else 'infinite'} "
592
+ f"in {backoff_delay:.2f}s..."
593
+ )
594
+ time.sleep(backoff_delay)
595
+ return retry_attempt_num
@@ -0,0 +1,51 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2024, NVIDIA CORPORATION & AFFILIATES.
2
+ # All rights reserved.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import logging
6
+ import re
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ DEPLOT_MAX_TOKENS = 128
11
+ DEPLOT_TEMPERATURE = 1.0
12
+ DEPLOT_TOP_P = 1.0
13
+
14
+
15
+ def remove_url_endpoints(url) -> str:
16
+ """Some configurations provide the full endpoint in the URL.
17
+ Ex: http://deplot:8000/v1/chat/completions. For hitting the
18
+ health endpoint we need to get just the hostname:port combo
19
+ that we can append the health/ready endpoint to so we attempt
20
+ to parse that information here.
21
+
22
+ Args:
23
+ url str: Incoming URL
24
+
25
+ Returns:
26
+ str: URL with just the hostname:port portion remaining
27
+ """
28
+ if "/v1" in url:
29
+ url = url.split("/v1")[0]
30
+
31
+ return url
32
+
33
+
34
+ def generate_url(url) -> str:
35
+ """Examines the user defined URL for http*://. If that
36
+ pattern is detected the URL is used as provided by the user.
37
+ If that pattern does not exist then the assumption is made that
38
+ the endpoint is simply `http://` and that is prepended
39
+ to the user supplied endpoint.
40
+
41
+ Args:
42
+ url str: Endpoint where the Rest service is running
43
+
44
+ Returns:
45
+ str: Fully validated URL
46
+ """
47
+ if not re.match(r"^https?://", url):
48
+ # Add the default `http://` if it's not already present in the URL
49
+ url = f"http://{url}"
50
+
51
+ return url