datalab-python-sdk 0.1.14__tar.gz → 0.1.15__tar.gz

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.
Files changed (52) hide show
  1. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/PKG-INFO +1 -1
  2. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/datalab_sdk/cli.py +79 -33
  3. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/datalab_sdk/client.py +214 -2
  4. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/datalab_sdk/mimetypes.py +4 -0
  5. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/datalab_sdk/models.py +35 -0
  6. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/pyproject.toml +1 -1
  7. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/uv.lock +2 -2
  8. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/.github/workflows/ci.yml +0 -0
  9. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/.github/workflows/publish.yml +0 -0
  10. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/.gitignore +0 -0
  11. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/.pre-commit-config.yaml +0 -0
  12. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/.python-version +0 -0
  13. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/LICENSE +0 -0
  14. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/README.md +0 -0
  15. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/data/08-Lambda-Calculus.pptx +0 -0
  16. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/data/adversarial.pdf +0 -0
  17. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/data/bid_evaluation.docx +0 -0
  18. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/data/book_review.ppt +0 -0
  19. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/data/book_store.xls +0 -0
  20. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/data/chi_hind.png +0 -0
  21. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/data/how_to_read.doc +0 -0
  22. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/data/normandy.epub +0 -0
  23. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/data/sample-1-sheet.xlsx +0 -0
  24. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/data/thinkpython.pdf +0 -0
  25. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/data/vibe.html +0 -0
  26. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/datalab_sdk/__init__.py +0 -0
  27. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/datalab_sdk/exceptions.py +0 -0
  28. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/datalab_sdk/settings.py +0 -0
  29. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/integration/README.md +0 -0
  30. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/integration/__init__.py +0 -0
  31. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/integration/test_live_api.py +0 -0
  32. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/integration/test_readme_examples.py +0 -0
  33. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/poetry.lock +0 -0
  34. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/pytest.ini +0 -0
  35. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/recipes/workflows/README.md +0 -0
  36. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/recipes/workflows/end_to_end_workflow.py +0 -0
  37. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/recipes/workflows/workflow_api_tutorial/1_get_step_types.py +0 -0
  38. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/recipes/workflows/workflow_api_tutorial/2_get_workflows.py +0 -0
  39. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/recipes/workflows/workflow_api_tutorial/3_create_workflow.py +0 -0
  40. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/recipes/workflows/workflow_api_tutorial/4_execute_workflow.py +0 -0
  41. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/recipes/workflows/workflow_api_tutorial/5_poll_workflow.py +0 -0
  42. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/recipes/workflows/workflow_api_tutorial/README.md +0 -0
  43. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/recipes/workflows/workflow_definitions/README.md +0 -0
  44. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/recipes/workflows/workflow_definitions/eval_segmentation.json +0 -0
  45. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/recipes/workflows/workflow_definitions/parse_segment.json +0 -0
  46. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/recipes/workflows/workflow_definitions/segment_parallel_extract.json +0 -0
  47. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/recipes/workflows/workflow_definitions/slack_alert.json +0 -0
  48. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/tests/__init__.py +0 -0
  49. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/tests/conftest.py +0 -0
  50. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/tests/test_cli_simple.py +0 -0
  51. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/tests/test_client_methods.py +0 -0
  52. {datalab_python_sdk-0.1.14 → datalab_python_sdk-0.1.15}/tests/test_workflows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datalab-python-sdk
3
- Version: 0.1.14
3
+ Version: 0.1.15
4
4
  Summary: SDK for the Datalab document intelligence API
5
5
  Author-email: Datalab Team <hi@datalab.to>
6
6
  License-Expression: MIT
@@ -93,6 +93,9 @@ def marker_options(func):
93
93
  func = click.option(
94
94
  "--page_schema", help="Schema to set to do structured extraction"
95
95
  )(func)
96
+ func = click.option(
97
+ "--add_block_ids", is_flag=True, help="Add block IDs to HTML output"
98
+ )(func)
96
99
  return func
97
100
 
98
101
 
@@ -139,13 +142,19 @@ async def process_files_async(
139
142
  ) as client:
140
143
  if method == "convert":
141
144
  result = await client.convert(
142
- file_path, options=options, save_output=output_path,
143
- max_polls=max_polls, poll_interval=poll_interval
145
+ file_path,
146
+ options=options,
147
+ save_output=output_path,
148
+ max_polls=max_polls,
149
+ poll_interval=poll_interval,
144
150
  )
145
151
  else: # method == 'ocr'
146
152
  result = await client.ocr(
147
- file_path, options=options, save_output=output_path,
148
- max_polls=max_polls, poll_interval=poll_interval
153
+ file_path,
154
+ options=options,
155
+ save_output=output_path,
156
+ max_polls=max_polls,
157
+ poll_interval=poll_interval,
149
158
  )
150
159
 
151
160
  return {
@@ -247,6 +256,7 @@ def process_documents(
247
256
  disable_image_extraction: bool = False,
248
257
  block_correction_prompt: Optional[str] = None,
249
258
  page_schema: Optional[str] = None,
259
+ add_block_ids: bool = False,
250
260
  ):
251
261
  """Unified document processing function"""
252
262
  try:
@@ -290,6 +300,7 @@ def process_documents(
290
300
  block_correction_prompt=block_correction_prompt,
291
301
  skip_cache=skip_cache,
292
302
  page_schema=page_schema,
303
+ add_block_ids=add_block_ids,
293
304
  )
294
305
  else: # method == "ocr"
295
306
  options = OCROptions(
@@ -352,6 +363,7 @@ def convert(
352
363
  disable_image_extraction: bool,
353
364
  block_correction_prompt: Optional[str],
354
365
  page_schema: Optional[str],
366
+ add_block_ids: bool,
355
367
  ):
356
368
  """Convert documents to markdown, HTML, or JSON"""
357
369
  process_documents(
@@ -376,6 +388,7 @@ def convert(
376
388
  disable_image_extraction=disable_image_extraction,
377
389
  block_correction_prompt=block_correction_prompt,
378
390
  page_schema=page_schema,
391
+ add_block_ids=add_block_ids,
379
392
  )
380
393
 
381
394
 
@@ -465,7 +478,7 @@ def create_workflow(
465
478
  name=name, team_id=team_id, steps=workflow_steps
466
479
  )
467
480
 
468
- click.echo(f"Workflow created successfully!")
481
+ click.echo("Workflow created successfully!")
469
482
  click.echo(f" ID: {workflow.id}")
470
483
  click.echo(f" Name: {workflow.name}")
471
484
  click.echo(f" Team ID: {workflow.team_id}")
@@ -497,7 +510,7 @@ def get_workflow(workflow_id: int, api_key: Optional[str], base_url: str):
497
510
  client = DatalabClient(api_key=api_key, base_url=base_url)
498
511
  workflow = client.get_workflow(workflow_id)
499
512
 
500
- click.echo(f"Workflow Details:")
513
+ click.echo("Workflow Details:")
501
514
  click.echo(f" ID: {workflow.id}")
502
515
  click.echo(f" Name: {workflow.name}")
503
516
  click.echo(f" Team ID: {workflow.team_id}")
@@ -642,13 +655,15 @@ def execute_workflow(
642
655
  input_config=input_cfg,
643
656
  )
644
657
 
645
- click.echo(f"\nSuccessfully triggered workflow execution!")
658
+ click.echo("\nSuccessfully triggered workflow execution!")
646
659
  click.echo(f" Execution ID: {execution.id}")
647
660
  click.echo(f" Status: {execution.status}")
648
- click.echo(f"\nTo check the status, run:")
661
+ click.echo("\nTo check the status, run:")
649
662
  click.echo(f" datalab get-execution-status --execution_id {execution.id}")
650
- click.echo(f"\n Or poll until complete:")
651
- click.echo(f" datalab get-execution-status --execution_id {execution.id} --max_polls 300 --poll_interval 2")
663
+ click.echo("\n Or poll until complete:")
664
+ click.echo(
665
+ f" datalab get-execution-status --execution_id {execution.id} --max_polls 300 --poll_interval 2"
666
+ )
652
667
 
653
668
  except DatalabError as e:
654
669
  click.echo(f"Error: {e}", err=True)
@@ -667,13 +682,18 @@ def execute_workflow(
667
682
  help="Output file path to save execution results",
668
683
  )
669
684
  @click.option(
670
- "--max_polls", default=1, type=int, help="Maximum number of polling attempts (1 for single check)"
685
+ "--max_polls",
686
+ default=1,
687
+ type=int,
688
+ help="Maximum number of polling attempts (1 for single check)",
689
+ )
690
+ @click.option(
691
+ "--poll_interval", default=1, type=int, help="Polling interval in seconds"
671
692
  )
672
- @click.option("--poll_interval", default=1, type=int, help="Polling interval in seconds")
673
693
  @click.option(
674
694
  "--download",
675
695
  is_flag=True,
676
- help="Download actual results from presigned URLs (default: just show URLs)"
696
+ help="Download actual results from presigned URLs (default: just show URLs)",
677
697
  )
678
698
  @click.option("--api_key", required=False, help="Datalab API key")
679
699
  @click.option("--base_url", default=settings.DATALAB_HOST, help="API base URL")
@@ -704,7 +724,7 @@ def get_execution_status(
704
724
  download_results=download,
705
725
  )
706
726
 
707
- click.echo(f"Execution Status:")
727
+ click.echo("Execution Status:")
708
728
  click.echo(f" Execution ID: {execution.id}")
709
729
  click.echo(f" Workflow ID: {execution.workflow_id}")
710
730
  click.echo(f" Status: {execution.status}")
@@ -712,7 +732,7 @@ def get_execution_status(
712
732
  click.echo(f" Created: {execution.created}")
713
733
 
714
734
  if execution.steps:
715
- click.echo(f"\n Step Results:")
735
+ click.echo("\n Step Results:")
716
736
  for step_name, step_data in execution.steps.items():
717
737
  click.echo(f"\n [{step_name}]")
718
738
 
@@ -720,20 +740,34 @@ def get_execution_status(
720
740
  for file_key, file_step_data in step_data.items():
721
741
  if isinstance(file_step_data, dict):
722
742
  click.echo(f"\n File/Group: {file_key}")
723
- click.echo(f" Step ID: {file_step_data.get('id', 'N/A')}")
724
- click.echo(f" Status: {file_step_data.get('status', 'N/A')}")
743
+ click.echo(
744
+ f" Step ID: {file_step_data.get('id', 'N/A')}"
745
+ )
746
+ click.echo(
747
+ f" Status: {file_step_data.get('status', 'N/A')}"
748
+ )
725
749
 
726
- if file_step_data.get('started_at'):
727
- click.echo(f" Started: {file_step_data.get('started_at')}")
728
- if file_step_data.get('finished_at'):
729
- click.echo(f" Finished: {file_step_data.get('finished_at')}")
730
- if file_step_data.get('file_ids'):
731
- click.echo(f" Files: {', '.join(file_step_data.get('file_ids'))}")
750
+ if file_step_data.get("started_at"):
751
+ click.echo(
752
+ f" Started: {file_step_data.get('started_at')}"
753
+ )
754
+ if file_step_data.get("finished_at"):
755
+ click.echo(
756
+ f" Finished: {file_step_data.get('finished_at')}"
757
+ )
758
+ if file_step_data.get("file_ids"):
759
+ click.echo(
760
+ f" Files: {', '.join(file_step_data.get('file_ids'))}"
761
+ )
732
762
 
733
763
  if "output_url" in file_step_data and not download:
734
- click.echo(f" Output URL: {file_step_data.get('output_url')}")
764
+ click.echo(
765
+ f" Output URL: {file_step_data.get('output_url')}"
766
+ )
735
767
  elif download and "downloaded_data" in file_step_data:
736
- click.echo(f" Results: {json.dumps(file_step_data['downloaded_data'], indent=12)}")
768
+ click.echo(
769
+ f" Results: {json.dumps(file_step_data['downloaded_data'], indent=12)}"
770
+ )
737
771
  else:
738
772
  click.echo(f" {file_step_data}")
739
773
 
@@ -750,7 +784,9 @@ def get_execution_status(
750
784
 
751
785
 
752
786
  @click.command()
753
- @click.option("--definition", required=True, help="Path to workflow definition JSON file")
787
+ @click.option(
788
+ "--definition", required=True, help="Path to workflow definition JSON file"
789
+ )
754
790
  def visualize_workflow(definition: str):
755
791
  """Visualize workflow DAG from a JSON definition file"""
756
792
  try:
@@ -763,7 +799,7 @@ def visualize_workflow(definition: str):
763
799
  if not definition_path.exists():
764
800
  raise DatalabError(f"Definition file not found: {definition}")
765
801
 
766
- with open(definition_path, 'r') as f:
802
+ with open(definition_path, "r") as f:
767
803
  workflow_def = json.load(f)
768
804
 
769
805
  name = workflow_def.get("name", "Unnamed Workflow")
@@ -776,9 +812,9 @@ def visualize_workflow(definition: str):
776
812
  # Build dependency graph
777
813
  step_map = {step["unique_name"]: step for step in steps}
778
814
 
779
- click.echo(f"\n{'='*70}")
815
+ click.echo(f"\n{'=' * 70}")
780
816
  click.echo(f"Workflow: {name}")
781
- click.echo(f"{'='*70}\n")
817
+ click.echo(f"{'=' * 70}\n")
782
818
 
783
819
  # Build parent/child maps
784
820
  children = defaultdict(list)
@@ -788,9 +824,13 @@ def visualize_workflow(definition: str):
788
824
  children[dep].append(step_name)
789
825
 
790
826
  # Topological sort to determine layers
791
- in_degree = {step["unique_name"]: len(step.get("depends_on", [])) for step in steps}
827
+ in_degree = {
828
+ step["unique_name"]: len(step.get("depends_on", [])) for step in steps
829
+ }
792
830
  layers = []
793
- queue = deque([s["unique_name"] for s in steps if in_degree[s["unique_name"]] == 0])
831
+ queue = deque(
832
+ [s["unique_name"] for s in steps if in_degree[s["unique_name"]] == 0]
833
+ )
794
834
 
795
835
  while queue:
796
836
  layer = []
@@ -844,11 +884,17 @@ def _render_dag_simple(layers, children, step_map):
844
884
 
845
885
  # Show the step - color code by step type
846
886
  step_color = "blue" if "marker" in step_key else "magenta"
847
- click.echo(f" • {click.style(step_name, fg=step_color)} ({click.style(step_key, fg='white', dim=True)})")
887
+ click.echo(
888
+ f" • {click.style(step_name, fg=step_color)} ({click.style(step_key, fg='white', dim=True)})"
889
+ )
848
890
 
849
891
  # Show dependencies if any
850
892
  if depends:
851
- click.echo(click.style(f" ← depends on: {', '.join(depends)}", fg="white", dim=True))
893
+ click.echo(
894
+ click.style(
895
+ f" ← depends on: {', '.join(depends)}", fg="white", dim=True
896
+ )
897
+ )
852
898
 
853
899
  # Show what comes next
854
900
  if layer_idx < len(layers) - 1:
@@ -109,7 +109,8 @@ class AsyncDatalabClient:
109
109
  except aiohttp.ClientResponseError as e:
110
110
  try:
111
111
  error_data = await response.json()
112
- error_message = error_data.get("error", str(e))
112
+ # FastAPI returns errors in "detail" field, but some APIs use "error"
113
+ error_message = error_data.get("detail") or error_data.get("error") or str(e)
113
114
  except Exception:
114
115
  error_message = str(e)
115
116
  raise DatalabAPIError(
@@ -180,6 +181,15 @@ class AsyncDatalabClient:
180
181
  if not file_path.exists():
181
182
  raise DatalabFileError(f"File not found: {file_path}")
182
183
 
184
+ # Read file content
185
+ file_data = file_path.read_bytes()
186
+
187
+ # Check if file is empty
188
+ if not file_data:
189
+ raise DatalabFileError(
190
+ f"File is empty: {file_path}. Please provide a file with content."
191
+ )
192
+
183
193
  # Determine MIME type
184
194
  mime_type, _ = mimetypes.guess_type(str(file_path))
185
195
  if not mime_type:
@@ -187,7 +197,7 @@ class AsyncDatalabClient:
187
197
  extension = file_path.suffix.lower()
188
198
  mime_type = MIMETYPE_MAP.get(extension, "application/octet-stream")
189
199
 
190
- return file_path.name, file_path.read_bytes(), mime_type
200
+ return file_path.name, file_data, mime_type
191
201
 
192
202
  def get_form_params(self, file_path=None, file_url=None, options=None):
193
203
  form_data = aiohttp.FormData()
@@ -267,11 +277,17 @@ class AsyncDatalabClient:
267
277
  json=result_data.get("json"),
268
278
  chunks=result_data.get("chunks"),
269
279
  extraction_schema_json=result_data.get("extraction_schema_json"),
280
+ segmentation_results=result_data.get("segmentation_results"),
270
281
  images=result_data.get("images"),
271
282
  metadata=result_data.get("metadata"),
272
283
  error=result_data.get("error"),
273
284
  page_count=result_data.get("page_count"),
274
285
  status=result_data.get("status", "complete"),
286
+ checkpoint_id=result_data.get("checkpoint_id"),
287
+ versions=result_data.get("versions"),
288
+ parse_quality_score=result_data.get("parse_quality_score"),
289
+ runtime=result_data.get("runtime"),
290
+ cost_breakdown=result_data.get("cost_breakdown"),
275
291
  )
276
292
 
277
293
  # Save output if requested
@@ -317,6 +333,8 @@ class AsyncDatalabClient:
317
333
  error=result_data.get("error"),
318
334
  page_count=result_data.get("page_count"),
319
335
  status=result_data.get("status", "complete"),
336
+ versions=result_data.get("versions"),
337
+ cost_breakdown=result_data.get("cost_breakdown"),
320
338
  )
321
339
 
322
340
  # Save output if requested
@@ -464,6 +482,31 @@ class AsyncDatalabClient:
464
482
 
465
483
  return workflows
466
484
 
485
+ async def delete_workflow(self, workflow_id: int) -> Dict[str, Any]:
486
+ """
487
+ Delete a workflow definition
488
+
489
+ Args:
490
+ workflow_id: ID of the workflow to delete
491
+
492
+ Returns:
493
+ Dictionary containing:
494
+ - success: Whether the deletion was successful
495
+ - message: Confirmation message
496
+
497
+ Raises:
498
+ DatalabAPIError: If workflow has executions or cannot be deleted
499
+ """
500
+ response = await self._make_request(
501
+ "DELETE",
502
+ f"/api/v1/workflows/workflows/{workflow_id}",
503
+ )
504
+
505
+ return {
506
+ "success": response.get("success", True),
507
+ "message": response.get("message", f"Workflow {workflow_id} deleted successfully"),
508
+ }
509
+
467
510
  async def execute_workflow(
468
511
  self,
469
512
  workflow_id: int,
@@ -800,6 +843,95 @@ class AsyncDatalabClient:
800
843
  "offset": response.get("offset", offset),
801
844
  }
802
845
 
846
+ async def get_file_metadata(
847
+ self,
848
+ file_id: Union[int, str],
849
+ ) -> UploadedFileMetadata:
850
+ """
851
+ Get metadata for an uploaded file
852
+
853
+ Args:
854
+ file_id: File ID (integer or hashid string)
855
+
856
+ Returns:
857
+ UploadedFileMetadata object with file information
858
+ """
859
+ response = await self._make_request(
860
+ "GET",
861
+ f"/api/v1/files/{file_id}",
862
+ )
863
+
864
+ return UploadedFileMetadata(
865
+ file_id=response["file_id"],
866
+ original_filename=response["original_filename"],
867
+ content_type=response["content_type"],
868
+ reference=response["reference"],
869
+ upload_status=response["upload_status"],
870
+ file_size=response.get("file_size"),
871
+ created=response.get("created"),
872
+ )
873
+
874
+ async def get_file_download_url(
875
+ self,
876
+ file_id: Union[int, str],
877
+ expires_in: int = 3600,
878
+ ) -> Dict[str, Any]:
879
+ """
880
+ Generate presigned URL for downloading a file
881
+
882
+ Args:
883
+ file_id: File ID (integer or hashid string)
884
+ expires_in: URL expiry time in seconds (default: 3600, max: 86400)
885
+
886
+ Returns:
887
+ Dictionary containing:
888
+ - download_url: Presigned URL for downloading the file
889
+ - expires_in: URL expiry time in seconds
890
+ - file_id: File ID
891
+ - original_filename: Original filename
892
+ """
893
+ if expires_in < 60 or expires_in > 86400:
894
+ raise ValueError("expires_in must be between 60 and 86400 seconds")
895
+
896
+ response = await self._make_request(
897
+ "GET",
898
+ f"/api/v1/files/{file_id}/download?expires_in={expires_in}",
899
+ )
900
+
901
+ return {
902
+ "download_url": response["download_url"],
903
+ "expires_in": response["expires_in"],
904
+ "file_id": response["file_id"],
905
+ "original_filename": response["original_filename"],
906
+ }
907
+
908
+ async def delete_file(
909
+ self,
910
+ file_id: Union[int, str],
911
+ ) -> Dict[str, Any]:
912
+ """
913
+ Delete an uploaded file
914
+
915
+ Removes the file from both storage and the database.
916
+
917
+ Args:
918
+ file_id: File ID (integer or hashid string)
919
+
920
+ Returns:
921
+ Dictionary containing:
922
+ - success: Whether the deletion was successful
923
+ - message: Confirmation message
924
+ """
925
+ response = await self._make_request(
926
+ "DELETE",
927
+ f"/api/v1/files/{file_id}",
928
+ )
929
+
930
+ return {
931
+ "success": response.get("success", True),
932
+ "message": response.get("message", f"File {file_id} deleted successfully"),
933
+ }
934
+
803
935
 
804
936
  class DatalabClient:
805
937
  """Synchronous wrapper around AsyncDatalabClient"""
@@ -993,3 +1125,83 @@ class DatalabClient:
993
1125
  return self._run_async(
994
1126
  self._async_client.list_files(limit=limit, offset=offset)
995
1127
  )
1128
+
1129
+ def get_file_metadata(
1130
+ self,
1131
+ file_id: Union[int, str],
1132
+ ) -> UploadedFileMetadata:
1133
+ """
1134
+ Get metadata for an uploaded file (sync version)
1135
+
1136
+ Args:
1137
+ file_id: File ID (integer or hashid string)
1138
+
1139
+ Returns:
1140
+ UploadedFileMetadata object with file information
1141
+ """
1142
+ return self._run_async(
1143
+ self._async_client.get_file_metadata(file_id=file_id)
1144
+ )
1145
+
1146
+ def get_file_download_url(
1147
+ self,
1148
+ file_id: Union[int, str],
1149
+ expires_in: int = 3600,
1150
+ ) -> Dict[str, Any]:
1151
+ """
1152
+ Generate presigned URL for downloading a file (sync version)
1153
+
1154
+ Args:
1155
+ file_id: File ID (integer or hashid string)
1156
+ expires_in: URL expiry time in seconds (default: 3600, max: 86400)
1157
+
1158
+ Returns:
1159
+ Dictionary containing:
1160
+ - download_url: Presigned URL for downloading the file
1161
+ - expires_in: URL expiry time in seconds
1162
+ - file_id: File ID
1163
+ - original_filename: Original filename
1164
+ """
1165
+ return self._run_async(
1166
+ self._async_client.get_file_download_url(file_id=file_id, expires_in=expires_in)
1167
+ )
1168
+
1169
+ def delete_file(
1170
+ self,
1171
+ file_id: Union[int, str],
1172
+ ) -> Dict[str, Any]:
1173
+ """
1174
+ Delete an uploaded file (sync version)
1175
+
1176
+ Removes the file from both storage and the database.
1177
+
1178
+ Args:
1179
+ file_id: File ID (integer or hashid string)
1180
+
1181
+ Returns:
1182
+ Dictionary containing:
1183
+ - success: Whether the deletion was successful
1184
+ - message: Confirmation message
1185
+ """
1186
+ return self._run_async(
1187
+ self._async_client.delete_file(file_id=file_id)
1188
+ )
1189
+
1190
+ def delete_workflow(self, workflow_id: int) -> Dict[str, Any]:
1191
+ """
1192
+ Delete a workflow definition (sync version)
1193
+
1194
+ Args:
1195
+ workflow_id: ID of the workflow to delete
1196
+
1197
+ Returns:
1198
+ Dictionary containing:
1199
+ - success: Whether the deletion was successful
1200
+ - message: Confirmation message
1201
+
1202
+ Raises:
1203
+ DatalabAPIError: If workflow has executions or cannot be deleted
1204
+ """
1205
+ return self._run_async(
1206
+ self._async_client.delete_workflow(workflow_id=workflow_id)
1207
+ )
@@ -9,7 +9,11 @@ MIMETYPE_MAP = {
9
9
  ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
10
10
  ".doc": "application/msword",
11
11
  ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
12
+ ".xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12",
13
+ ".xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
12
14
  ".xls": "application/vnd.ms-excel",
15
+ ".csv": "text/csv",
16
+ ".ods": "application/vnd.oasis.opendocument.spreadsheet",
13
17
  ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
14
18
  ".ppt": "application/vnd.ms-powerpoint",
15
19
  ".html": "text/html",
@@ -44,13 +44,40 @@ class ConvertOptions(ProcessingOptions):
44
44
  use_llm: bool = False
45
45
  strip_existing_ocr: bool = False
46
46
  disable_image_extraction: bool = False
47
+ disable_ocr_math: bool = False
47
48
  block_correction_prompt: Optional[str] = None
48
49
  additional_config: Optional[Dict[str, Any]] = None
49
50
  page_schema: Optional[Dict[str, Any]] = None
51
+ segmentation_schema: Optional[str] = None # JSON string for document segmentation
52
+ save_checkpoint: bool = False
53
+ extras: Optional[str] = (
54
+ None # Comma-separated list: 'track_changes', 'chart_understanding'
55
+ )
50
56
  output_format: str = "markdown" # markdown, json, html, chunks
51
57
  mode: str = "fast" # fast, balanced, accurate
58
+ keep_spreadsheet_formatting: bool = False
52
59
  webhook_url: Optional[str] = None
53
60
  extras: Optional[str] = None # comma-separated extras
61
+ add_block_ids: bool = False # add block IDs to HTML output
62
+
63
+ def to_form_data(self) -> Dict[str, Any]:
64
+ """Convert to form data format for API requests"""
65
+ # Start with parent's form data
66
+ form_data = super().to_form_data()
67
+
68
+ # Remove keep_spreadsheet_formatting from top-level (it goes in additional_config)
69
+ form_data.pop("keep_spreadsheet_formatting", None)
70
+
71
+ additional_config_dict = {}
72
+ if self.additional_config:
73
+ additional_config_dict.update(self.additional_config)
74
+ if self.keep_spreadsheet_formatting:
75
+ additional_config_dict["keep_spreadsheet_formatting"] = True
76
+
77
+ if additional_config_dict:
78
+ form_data["additional_config"] = (None, json.dumps(additional_config_dict))
79
+
80
+ return form_data
54
81
 
55
82
 
56
83
  @dataclass
@@ -69,11 +96,17 @@ class ConversionResult:
69
96
  json: Optional[Dict[str, Any]] = None
70
97
  chunks: Optional[Dict[str, Any]] = None
71
98
  extraction_schema_json: Optional[str] = None
99
+ segmentation_results: Optional[Dict[str, Any]] = None
72
100
  images: Optional[Dict[str, str]] = None
73
101
  metadata: Optional[Dict[str, Any]] = None
74
102
  error: Optional[str] = None
75
103
  page_count: Optional[int] = None
76
104
  status: str = "complete"
105
+ checkpoint_id: Optional[str] = None
106
+ versions: Optional[Union[Dict[str, Any], str]] = None
107
+ parse_quality_score: Optional[float] = None
108
+ runtime: Optional[float] = None
109
+ cost_breakdown: Optional[Dict[str, Any]] = None
77
110
 
78
111
  def save_output(
79
112
  self, output_path: Union[str, Path], save_images: bool = True
@@ -270,6 +303,8 @@ class OCRResult:
270
303
  error: Optional[str] = None
271
304
  page_count: Optional[int] = None
272
305
  status: str = "complete"
306
+ versions: Optional[Union[Dict[str, Any], str]] = None
307
+ cost_breakdown: Optional[Dict[str, Any]] = None
273
308
 
274
309
  def get_text(self, page_num: Optional[int] = None) -> str:
275
310
  """Extract text from OCR results"""
@@ -7,7 +7,7 @@ readme = "README.md"
7
7
  license = "MIT"
8
8
  repository = "https://github.com/datalab-to/sdk"
9
9
  keywords = ["datalab", "sdk", "document-intelligence", "api"]
10
- version = "0.1.14"
10
+ version = "0.1.15"
11
11
  description = "SDK for the Datalab document intelligence API"
12
12
  requires-python = ">=3.10"
13
13
  dependencies = [
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 2
2
+ revision = 3
3
3
  requires-python = ">=3.10"
4
4
 
5
5
  [[package]]
@@ -212,7 +212,7 @@ wheels = [
212
212
 
213
213
  [[package]]
214
214
  name = "datalab-python-sdk"
215
- version = "0.1.14"
215
+ version = "0.1.15"
216
216
  source = { editable = "." }
217
217
  dependencies = [
218
218
  { name = "aiohttp" },