nv-ingest-api 2025.5.11.dev20250511__py3-none-any.whl → 2025.5.13.dev20250513__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 (28) hide show
  1. nv_ingest_api/interface/transform.py +1 -1
  2. nv_ingest_api/internal/extract/docx/docx_extractor.py +3 -3
  3. nv_ingest_api/internal/extract/image/image_extractor.py +5 -5
  4. nv_ingest_api/internal/extract/pdf/engines/nemoretriever.py +1 -1
  5. nv_ingest_api/internal/extract/pptx/engines/pptx_helper.py +44 -17
  6. nv_ingest_api/internal/extract/pptx/pptx_extractor.py +1 -1
  7. nv_ingest_api/internal/primitives/nim/model_interface/text_embedding.py +35 -38
  8. nv_ingest_api/internal/primitives/nim/model_interface/yolox.py +7 -1
  9. nv_ingest_api/internal/primitives/nim/nim_client.py +17 -9
  10. nv_ingest_api/internal/primitives/tracing/tagging.py +20 -16
  11. nv_ingest_api/internal/schemas/extract/extract_pdf_schema.py +1 -1
  12. nv_ingest_api/internal/schemas/meta/ingest_job_schema.py +2 -2
  13. nv_ingest_api/internal/schemas/transform/transform_image_caption_schema.py +1 -1
  14. nv_ingest_api/internal/transform/caption_image.py +1 -1
  15. nv_ingest_api/internal/transform/embed_text.py +75 -56
  16. nv_ingest_api/util/exception_handlers/converters.py +1 -1
  17. nv_ingest_api/util/exception_handlers/decorators.py +309 -51
  18. nv_ingest_api/util/logging/configuration.py +15 -8
  19. nv_ingest_api/util/pdf/pdfium.py +1 -1
  20. nv_ingest_api/util/service_clients/redis/redis_client.py +1 -1
  21. nv_ingest_api/util/service_clients/rest/rest_client.py +1 -1
  22. nv_ingest_api/util/system/__init__.py +0 -0
  23. nv_ingest_api/util/system/hardware_info.py +426 -0
  24. {nv_ingest_api-2025.5.11.dev20250511.dist-info → nv_ingest_api-2025.5.13.dev20250513.dist-info}/METADATA +1 -1
  25. {nv_ingest_api-2025.5.11.dev20250511.dist-info → nv_ingest_api-2025.5.13.dev20250513.dist-info}/RECORD +28 -26
  26. {nv_ingest_api-2025.5.11.dev20250511.dist-info → nv_ingest_api-2025.5.13.dev20250513.dist-info}/WHEEL +0 -0
  27. {nv_ingest_api-2025.5.11.dev20250511.dist-info → nv_ingest_api-2025.5.13.dev20250513.dist-info}/licenses/LICENSE +0 -0
  28. {nv_ingest_api-2025.5.11.dev20250511.dist-info → nv_ingest_api-2025.5.13.dev20250513.dist-info}/top_level.txt +0 -0
@@ -9,6 +9,7 @@ from enum import Enum
9
9
 
10
10
 
11
11
  class LogLevel(str, Enum):
12
+ DEFAULT = "DEFAULT"
12
13
  DEBUG = "DEBUG"
13
14
  INFO = "INFO"
14
15
  WARNING = "WARNING"
@@ -16,16 +17,22 @@ class LogLevel(str, Enum):
16
17
  CRITICAL = "CRITICAL"
17
18
 
18
19
 
19
- def configure_logging(logger, level_name):
20
- """
21
- Parameters:
22
- - level_name (str): The name of the logging level (e.g., "DEBUG", "INFO").
20
+ def configure_logging(level_name: str) -> None:
23
21
  """
22
+ Configures global logging.
24
23
 
25
- numeric_level = getattr(logging, level_name, None)
24
+ Parameters
25
+ ----------
26
+ level_name : str
27
+ The name of the logging level (e.g., "DEBUG", "INFO").
28
+ """
29
+ numeric_level = getattr(logging, level_name.upper(), None)
26
30
  if not isinstance(numeric_level, int):
27
31
  raise ValueError(f"Invalid log level: {level_name}")
28
32
 
29
- logging.StreamHandler(sys.stdout)
30
- logging.basicConfig(level=numeric_level, format="%(asctime)s - %(levelname)s - %(message)s")
31
- logger.setLevel(numeric_level)
33
+ logging.basicConfig(
34
+ level=numeric_level,
35
+ format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
36
+ stream=sys.stdout,
37
+ force=True, # <- reconfigures even if basicConfig was called earlier (Python 3.8+)
38
+ )
@@ -394,7 +394,7 @@ def extract_image_like_objects_from_pdfium_page(page, merge=True, **kwargs):
394
394
  try:
395
395
  original_images, _ = pdfium_pages_to_numpy(
396
396
  [page], # A batch with a single image.
397
- render_dpi=72, # dpi = 72 is equivalent to scale = 1.
397
+ render_dpi=300, # dpi = 72 is equivalent to scale = 1.
398
398
  rotation=rotation, # Without rotation, coordinates from page.get_pos() will not match.
399
399
  )
400
400
  image_bboxes = extract_merged_images_from_pdfium_page(page, merge=merge, **kwargs)
@@ -446,7 +446,7 @@ class RedisClient(MessageBrokerClientBase):
446
446
  current_time: float = time.monotonic()
447
447
  elapsed_time: float = current_time - start_time
448
448
  if elapsed_time > timeout:
449
- logger.warning(f"Overall timeout ({timeout}s) exceeded for non-destructive fetch of '{channel_name}'.")
449
+ logger.debug(f"Overall timeout ({timeout}s) exceeded for non-destructive fetch of '{channel_name}'.")
450
450
  if expected_count:
451
451
  raise TimeoutError(
452
452
  f"Timeout collecting fragments for {channel_name}. "
@@ -470,7 +470,7 @@ class RestClient(MessageBrokerClientBase):
470
470
  f"Requires a requests.Session compatible API."
471
471
  )
472
472
  except requests.exceptions.RequestException as err:
473
- logger.warning(
473
+ logger.debug(
474
474
  f"RequestException submitting job: {err}. Attempting retry ({retries + 1}/{self._max_retries})..."
475
475
  )
476
476
  try:
File without changes
@@ -0,0 +1,426 @@
1
+ import logging
2
+ import os
3
+ import platform
4
+ from typing import Optional, Dict, Any, Tuple
5
+
6
+ # Try importing psutil, but don't make it a hard requirement if only cgroups are needed
7
+ try:
8
+ import psutil
9
+ except ImportError:
10
+ psutil = None
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # --- Cgroup Constants ---
15
+ CGROUP_V1_CPU_DIR = "/sys/fs/cgroup/cpu"
16
+ CGROUP_V1_CPUACCT_DIR = "/sys/fs/cgroup/cpuacct" # Sometimes usage is here
17
+ CGROUP_V2_CPU_FILE = "/sys/fs/cgroup/cpu.max" # Standard path in v2 unified hierarchy
18
+
19
+
20
+ class SystemResourceProbe:
21
+ """
22
+ Detects the effective CPU core count available to the current process,
23
+ optionally applying a weighting factor for hyperthreads (SMT).
24
+
25
+ It attempts to reconcile information from:
26
+ 1. Linux Cgroup v2 CPU limits (cpu.max)
27
+ 2. Linux Cgroup v1 CPU limits (cpu.cfs_quota_us, cpu.cfs_period_us)
28
+ 3. OS scheduler affinity (os.sched_getaffinity)
29
+ 4. OS reported CPU counts (psutil.cpu_count for logical/physical)
30
+
31
+ Prioritizes Cgroup quota limits. If the limit is based on core count
32
+ (affinity/OS), it applies hyperthreading weight if psutil provides
33
+ physical/logical counts.
34
+ """
35
+
36
+ def __init__(self, hyperthread_weight: float = 0.75):
37
+ """
38
+ Initializes the detector and performs the detection.
39
+
40
+ Parameters
41
+ ----------
42
+ hyperthread_weight : float, optional
43
+ The performance weighting factor for hyperthreads (0.0 to 1.0).
44
+ A value of 1.0 treats hyperthreads the same as physical cores.
45
+ A value of 0.5 suggests a hyperthread adds 50% extra performance.
46
+ Requires psutil to be installed and report physical cores.
47
+ Defaults to 0.75.
48
+ """
49
+ if not (0.0 <= hyperthread_weight <= 1.0):
50
+ raise ValueError("hyperthread_weight must be between 0.0 and 1.0")
51
+
52
+ self.hyperthread_weight: float = hyperthread_weight if psutil else 1.0 # Force 1.0 if psutil missing
53
+ if not psutil and hyperthread_weight != 1.0:
54
+ logger.warning("psutil not found. Hyperthreading weight ignored (effectively 1.0).")
55
+
56
+ # OS Info
57
+ self.os_logical_cores: Optional[int] = None
58
+ self.os_physical_cores: Optional[int] = None
59
+ self.os_sched_affinity_cores: Optional[int] = None
60
+
61
+ # Cgroup Info
62
+ self.cgroup_type: Optional[str] = None
63
+ self.cgroup_quota_cores: Optional[float] = None
64
+ self.cgroup_period_us: Optional[int] = None
65
+ self.cgroup_shares: Optional[int] = None
66
+ self.cgroup_usage_percpu_us: Optional[list[int]] = None
67
+ self.cgroup_usage_total_us: Optional[int] = None
68
+
69
+ # --- Result ---
70
+ # Raw limit before potential weighting
71
+ self.raw_limit_value: Optional[float] = None
72
+ self.raw_limit_method: str = "unknown"
73
+ # Final potentially weighted result
74
+ self.effective_cores: Optional[float] = None
75
+ self.detection_method: str = "unknown" # Method for the final effective_cores
76
+
77
+ self._detect()
78
+
79
+ @staticmethod
80
+ def _read_file_int(path: str) -> Optional[int]:
81
+ """Safely reads an integer from a file."""
82
+ try:
83
+ if os.path.exists(path):
84
+ with open(path, "r") as f:
85
+ content = f.read().strip()
86
+ if content:
87
+ return int(content)
88
+ except (IOError, ValueError, PermissionError) as e:
89
+ logger.debug(f"Failed to read or parse int from {path}: {e}")
90
+ return None
91
+
92
+ @staticmethod
93
+ def _read_file_str(path: str) -> Optional[str]:
94
+ """Safely reads a string from a file."""
95
+ try:
96
+ if os.path.exists(path):
97
+ with open(path, "r") as f:
98
+ return f.read().strip()
99
+ except (IOError, PermissionError) as e:
100
+ logger.debug(f"Failed to read string from {path}: {e}")
101
+ return None
102
+
103
+ def _read_cgroup_v1(self) -> bool:
104
+ """Attempts to read Cgroup v1 CPU limits."""
105
+ if not os.path.exists(CGROUP_V1_CPU_DIR):
106
+ logger.debug(f"Cgroup v1 CPU dir not found: {CGROUP_V1_CPU_DIR}")
107
+ return False
108
+
109
+ logger.debug(f"Checking Cgroup v1 limits in {CGROUP_V1_CPU_DIR}")
110
+ quota_us = self._read_file_int(os.path.join(CGROUP_V1_CPU_DIR, "cpu.cfs_quota_us"))
111
+ period_us = self._read_file_int(os.path.join(CGROUP_V1_CPU_DIR, "cpu.cfs_period_us"))
112
+ shares = self._read_file_int(os.path.join(CGROUP_V1_CPU_DIR, "cpu.shares"))
113
+
114
+ # Check cpuacct for usage stats if dir exists
115
+ if os.path.exists(CGROUP_V1_CPUACCT_DIR):
116
+ usage_total = self._read_file_int(os.path.join(CGROUP_V1_CPUACCT_DIR, "cpuacct.usage"))
117
+ usage_percpu_str = self._read_file_str(os.path.join(CGROUP_V1_CPUACCT_DIR, "cpuacct.usage_percpu"))
118
+ if usage_percpu_str:
119
+ try:
120
+ self.cgroup_usage_percpu_us = [int(x) for x in usage_percpu_str.split()]
121
+ except ValueError:
122
+ logger.warning("Could not parse cpuacct.usage_percpu")
123
+ if usage_total is not None:
124
+ self.cgroup_usage_total_us = usage_total
125
+
126
+ if quota_us is not None and period_us is not None:
127
+ self.cgroup_type = "v1"
128
+ self.cgroup_period_us = period_us
129
+ self.cgroup_shares = shares # May be None if file doesn't exist/readable
130
+
131
+ if quota_us > 0 and period_us > 0:
132
+ self.cgroup_quota_cores = quota_us / period_us
133
+ logger.info(
134
+ f"Cgroup v1 quota detected: {quota_us} us / {period_us} us = {self.cgroup_quota_cores:.2f}"
135
+ f" effective cores"
136
+ )
137
+ return True
138
+ elif quota_us == -1:
139
+ logger.info("Cgroup v1 quota detected: Unlimited (-1)")
140
+ # No quota limit, but we know it's cgroup v1
141
+ return True # Return true because we identified the type
142
+ else:
143
+ logger.warning(f"Cgroup v1 quota/period values invalid? Quota: {quota_us}, Period: {period_us}")
144
+
145
+ elif shares is not None: # If only shares are readable, still note it's v1
146
+ self.cgroup_type = "v1"
147
+ self.cgroup_shares = shares
148
+ logger.info(f"Cgroup v1 shares detected: {shares} (no quota found)")
149
+ return True
150
+
151
+ return False
152
+
153
+ def _read_cgroup_v2(self) -> bool:
154
+ """Attempts to read Cgroup v2 CPU limits."""
155
+ if not os.path.exists(CGROUP_V2_CPU_FILE):
156
+ logger.debug(f"Cgroup v2 cpu.max file not found: {CGROUP_V2_CPU_FILE}")
157
+ return False
158
+
159
+ logger.debug(f"Checking Cgroup v2 limits in {CGROUP_V2_CPU_FILE}")
160
+ content = self._read_file_str(CGROUP_V2_CPU_FILE)
161
+ if content:
162
+ self.cgroup_type = "v2"
163
+ parts = content.split()
164
+ if len(parts) == 2:
165
+ quota_str, period_str = parts
166
+ try:
167
+ period_us = int(period_str)
168
+ self.cgroup_period_us = period_us
169
+ if quota_str == "max":
170
+ logger.info("Cgroup v2 quota detected: Unlimited ('max')")
171
+ return True # Identified type, no quota limit
172
+ else:
173
+ quota_us = int(quota_str)
174
+ if quota_us > 0 and period_us > 0:
175
+ self.cgroup_quota_cores = quota_us / period_us
176
+ logger.info(
177
+ f"Cgroup v2 quota detected: {quota_us} us / {period_us}"
178
+ f" us = {self.cgroup_quota_cores:.2f} effective cores"
179
+ )
180
+ return True
181
+ else:
182
+ logger.warning(
183
+ f"Cgroup v2 quota/period values invalid? Quota: {quota_us}, Period: {period_us}"
184
+ )
185
+
186
+ except ValueError:
187
+ logger.warning(f"Could not parse Cgroup v2 cpu.max content: '{content}'")
188
+ else:
189
+ logger.warning(f"Unexpected format in Cgroup v2 cpu.max: '{content}'")
190
+ return False
191
+
192
+ @staticmethod
193
+ def _get_os_affinity() -> Optional[int]:
194
+ """Gets CPU count via os.sched_getaffinity."""
195
+ if platform.system() != "Linux":
196
+ logger.debug("os.sched_getaffinity is Linux-specific.")
197
+ return None
198
+ try:
199
+ # sched_getaffinity exists on Linux
200
+ affinity = os.sched_getaffinity(0) # 0 for current process
201
+ count = len(affinity)
202
+ if count > 0:
203
+ logger.info(f"Detected {count} cores via os.sched_getaffinity.")
204
+ return count
205
+ else:
206
+ logger.warning("os.sched_getaffinity(0) returned 0 or empty set.")
207
+ return None
208
+ except AttributeError:
209
+ logger.debug("os.sched_getaffinity not available on this platform/Python version.")
210
+ return None
211
+ except OSError as e:
212
+ logger.warning(f"Could not get affinity: {e}")
213
+ return None
214
+
215
+ @staticmethod
216
+ def _get_os_cpu_counts() -> Tuple[Optional[int], Optional[int]]:
217
+ """Gets logical and physical CPU counts using psutil or os.cpu_count."""
218
+ logical = None
219
+ physical = None
220
+ source = "unknown"
221
+
222
+ if psutil:
223
+ try:
224
+ logical = psutil.cpu_count(logical=True)
225
+ physical = psutil.cpu_count(logical=False)
226
+ source = "psutil"
227
+ if not logical:
228
+ logical = None # Ensure None if psutil returns 0/None
229
+ if not physical:
230
+ physical = None
231
+ except Exception as e:
232
+ logger.warning(f"psutil.cpu_count failed: {e}. Falling back to os.cpu_count.")
233
+ logical, physical = None, None # Reset before fallback
234
+
235
+ if logical is None: # Fallback if psutil failed or not installed
236
+ try:
237
+ logical = os.cpu_count()
238
+ source = "os.cpu_count"
239
+ # os.cpu_count doesn't usually provide physical count, leave as None
240
+ except NotImplementedError:
241
+ logger.error("os.cpu_count() is not implemented on this system.")
242
+ except Exception as e:
243
+ logger.error(f"os.cpu_count() failed: {e}")
244
+
245
+ if logical:
246
+ logger.info(f"Detected {logical} logical cores via {source}.")
247
+ if physical:
248
+ logger.info(f"Detected {physical} physical cores via {source}.")
249
+
250
+ return logical, physical
251
+
252
+ # --- Weighting Function ---
253
+ def _apply_hyperthread_weight(self, logical_limit: int) -> float:
254
+ """
255
+ Applies hyperthreading weight to an integer logical core limit.
256
+
257
+ Parameters
258
+ ----------
259
+ logical_limit : int
260
+ The maximum number of logical cores allowed (e.g., from affinity or OS count).
261
+
262
+ Returns
263
+ -------
264
+ float
265
+ The estimated effective core performance based on weighting.
266
+ Returns logical_limit if weighting cannot be applied.
267
+ """
268
+ P = self.os_physical_cores
269
+ # Weighting requires knowing both physical and logical counts
270
+ if P is not None and P > 0 and self.os_logical_cores is not None:
271
+ # Apply the heuristic: P physical cores + (N-P) hyperthreads * weight
272
+ # Ensure N is capped by the actual number of logical cores available
273
+ N = min(logical_limit, self.os_logical_cores)
274
+
275
+ physical_part = min(N, P)
276
+ hyperthread_part = max(0, N - P)
277
+
278
+ weighted_cores = (physical_part * 1.0) + (hyperthread_part * self.hyperthread_weight)
279
+
280
+ if weighted_cores != N: # Log only if weighting changes the value
281
+ logger.info(
282
+ f"Applying hyperthread weight ({self.hyperthread_weight:.2f}) to "
283
+ f"logical limit {logical_limit} (System: {P}P/{self.os_logical_cores}L): "
284
+ f"Effective weighted cores = {weighted_cores:.2f}"
285
+ )
286
+ else:
287
+ logger.debug(
288
+ f"Hyperthread weighting ({self.hyperthread_weight:.2f}) applied to "
289
+ f"logical limit {logical_limit} (System: {P}P/{self.os_logical_cores}L), "
290
+ f"but result is still {weighted_cores:.2f} (e.g., limit <= physical or weight=1.0)"
291
+ )
292
+ return weighted_cores
293
+ else:
294
+ # Cannot apply weighting
295
+ if self.hyperthread_weight != 1.0: # Only warn if weighting was requested
296
+ if not psutil:
297
+ # Already warned about missing psutil during init
298
+ pass
299
+ elif P is None:
300
+ logger.warning("Cannot apply hyperthread weight: Physical core count not available.")
301
+ else: # L must be missing
302
+ logger.warning("Cannot apply hyperthread weight: Logical core count not available.")
303
+
304
+ logger.debug(f"Skipping hyperthread weight calculation for logical limit {logical_limit}.")
305
+ return float(logical_limit) # Return the original limit as float
306
+
307
+ def _detect(self):
308
+ """Performs the detection sequence and applies weighting."""
309
+ logger.debug("Starting effective core count detection...")
310
+
311
+ # 1. Get OS level counts first
312
+ self.os_logical_cores, self.os_physical_cores = self._get_os_cpu_counts()
313
+
314
+ # 2. Try Cgroup v2
315
+ cgroup_detected = self._read_cgroup_v2()
316
+
317
+ # 3. Try Cgroup v1 if v2 not found or didn't yield quota
318
+ if not cgroup_detected or (self.cgroup_type == "v2" and self.cgroup_quota_cores is None):
319
+ cgroup_detected = self._read_cgroup_v1()
320
+
321
+ # 4. Get OS Affinity
322
+ self.os_sched_affinity_cores = self._get_os_affinity()
323
+
324
+ # --- 5. Determine the RAW Limit (before weighting) ---
325
+ raw_limit = float("inf")
326
+ raw_method = "unknown"
327
+
328
+ # Priority 1: Cgroup Quota
329
+ if self.cgroup_quota_cores is not None and self.cgroup_quota_cores > 0:
330
+ raw_limit = min(raw_limit, self.cgroup_quota_cores)
331
+ raw_method = f"cgroup_{self.cgroup_type}_quota"
332
+ logger.debug(f"Raw limit set by Cgroup Quota: {self.cgroup_quota_cores:.2f}")
333
+
334
+ # Priority 2: Scheduler Affinity
335
+ if self.os_sched_affinity_cores is not None and self.os_sched_affinity_cores > 0:
336
+ affinity_limit = float(self.os_sched_affinity_cores)
337
+ if affinity_limit < raw_limit:
338
+ raw_limit = affinity_limit
339
+ raw_method = "sched_affinity"
340
+ logger.debug(f"Raw limit updated by Sched Affinity: {affinity_limit}")
341
+ elif raw_method.startswith("cgroup"):
342
+ logger.debug(
343
+ f"Sched Affinity limit ({affinity_limit}) not stricter than Cgroup Quota ({raw_limit:.2f})."
344
+ )
345
+
346
+ # Priority 3: OS Logical Cores
347
+ if raw_limit == float("inf"): # If no cgroup quota or affinity was found/applied
348
+ if self.os_logical_cores is not None and self.os_logical_cores > 0:
349
+ raw_limit = float(self.os_logical_cores)
350
+ raw_method = "os_logical_count"
351
+ logger.debug(f"Raw limit set by OS Logical Core count: {self.os_logical_cores}")
352
+ else:
353
+ # Absolute fallback
354
+ logger.warning("Could not determine any CPU core limit. Defaulting raw limit to 1.0.")
355
+ raw_limit = 1.0
356
+ raw_method = "fallback_default"
357
+
358
+ self.raw_limit_value = raw_limit
359
+ self.raw_limit_method = raw_method
360
+ logger.info(f"Raw CPU limit determined: {self.raw_limit_value:.2f} (Method: {self.raw_limit_method})")
361
+
362
+ # --- 6. Apply Weighting (if applicable) ---
363
+ final_effective_cores = raw_limit
364
+ final_method = raw_method
365
+
366
+ # Apply weighting ONLY if the raw limit is NOT from a cgroup quota
367
+ # AND the limit is an integer (or effectively integer) core count
368
+ if not raw_method.startswith("cgroup_"):
369
+ # Check if raw_limit is effectively an integer
370
+ if abs(raw_limit - round(raw_limit)) < 1e-9 and raw_limit > 0:
371
+ logical_limit_int = int(round(raw_limit))
372
+ weighted_value = self._apply_hyperthread_weight(logical_limit_int)
373
+ final_effective_cores = weighted_value
374
+ # Update method if weighting was actually applied and changed the value
375
+ if abs(weighted_value - raw_limit) > 1e-9:
376
+ final_method = f"{raw_method}_weighted"
377
+ else:
378
+ # Keep original method name if weighting didn't change result
379
+ final_method = raw_method
380
+
381
+ else: # Raw limit was affinity/os count but not an integer? Should be rare.
382
+ logger.debug(
383
+ f"Raw limit method '{raw_method}' is not cgroup quota, "
384
+ f"but value {raw_limit:.2f} is not integer. Skipping weighting."
385
+ )
386
+
387
+ elif raw_method.startswith("cgroup_"):
388
+ logger.debug("Raw limit is from Cgroup quota. Using quota value directly (skipping SMT weighting).")
389
+
390
+ self.effective_cores = final_effective_cores
391
+ self.detection_method = final_method # The method for the final value
392
+
393
+ logger.info(
394
+ f"Effective CPU core limit determined: {self.effective_cores:.2f} " f"(Method: {self.detection_method})"
395
+ )
396
+
397
+ def get_effective_cores(self) -> Optional[float]:
398
+ """Returns the primary result: the effective core limit, potentially weighted."""
399
+ return self.effective_cores
400
+
401
+ def get_details(self) -> Dict[str, Any]:
402
+ """Returns a dictionary with all detected information."""
403
+ # Calculate full system weighted potential for info
404
+ os_weighted_cores = None
405
+ if self.os_physical_cores and self.os_logical_cores:
406
+ # Use weighting func with the total logical cores as the limit
407
+ os_weighted_cores = self._apply_hyperthread_weight(self.os_logical_cores)
408
+
409
+ return {
410
+ "effective_cores": self.effective_cores,
411
+ "detection_method": self.detection_method,
412
+ "raw_limit_value": self.raw_limit_value,
413
+ "raw_limit_method": self.raw_limit_method,
414
+ "hyperthread_weight_applied": self.hyperthread_weight,
415
+ "os_logical_cores": self.os_logical_cores,
416
+ "os_physical_cores": self.os_physical_cores,
417
+ "os_weighted_potential": os_weighted_cores, # Full system potential weighted
418
+ "os_sched_affinity_cores": self.os_sched_affinity_cores,
419
+ "cgroup_type": self.cgroup_type,
420
+ "cgroup_quota_cores": self.cgroup_quota_cores,
421
+ "cgroup_period_us": self.cgroup_period_us,
422
+ "cgroup_shares": self.cgroup_shares,
423
+ "cgroup_usage_total_us": self.cgroup_usage_total_us,
424
+ "cgroup_usage_percpu_us": self.cgroup_usage_percpu_us,
425
+ "platform": platform.system(),
426
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nv-ingest-api
3
- Version: 2025.5.11.dev20250511
3
+ Version: 2025.5.13.dev20250513
4
4
  Summary: Python module with core document ingestion functions.
5
5
  Author-email: Jeremy Dyer <jdyer@nvidia.com>
6
6
  License: Apache License