datalab-python-sdk 0.3.0__py3-none-any.whl → 0.4.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datalab-python-sdk
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: SDK for the Datalab document intelligence API
5
5
  Author-email: Datalab Team <hi@datalab.to>
6
6
  License-Expression: MIT
@@ -9,6 +9,7 @@ Keywords: api,datalab,document-intelligence,sdk
9
9
  Requires-Python: >=3.10
10
10
  Requires-Dist: aiohttp>=3.12.14
11
11
  Requires-Dist: click>=8.2.1
12
+ Requires-Dist: ijson>=3.3.0
12
13
  Requires-Dist: pydantic-settings>=2.10.1
13
14
  Requires-Dist: pydantic>=2.11.7
14
15
  Requires-Dist: tenacity>=8.2.3
@@ -0,0 +1,12 @@
1
+ datalab_sdk/__init__.py,sha256=kK3JxJILvUTpYe6pFLMRHI19lmQL6pSY9p7urNy6-WM,1290
2
+ datalab_sdk/cli.py,sha256=NELgNZstpBrhPoQHqsI67kNRYew_nJpq23_ENsWvmGY,38859
3
+ datalab_sdk/client.py,sha256=qZyfFsalkeR715tGmMGTILtRtFsD0ILjjbQ8JoCN7LE,73975
4
+ datalab_sdk/exceptions.py,sha256=MDfNd-l152OUVhZSfEsE95McJq-IuOsrEsEQ4CRMbEI,785
5
+ datalab_sdk/mimetypes.py,sha256=a4ULJmHxoR_RMsD0j51hx0t2Q7In_PzNm7UlWX07eEE,932
6
+ datalab_sdk/models.py,sha256=-pxDfTG-8jmp8DG-o1waJsaIk6li6iO3MR7yDwFGZ_8,17108
7
+ datalab_sdk/settings.py,sha256=BoojrRcKE7T04s-vqCPFzKqopV_qqTrAtCCsjHKk3FU,365
8
+ datalab_python_sdk-0.4.0.dist-info/METADATA,sha256=jFyvrY1HwLRDl2_nRpUH9aV3k7FoUvtU9Dbacgp-l9w,2238
9
+ datalab_python_sdk-0.4.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
10
+ datalab_python_sdk-0.4.0.dist-info/entry_points.txt,sha256=rqWxO21UV57JxxyeF20iBTxHpzBYOA69hCHIqwdn-Ok,48
11
+ datalab_python_sdk-0.4.0.dist-info/licenses/LICENSE,sha256=XGjT-Uz6t7UZzr3JCZfilsGcZEgMm_1aONcRunohxWg,1064
12
+ datalab_python_sdk-0.4.0.dist-info/RECORD,,
datalab_sdk/__init__.py CHANGED
@@ -10,6 +10,7 @@ from .exceptions import DatalabError, DatalabAPIError, DatalabTimeoutError
10
10
  from .models import (
11
11
  ConversionResult,
12
12
  CreateDocumentResult,
13
+ FileResult,
13
14
  OCRResult,
14
15
  ConvertOptions,
15
16
  ExtractOptions,
@@ -36,6 +37,7 @@ __all__ = [
36
37
  "DatalabTimeoutError",
37
38
  "ConversionResult",
38
39
  "CreateDocumentResult",
40
+ "FileResult",
39
41
  "OCRResult",
40
42
  "ConvertOptions",
41
43
  "ExtractOptions",
datalab_sdk/client.py CHANGED
@@ -4,8 +4,13 @@ Datalab API client - async core with sync wrapper
4
4
 
5
5
  import asyncio
6
6
  import mimetypes
7
+ import os
8
+ import shutil
9
+ import tempfile
7
10
  import warnings
11
+
8
12
  import aiohttp
13
+ import ijson
9
14
  from tenacity import (
10
15
  retry,
11
16
  retry_if_exception,
@@ -25,6 +30,7 @@ from datalab_sdk.mimetypes import MIMETYPE_MAP
25
30
  from datalab_sdk.models import (
26
31
  ConversionResult,
27
32
  CreateDocumentResult,
33
+ FileResult,
28
34
  OCRResult,
29
35
  ProcessingOptions,
30
36
  ConvertOptions,
@@ -44,6 +50,21 @@ from datalab_sdk.models import (
44
50
  from datalab_sdk.settings import settings
45
51
 
46
52
 
53
+ class _AsyncIterableReader:
54
+ """Adapts an async iterable of bytes into an async file-like object for ijson."""
55
+
56
+ def __init__(self, async_iterable):
57
+ self._iter = async_iterable.__aiter__()
58
+
59
+ async def read(self, n=-1):
60
+ if n == 0:
61
+ return b""
62
+ try:
63
+ return await self._iter.__anext__()
64
+ except StopAsyncIteration:
65
+ return b""
66
+
67
+
47
68
  class AsyncDatalabClient:
48
69
  """Asynchronous client for Datalab API"""
49
70
 
@@ -150,8 +171,12 @@ class AsyncDatalabClient:
150
171
  return await self._make_request("POST", endpoint, **kwargs)
151
172
 
152
173
  async def _poll_result(
153
- self, check_url: str, max_polls: int = 300, poll_interval: int = 1
154
- ) -> Dict[str, Any]:
174
+ self,
175
+ check_url: str,
176
+ max_polls: int = 300,
177
+ poll_interval: int = 1,
178
+ stream_response_to: Optional[Path] = None,
179
+ ) -> Union[Dict[str, Any], FileResult]:
155
180
  """Poll for result completion"""
156
181
  full_url = (
157
182
  check_url
@@ -160,14 +185,21 @@ class AsyncDatalabClient:
160
185
  )
161
186
 
162
187
  for i in range(max_polls):
163
- data = await self._poll_get_with_retry(full_url)
188
+ if stream_response_to:
189
+ result = await self._poll_get_streaming(full_url, stream_response_to)
190
+ status, success, error = result.status, result.success, result.error
191
+ else:
192
+ data = await self._poll_get_with_retry(full_url)
193
+ status = data.get("status")
194
+ success = data.get("success", True)
195
+ error = data.get("error")
164
196
 
165
- if data.get("status") == "complete":
166
- return data
197
+ if status == "complete":
198
+ return result if stream_response_to else data
167
199
 
168
- if not data.get("success", True) and not data.get("status") == "processing":
200
+ if not success and status != "processing":
169
201
  raise DatalabAPIError(
170
- f"Processing failed: {data.get('error', 'Unknown error')}"
202
+ f"Processing failed: {error or 'Unknown error'}"
171
203
  )
172
204
 
173
205
  await asyncio.sleep(poll_interval)
@@ -202,6 +234,116 @@ class AsyncDatalabClient:
202
234
  """GET wrapper for polling with scoped retries for transient failures"""
203
235
  return await self._make_request("GET", url)
204
236
 
237
+ @staticmethod
238
+ async def _tee_stream_to_file(content, fd):
239
+ """Async generator that writes each chunk to fd and yields it for ijson."""
240
+ async for chunk in content.iter_any():
241
+ os.write(fd, chunk)
242
+ yield chunk
243
+
244
+ @retry(
245
+ retry=(
246
+ retry_if_exception_type(DatalabTimeoutError)
247
+ | retry_if_exception(
248
+ lambda e: isinstance(e, DatalabAPIError)
249
+ and (
250
+ getattr(e, "status_code", None) in (408, 429)
251
+ or (
252
+ getattr(e, "status_code", None) is not None
253
+ and getattr(e, "status_code") >= 500
254
+ )
255
+ or getattr(e, "status_code", None) is None
256
+ )
257
+ )
258
+ ),
259
+ stop=stop_after_attempt(10),
260
+ wait=wait_exponential_jitter(initial=5, max=120),
261
+ reraise=True,
262
+ )
263
+ async def _poll_get_streaming(self, url: str, stream_response_to: Path) -> FileResult:
264
+ """GET with streaming to disk. Extracts status/success/error via ijson."""
265
+ await self._ensure_session()
266
+
267
+ fd = None
268
+ tmp_path = None
269
+ try:
270
+ fd, tmp_path = tempfile.mkstemp()
271
+
272
+ try:
273
+ resp = await self._session.get(url)
274
+ except asyncio.TimeoutError:
275
+ raise DatalabTimeoutError(f"Request timed out after {self.timeout} seconds")
276
+ except aiohttp.ClientError as e:
277
+ raise DatalabAPIError(f"Request failed: {str(e)}")
278
+
279
+ try:
280
+ if resp.status >= 400:
281
+ body = await resp.read()
282
+ try:
283
+ import json as _json
284
+ error_data = _json.loads(body)
285
+ error_message = (
286
+ error_data.get("detail") or error_data.get("error") or str(resp.status)
287
+ )
288
+ except Exception:
289
+ error_message = f"HTTP {resp.status}"
290
+ raise DatalabAPIError(error_message, resp.status)
291
+
292
+ status = None
293
+ success = None
294
+ error = None
295
+
296
+ tee_reader = _AsyncIterableReader(
297
+ self._tee_stream_to_file(resp.content, fd)
298
+ )
299
+ async for prefix, event, value in ijson.parse_async(tee_reader):
300
+ if prefix == "status" and event == "string":
301
+ status = value
302
+ elif prefix == "success" and event == "boolean":
303
+ success = value
304
+ elif prefix == "error" and event in ("string", "null"):
305
+ error = value
306
+ finally:
307
+ resp.release()
308
+
309
+ if status is None:
310
+ raise DatalabAPIError("Response missing 'status' field")
311
+
312
+ if success is None:
313
+ success = True
314
+
315
+ # Close fd before moving
316
+ os.close(fd)
317
+ fd = None
318
+
319
+ if status == "complete":
320
+ shutil.move(tmp_path, stream_response_to)
321
+ tmp_path = None
322
+ return FileResult(
323
+ success=success,
324
+ status=status,
325
+ output_path=stream_response_to,
326
+ error=error,
327
+ )
328
+ else:
329
+ return FileResult(
330
+ success=success,
331
+ status=status,
332
+ output_path=stream_response_to,
333
+ error=error,
334
+ )
335
+ finally:
336
+ if fd is not None:
337
+ try:
338
+ os.close(fd)
339
+ except OSError:
340
+ pass
341
+ if tmp_path is not None:
342
+ try:
343
+ os.unlink(tmp_path)
344
+ except OSError:
345
+ pass
346
+
205
347
  def _prepare_file_data(self, file_path: Union[str, Path]) -> tuple:
206
348
  """Prepare file data for upload"""
207
349
  file_path = Path(file_path)
@@ -284,7 +426,8 @@ class AsyncDatalabClient:
284
426
  data: aiohttp.FormData,
285
427
  max_polls: int = 300,
286
428
  poll_interval: int = 1,
287
- ) -> Dict[str, Any]:
429
+ stream_response_to: Optional[Path] = None,
430
+ ) -> Union[Dict[str, Any], FileResult]:
288
431
  """Submit a request and poll for the result"""
289
432
  initial_data = await self._submit_with_retry(endpoint, data=data)
290
433
 
@@ -297,6 +440,7 @@ class AsyncDatalabClient:
297
440
  initial_data["request_check_url"],
298
441
  max_polls=max_polls,
299
442
  poll_interval=poll_interval,
443
+ stream_response_to=stream_response_to,
300
444
  )
301
445
 
302
446
  # Convenient endpoint-specific methods
@@ -306,9 +450,10 @@ class AsyncDatalabClient:
306
450
  file_url: Optional[str] = None,
307
451
  options: Optional[ConvertOptions] = None,
308
452
  save_output: Optional[Union[str, Path]] = None,
453
+ stream_response_to: Optional[Union[str, Path]] = None,
309
454
  max_polls: int = 300,
310
455
  poll_interval: int = 1,
311
- ) -> ConversionResult:
456
+ ) -> Union[ConversionResult, FileResult]:
312
457
  """
313
458
  Convert a document to markdown, HTML, JSON, or chunks
314
459
 
@@ -317,9 +462,19 @@ class AsyncDatalabClient:
317
462
  file_url: URL of the file to convert
318
463
  options: Processing options for conversion
319
464
  save_output: Optional path to save output files
465
+ stream_response_to: Optional path to stream raw JSON response to disk
320
466
  max_polls: Maximum number of polling attempts
321
467
  poll_interval: Seconds between polling attempts
322
468
  """
469
+ if save_output and stream_response_to:
470
+ raise ValueError("Cannot use both 'save_output' and 'stream_response_to'.")
471
+
472
+ resolved_stream_response_to = None
473
+ if stream_response_to:
474
+ resolved_stream_response_to = Path(stream_response_to)
475
+ if not resolved_stream_response_to.parent.is_dir():
476
+ raise ValueError(f"Directory does not exist: {resolved_stream_response_to.parent}")
477
+
323
478
  if options is None:
324
479
  options = ConvertOptions()
325
480
 
@@ -330,8 +485,12 @@ class AsyncDatalabClient:
330
485
  ),
331
486
  max_polls=max_polls,
332
487
  poll_interval=poll_interval,
488
+ stream_response_to=resolved_stream_response_to,
333
489
  )
334
490
 
491
+ if isinstance(result_data, FileResult):
492
+ return result_data
493
+
335
494
  result = self._build_conversion_result(result_data, options.output_format)
336
495
 
337
496
  # Save output if requested
@@ -348,9 +507,10 @@ class AsyncDatalabClient:
348
507
  file_url: Optional[str] = None,
349
508
  options: Optional[ExtractOptions] = None,
350
509
  save_output: Optional[Union[str, Path]] = None,
510
+ stream_response_to: Optional[Union[str, Path]] = None,
351
511
  max_polls: int = 300,
352
512
  poll_interval: int = 1,
353
- ) -> ConversionResult:
513
+ ) -> Union[ConversionResult, FileResult]:
354
514
  """
355
515
  Extract structured data from a document using a JSON schema
356
516
 
@@ -362,9 +522,19 @@ class AsyncDatalabClient:
362
522
  file_url: URL of the file to extract from
363
523
  options: Extraction options (must include page_schema)
364
524
  save_output: Optional path to save output files
525
+ stream_response_to: Optional path to stream raw JSON response to disk
365
526
  max_polls: Maximum number of polling attempts
366
527
  poll_interval: Seconds between polling attempts
367
528
  """
529
+ if save_output and stream_response_to:
530
+ raise ValueError("Cannot use both 'save_output' and 'stream_response_to'.")
531
+
532
+ resolved_stream_response_to = None
533
+ if stream_response_to:
534
+ resolved_stream_response_to = Path(stream_response_to)
535
+ if not resolved_stream_response_to.parent.is_dir():
536
+ raise ValueError(f"Directory does not exist: {resolved_stream_response_to.parent}")
537
+
368
538
  if options is None:
369
539
  raise ValueError("options must be provided with page_schema")
370
540
 
@@ -383,8 +553,12 @@ class AsyncDatalabClient:
383
553
  ),
384
554
  max_polls=max_polls,
385
555
  poll_interval=poll_interval,
556
+ stream_response_to=resolved_stream_response_to,
386
557
  )
387
558
 
559
+ if isinstance(result_data, FileResult):
560
+ return result_data
561
+
388
562
  result = self._build_conversion_result(result_data, options.output_format)
389
563
 
390
564
  if save_output and result.success:
@@ -400,9 +574,10 @@ class AsyncDatalabClient:
400
574
  file_url: Optional[str] = None,
401
575
  options: Optional[SegmentOptions] = None,
402
576
  save_output: Optional[Union[str, Path]] = None,
577
+ stream_response_to: Optional[Union[str, Path]] = None,
403
578
  max_polls: int = 300,
404
579
  poll_interval: int = 1,
405
- ) -> ConversionResult:
580
+ ) -> Union[ConversionResult, FileResult]:
406
581
  """
407
582
  Segment a document into sections using a schema
408
583
 
@@ -414,9 +589,19 @@ class AsyncDatalabClient:
414
589
  file_url: URL of the file to segment
415
590
  options: Segmentation options (must include segmentation_schema)
416
591
  save_output: Optional path to save output files
592
+ stream_response_to: Optional path to stream raw JSON response to disk
417
593
  max_polls: Maximum number of polling attempts
418
594
  poll_interval: Seconds between polling attempts
419
595
  """
596
+ if save_output and stream_response_to:
597
+ raise ValueError("Cannot use both 'save_output' and 'stream_response_to'.")
598
+
599
+ resolved_stream_response_to = None
600
+ if stream_response_to:
601
+ resolved_stream_response_to = Path(stream_response_to)
602
+ if not resolved_stream_response_to.parent.is_dir():
603
+ raise ValueError(f"Directory does not exist: {resolved_stream_response_to.parent}")
604
+
420
605
  if options is None:
421
606
  raise ValueError("options must be provided with segmentation_schema")
422
607
 
@@ -435,8 +620,12 @@ class AsyncDatalabClient:
435
620
  ),
436
621
  max_polls=max_polls,
437
622
  poll_interval=poll_interval,
623
+ stream_response_to=resolved_stream_response_to,
438
624
  )
439
625
 
626
+ if isinstance(result_data, FileResult):
627
+ return result_data
628
+
440
629
  result = self._build_conversion_result(result_data, "markdown")
441
630
 
442
631
  if save_output and result.success:
@@ -452,9 +641,10 @@ class AsyncDatalabClient:
452
641
  file_url: Optional[str] = None,
453
642
  options: Optional[CustomPipelineOptions] = None,
454
643
  save_output: Optional[Union[str, Path]] = None,
644
+ stream_response_to: Optional[Union[str, Path]] = None,
455
645
  max_polls: int = 300,
456
646
  poll_interval: int = 1,
457
- ) -> ConversionResult:
647
+ ) -> Union[ConversionResult, FileResult]:
458
648
  """
459
649
  Execute a custom pipeline configuration
460
650
 
@@ -463,9 +653,19 @@ class AsyncDatalabClient:
463
653
  file_url: URL of the file to process
464
654
  options: Custom pipeline options (must include pipeline_id)
465
655
  save_output: Optional path to save output files
656
+ stream_response_to: Optional path to stream raw JSON response to disk
466
657
  max_polls: Maximum number of polling attempts
467
658
  poll_interval: Seconds between polling attempts
468
659
  """
660
+ if save_output and stream_response_to:
661
+ raise ValueError("Cannot use both 'save_output' and 'stream_response_to'.")
662
+
663
+ resolved_stream_response_to = None
664
+ if stream_response_to:
665
+ resolved_stream_response_to = Path(stream_response_to)
666
+ if not resolved_stream_response_to.parent.is_dir():
667
+ raise ValueError(f"Directory does not exist: {resolved_stream_response_to.parent}")
668
+
469
669
  if options is None:
470
670
  raise ValueError("options must be provided with pipeline_id")
471
671
 
@@ -476,8 +676,12 @@ class AsyncDatalabClient:
476
676
  ),
477
677
  max_polls=max_polls,
478
678
  poll_interval=poll_interval,
679
+ stream_response_to=resolved_stream_response_to,
479
680
  )
480
681
 
682
+ if isinstance(result_data, FileResult):
683
+ return result_data
684
+
481
685
  result = self._build_conversion_result(result_data, options.output_format)
482
686
 
483
687
  if save_output and result.success:
@@ -493,9 +697,10 @@ class AsyncDatalabClient:
493
697
  file_url: Optional[str] = None,
494
698
  options: Optional[TrackChangesOptions] = None,
495
699
  save_output: Optional[Union[str, Path]] = None,
700
+ stream_response_to: Optional[Union[str, Path]] = None,
496
701
  max_polls: int = 300,
497
702
  poll_interval: int = 1,
498
- ) -> ConversionResult:
703
+ ) -> Union[ConversionResult, FileResult]:
499
704
  """
500
705
  Extract and display tracked changes from DOCX documents
501
706
 
@@ -504,9 +709,19 @@ class AsyncDatalabClient:
504
709
  file_url: URL of the DOCX file
505
710
  options: Track changes options
506
711
  save_output: Optional path to save output files
712
+ stream_response_to: Optional path to stream raw JSON response to disk
507
713
  max_polls: Maximum number of polling attempts
508
714
  poll_interval: Seconds between polling attempts
509
715
  """
716
+ if save_output and stream_response_to:
717
+ raise ValueError("Cannot use both 'save_output' and 'stream_response_to'.")
718
+
719
+ resolved_stream_response_to = None
720
+ if stream_response_to:
721
+ resolved_stream_response_to = Path(stream_response_to)
722
+ if not resolved_stream_response_to.parent.is_dir():
723
+ raise ValueError(f"Directory does not exist: {resolved_stream_response_to.parent}")
724
+
510
725
  if options is None:
511
726
  options = TrackChangesOptions()
512
727
 
@@ -517,8 +732,12 @@ class AsyncDatalabClient:
517
732
  ),
518
733
  max_polls=max_polls,
519
734
  poll_interval=poll_interval,
735
+ stream_response_to=resolved_stream_response_to,
520
736
  )
521
737
 
738
+ if isinstance(result_data, FileResult):
739
+ return result_data
740
+
522
741
  result = self._build_conversion_result(result_data, options.output_format)
523
742
 
524
743
  if save_output and result.success:
@@ -534,9 +753,10 @@ class AsyncDatalabClient:
534
753
  output_format: str = "docx",
535
754
  webhook_url: Optional[str] = None,
536
755
  save_output: Optional[Union[str, Path]] = None,
756
+ stream_response_to: Optional[Union[str, Path]] = None,
537
757
  max_polls: int = 300,
538
758
  poll_interval: int = 1,
539
- ) -> CreateDocumentResult:
759
+ ) -> Union[CreateDocumentResult, FileResult]:
540
760
  """
541
761
  Create a DOCX document from markdown with track changes support
542
762
 
@@ -550,9 +770,19 @@ class AsyncDatalabClient:
550
770
  output_format: Output format (currently only 'docx')
551
771
  webhook_url: Optional webhook URL for completion notification
552
772
  save_output: Optional path to save the output file
773
+ stream_response_to: Optional path to stream raw JSON response to disk
553
774
  max_polls: Maximum number of polling attempts
554
775
  poll_interval: Seconds between polling attempts
555
776
  """
777
+ if save_output and stream_response_to:
778
+ raise ValueError("Cannot use both 'save_output' and 'stream_response_to'.")
779
+
780
+ resolved_stream_response_to = None
781
+ if stream_response_to:
782
+ resolved_stream_response_to = Path(stream_response_to)
783
+ if not resolved_stream_response_to.parent.is_dir():
784
+ raise ValueError(f"Directory does not exist: {resolved_stream_response_to.parent}")
785
+
556
786
  payload = {
557
787
  "markdown": markdown,
558
788
  "output_format": output_format,
@@ -574,8 +804,12 @@ class AsyncDatalabClient:
574
804
  initial_data["request_check_url"],
575
805
  max_polls=max_polls,
576
806
  poll_interval=poll_interval,
807
+ stream_response_to=resolved_stream_response_to,
577
808
  )
578
809
 
810
+ if isinstance(result_data, FileResult):
811
+ return result_data
812
+
579
813
  result = CreateDocumentResult(
580
814
  status=result_data.get("status", "complete"),
581
815
  success=result_data.get("success"),
@@ -600,14 +834,24 @@ class AsyncDatalabClient:
600
834
  file_path: Union[str, Path],
601
835
  options: Optional[ProcessingOptions] = None,
602
836
  save_output: Optional[Union[str, Path]] = None,
837
+ stream_response_to: Optional[Union[str, Path]] = None,
603
838
  max_polls: int = 300,
604
839
  poll_interval: int = 1,
605
- ) -> OCRResult:
840
+ ) -> Union[OCRResult, FileResult]:
606
841
  """Perform OCR on a document
607
842
 
608
843
  .. deprecated::
609
844
  The /ocr endpoint is deprecated. Use convert() instead.
610
845
  """
846
+ if save_output and stream_response_to:
847
+ raise ValueError("Cannot use both 'save_output' and 'stream_response_to'.")
848
+
849
+ resolved_stream_response_to = None
850
+ if stream_response_to:
851
+ resolved_stream_response_to = Path(stream_response_to)
852
+ if not resolved_stream_response_to.parent.is_dir():
853
+ raise ValueError(f"Directory does not exist: {resolved_stream_response_to.parent}")
854
+
611
855
  warnings.warn(
612
856
  "The ocr() method is deprecated and will be removed in a future version. "
613
857
  "Use convert() instead.",
@@ -631,8 +875,12 @@ class AsyncDatalabClient:
631
875
  initial_data["request_check_url"],
632
876
  max_polls=max_polls,
633
877
  poll_interval=poll_interval,
878
+ stream_response_to=resolved_stream_response_to,
634
879
  )
635
880
 
881
+ if isinstance(result_data, FileResult):
882
+ return result_data
883
+
636
884
  result = OCRResult(
637
885
  success=result_data.get("success", False),
638
886
  pages=result_data.get("pages", []),
@@ -657,9 +905,10 @@ class AsyncDatalabClient:
657
905
  file_url: Optional[str] = None,
658
906
  options: Optional[FormFillingOptions] = None,
659
907
  save_output: Optional[Union[str, Path]] = None,
908
+ stream_response_to: Optional[Union[str, Path]] = None,
660
909
  max_polls: int = 300,
661
910
  poll_interval: int = 1,
662
- ) -> FormFillingResult:
911
+ ) -> Union[FormFillingResult, FileResult]:
663
912
  """
664
913
  Fill PDF or image forms with provided field data
665
914
 
@@ -668,9 +917,19 @@ class AsyncDatalabClient:
668
917
  file_url: URL of the file to fill
669
918
  options: Form filling options (must include field_data)
670
919
  save_output: Optional path to save output files
920
+ stream_response_to: Optional path to stream raw JSON response to disk
671
921
  max_polls: Maximum number of polling attempts
672
922
  poll_interval: Seconds between polling attempts
673
923
  """
924
+ if save_output and stream_response_to:
925
+ raise ValueError("Cannot use both 'save_output' and 'stream_response_to'.")
926
+
927
+ resolved_stream_response_to = None
928
+ if stream_response_to:
929
+ resolved_stream_response_to = Path(stream_response_to)
930
+ if not resolved_stream_response_to.parent.is_dir():
931
+ raise ValueError(f"Directory does not exist: {resolved_stream_response_to.parent}")
932
+
674
933
  if options is None:
675
934
  raise ValueError("options must be provided with field_data")
676
935
 
@@ -690,8 +949,12 @@ class AsyncDatalabClient:
690
949
  initial_data["request_check_url"],
691
950
  max_polls=max_polls,
692
951
  poll_interval=poll_interval,
952
+ stream_response_to=resolved_stream_response_to,
693
953
  )
694
954
 
955
+ if isinstance(result_data, FileResult):
956
+ return result_data
957
+
695
958
  result = FormFillingResult(
696
959
  status=result_data.get("status", "complete"),
697
960
  success=result_data.get("success"),
@@ -1344,9 +1607,10 @@ class DatalabClient:
1344
1607
  file_url: Optional[str] = None,
1345
1608
  options: Optional[ConvertOptions] = None,
1346
1609
  save_output: Optional[Union[str, Path]] = None,
1610
+ stream_response_to: Optional[Union[str, Path]] = None,
1347
1611
  max_polls: int = 300,
1348
1612
  poll_interval: int = 1,
1349
- ) -> ConversionResult:
1613
+ ) -> Union[ConversionResult, FileResult]:
1350
1614
  """
1351
1615
  Convert a document to markdown, HTML, JSON, or chunks (sync version)
1352
1616
 
@@ -1355,6 +1619,7 @@ class DatalabClient:
1355
1619
  file_url: URL of the file to convert
1356
1620
  options: Processing options for conversion
1357
1621
  save_output: Optional path to save output files
1622
+ stream_response_to: Optional path to stream raw JSON response to disk
1358
1623
  max_polls: Maximum number of polling attempts
1359
1624
  poll_interval: Seconds between polling attempts
1360
1625
  """
@@ -1364,6 +1629,7 @@ class DatalabClient:
1364
1629
  file_url=file_url,
1365
1630
  options=options,
1366
1631
  save_output=save_output,
1632
+ stream_response_to=stream_response_to,
1367
1633
  max_polls=max_polls,
1368
1634
  poll_interval=poll_interval,
1369
1635
  )
@@ -1375,9 +1641,10 @@ class DatalabClient:
1375
1641
  file_url: Optional[str] = None,
1376
1642
  options: Optional[ExtractOptions] = None,
1377
1643
  save_output: Optional[Union[str, Path]] = None,
1644
+ stream_response_to: Optional[Union[str, Path]] = None,
1378
1645
  max_polls: int = 300,
1379
1646
  poll_interval: int = 1,
1380
- ) -> ConversionResult:
1647
+ ) -> Union[ConversionResult, FileResult]:
1381
1648
  """
1382
1649
  Extract structured data from a document using a JSON schema (sync version)
1383
1650
 
@@ -1386,6 +1653,7 @@ class DatalabClient:
1386
1653
  file_url: URL of the file to extract from
1387
1654
  options: Extraction options (must include page_schema)
1388
1655
  save_output: Optional path to save output files
1656
+ stream_response_to: Optional path to stream raw JSON response to disk
1389
1657
  max_polls: Maximum number of polling attempts
1390
1658
  poll_interval: Seconds between polling attempts
1391
1659
  """
@@ -1395,6 +1663,7 @@ class DatalabClient:
1395
1663
  file_url=file_url,
1396
1664
  options=options,
1397
1665
  save_output=save_output,
1666
+ stream_response_to=stream_response_to,
1398
1667
  max_polls=max_polls,
1399
1668
  poll_interval=poll_interval,
1400
1669
  )
@@ -1406,9 +1675,10 @@ class DatalabClient:
1406
1675
  file_url: Optional[str] = None,
1407
1676
  options: Optional[SegmentOptions] = None,
1408
1677
  save_output: Optional[Union[str, Path]] = None,
1678
+ stream_response_to: Optional[Union[str, Path]] = None,
1409
1679
  max_polls: int = 300,
1410
1680
  poll_interval: int = 1,
1411
- ) -> ConversionResult:
1681
+ ) -> Union[ConversionResult, FileResult]:
1412
1682
  """
1413
1683
  Segment a document into sections using a schema (sync version)
1414
1684
 
@@ -1417,6 +1687,7 @@ class DatalabClient:
1417
1687
  file_url: URL of the file to segment
1418
1688
  options: Segmentation options (must include segmentation_schema)
1419
1689
  save_output: Optional path to save output files
1690
+ stream_response_to: Optional path to stream raw JSON response to disk
1420
1691
  max_polls: Maximum number of polling attempts
1421
1692
  poll_interval: Seconds between polling attempts
1422
1693
  """
@@ -1426,6 +1697,7 @@ class DatalabClient:
1426
1697
  file_url=file_url,
1427
1698
  options=options,
1428
1699
  save_output=save_output,
1700
+ stream_response_to=stream_response_to,
1429
1701
  max_polls=max_polls,
1430
1702
  poll_interval=poll_interval,
1431
1703
  )
@@ -1437,9 +1709,10 @@ class DatalabClient:
1437
1709
  file_url: Optional[str] = None,
1438
1710
  options: Optional[CustomPipelineOptions] = None,
1439
1711
  save_output: Optional[Union[str, Path]] = None,
1712
+ stream_response_to: Optional[Union[str, Path]] = None,
1440
1713
  max_polls: int = 300,
1441
1714
  poll_interval: int = 1,
1442
- ) -> ConversionResult:
1715
+ ) -> Union[ConversionResult, FileResult]:
1443
1716
  """
1444
1717
  Execute a custom pipeline configuration (sync version)
1445
1718
 
@@ -1448,6 +1721,7 @@ class DatalabClient:
1448
1721
  file_url: URL of the file to process
1449
1722
  options: Custom pipeline options (must include pipeline_id)
1450
1723
  save_output: Optional path to save output files
1724
+ stream_response_to: Optional path to stream raw JSON response to disk
1451
1725
  max_polls: Maximum number of polling attempts
1452
1726
  poll_interval: Seconds between polling attempts
1453
1727
  """
@@ -1457,6 +1731,7 @@ class DatalabClient:
1457
1731
  file_url=file_url,
1458
1732
  options=options,
1459
1733
  save_output=save_output,
1734
+ stream_response_to=stream_response_to,
1460
1735
  max_polls=max_polls,
1461
1736
  poll_interval=poll_interval,
1462
1737
  )
@@ -1468,9 +1743,10 @@ class DatalabClient:
1468
1743
  file_url: Optional[str] = None,
1469
1744
  options: Optional[TrackChangesOptions] = None,
1470
1745
  save_output: Optional[Union[str, Path]] = None,
1746
+ stream_response_to: Optional[Union[str, Path]] = None,
1471
1747
  max_polls: int = 300,
1472
1748
  poll_interval: int = 1,
1473
- ) -> ConversionResult:
1749
+ ) -> Union[ConversionResult, FileResult]:
1474
1750
  """
1475
1751
  Extract and display tracked changes from DOCX documents (sync version)
1476
1752
 
@@ -1479,6 +1755,7 @@ class DatalabClient:
1479
1755
  file_url: URL of the DOCX file
1480
1756
  options: Track changes options
1481
1757
  save_output: Optional path to save output files
1758
+ stream_response_to: Optional path to stream raw JSON response to disk
1482
1759
  max_polls: Maximum number of polling attempts
1483
1760
  poll_interval: Seconds between polling attempts
1484
1761
  """
@@ -1488,6 +1765,7 @@ class DatalabClient:
1488
1765
  file_url=file_url,
1489
1766
  options=options,
1490
1767
  save_output=save_output,
1768
+ stream_response_to=stream_response_to,
1491
1769
  max_polls=max_polls,
1492
1770
  poll_interval=poll_interval,
1493
1771
  )
@@ -1499,9 +1777,10 @@ class DatalabClient:
1499
1777
  output_format: str = "docx",
1500
1778
  webhook_url: Optional[str] = None,
1501
1779
  save_output: Optional[Union[str, Path]] = None,
1780
+ stream_response_to: Optional[Union[str, Path]] = None,
1502
1781
  max_polls: int = 300,
1503
1782
  poll_interval: int = 1,
1504
- ) -> CreateDocumentResult:
1783
+ ) -> Union[CreateDocumentResult, FileResult]:
1505
1784
  """
1506
1785
  Create a DOCX document from markdown (sync version)
1507
1786
 
@@ -1510,6 +1789,7 @@ class DatalabClient:
1510
1789
  output_format: Output format (currently only 'docx')
1511
1790
  webhook_url: Optional webhook URL for completion notification
1512
1791
  save_output: Optional path to save the output file
1792
+ stream_response_to: Optional path to stream raw JSON response to disk
1513
1793
  max_polls: Maximum number of polling attempts
1514
1794
  poll_interval: Seconds between polling attempts
1515
1795
  """
@@ -1519,6 +1799,7 @@ class DatalabClient:
1519
1799
  output_format=output_format,
1520
1800
  webhook_url=webhook_url,
1521
1801
  save_output=save_output,
1802
+ stream_response_to=stream_response_to,
1522
1803
  max_polls=max_polls,
1523
1804
  poll_interval=poll_interval,
1524
1805
  )
@@ -1529,9 +1810,10 @@ class DatalabClient:
1529
1810
  file_path: Union[str, Path],
1530
1811
  options: Optional[ProcessingOptions] = None,
1531
1812
  save_output: Optional[Union[str, Path]] = None,
1813
+ stream_response_to: Optional[Union[str, Path]] = None,
1532
1814
  max_polls: int = 300,
1533
1815
  poll_interval: int = 1,
1534
- ) -> OCRResult:
1816
+ ) -> Union[OCRResult, FileResult]:
1535
1817
  """Perform OCR on a document (sync version)
1536
1818
 
1537
1819
  .. deprecated::
@@ -1542,6 +1824,7 @@ class DatalabClient:
1542
1824
  file_path=file_path,
1543
1825
  options=options,
1544
1826
  save_output=save_output,
1827
+ stream_response_to=stream_response_to,
1545
1828
  max_polls=max_polls,
1546
1829
  poll_interval=poll_interval,
1547
1830
  )
@@ -1553,9 +1836,10 @@ class DatalabClient:
1553
1836
  file_url: Optional[str] = None,
1554
1837
  options: Optional[FormFillingOptions] = None,
1555
1838
  save_output: Optional[Union[str, Path]] = None,
1839
+ stream_response_to: Optional[Union[str, Path]] = None,
1556
1840
  max_polls: int = 300,
1557
1841
  poll_interval: int = 1,
1558
- ) -> FormFillingResult:
1842
+ ) -> Union[FormFillingResult, FileResult]:
1559
1843
  """
1560
1844
  Fill PDF or image forms with provided field data (sync version)
1561
1845
 
@@ -1564,6 +1848,7 @@ class DatalabClient:
1564
1848
  file_url: URL of the file to fill
1565
1849
  options: Form filling options (must include field_data)
1566
1850
  save_output: Optional path to save output files
1851
+ stream_response_to: Optional path to stream raw JSON response to disk
1567
1852
  max_polls: Maximum number of polling attempts
1568
1853
  poll_interval: Seconds between polling attempts
1569
1854
  """
@@ -1573,6 +1858,7 @@ class DatalabClient:
1573
1858
  file_url=file_url,
1574
1859
  options=options,
1575
1860
  save_output=save_output,
1861
+ stream_response_to=stream_response_to,
1576
1862
  max_polls=max_polls,
1577
1863
  poll_interval=poll_interval,
1578
1864
  )
datalab_sdk/models.py CHANGED
@@ -498,3 +498,14 @@ class CreateDocumentResult:
498
498
 
499
499
  with open(output_path, "wb") as f:
500
500
  f.write(base64.b64decode(self.output_base64))
501
+
502
+
503
+ @dataclass
504
+ class FileResult:
505
+ """Lightweight result returned when stream_response_to is used.
506
+ The full JSON response is written directly to disk at output_path,
507
+ never fully loaded into memory."""
508
+ success: bool
509
+ status: str
510
+ output_path: Path
511
+ error: Optional[str] = None
datalab_sdk/settings.py CHANGED
@@ -6,7 +6,7 @@ class Settings(BaseSettings):
6
6
  # Paths
7
7
  BASE_DIR: str = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
8
8
  LOGLEVEL: str = "DEBUG"
9
- VERSION: str = "0.3.0"
9
+ VERSION: str = "0.4.0"
10
10
 
11
11
  # Base settings
12
12
  DATALAB_API_KEY: str | None = None
@@ -1,12 +0,0 @@
1
- datalab_sdk/__init__.py,sha256=KjPwAikE20IeuZHKhLENi4Aqrb9IpJV63nwNh8vB0so,1256
2
- datalab_sdk/cli.py,sha256=NELgNZstpBrhPoQHqsI67kNRYew_nJpq23_ENsWvmGY,38859
3
- datalab_sdk/client.py,sha256=zTY0_javpBQb8oJcNUoMqvbfkp2JkgWxo5m_TzZ7u4Y,61454
4
- datalab_sdk/exceptions.py,sha256=MDfNd-l152OUVhZSfEsE95McJq-IuOsrEsEQ4CRMbEI,785
5
- datalab_sdk/mimetypes.py,sha256=a4ULJmHxoR_RMsD0j51hx0t2Q7In_PzNm7UlWX07eEE,932
6
- datalab_sdk/models.py,sha256=RA3AaBgAiLHv2P9VibBbK7XUzpGPe8VBtB3axnYz7dw,16811
7
- datalab_sdk/settings.py,sha256=fGAqE-Kb7LUtz9wlDrQklWxQlQTiz8ilD4iRMT1xwyg,365
8
- datalab_python_sdk-0.3.0.dist-info/METADATA,sha256=Wf3x0RZTVDvi-47Ef9skUL4NO_5VKOwGcJcLqK9LGgY,2210
9
- datalab_python_sdk-0.3.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
10
- datalab_python_sdk-0.3.0.dist-info/entry_points.txt,sha256=rqWxO21UV57JxxyeF20iBTxHpzBYOA69hCHIqwdn-Ok,48
11
- datalab_python_sdk-0.3.0.dist-info/licenses/LICENSE,sha256=XGjT-Uz6t7UZzr3JCZfilsGcZEgMm_1aONcRunohxWg,1064
12
- datalab_python_sdk-0.3.0.dist-info/RECORD,,