datalab-python-sdk 0.4.0__py3-none-any.whl → 0.5.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.
- {datalab_python_sdk-0.4.0.dist-info → datalab_python_sdk-0.5.0.dist-info}/METADATA +1 -1
- datalab_python_sdk-0.5.0.dist-info/RECORD +12 -0
- datalab_sdk/__init__.py +18 -0
- datalab_sdk/client.py +841 -17
- datalab_sdk/models.py +169 -5
- datalab_python_sdk-0.4.0.dist-info/RECORD +0 -12
- {datalab_python_sdk-0.4.0.dist-info → datalab_python_sdk-0.5.0.dist-info}/WHEEL +0 -0
- {datalab_python_sdk-0.4.0.dist-info → datalab_python_sdk-0.5.0.dist-info}/entry_points.txt +0 -0
- {datalab_python_sdk-0.4.0.dist-info → datalab_python_sdk-0.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
datalab_sdk/__init__.py,sha256=3knlmAjdZBwfVqppayHc9YxBmG47KCtRMxK1K3-OyWI,1746
|
|
2
|
+
datalab_sdk/cli.py,sha256=NELgNZstpBrhPoQHqsI67kNRYew_nJpq23_ENsWvmGY,38859
|
|
3
|
+
datalab_sdk/client.py,sha256=BQoWuHDoEUJYHFSojzdsNEB5sc90Cj53YYavHZUTLg4,105118
|
|
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=vuC94U1c76ooLShrnO_7lzzfBjiJkHcsSZceYJJ8ggQ,22427
|
|
7
|
+
datalab_sdk/settings.py,sha256=BoojrRcKE7T04s-vqCPFzKqopV_qqTrAtCCsjHKk3FU,365
|
|
8
|
+
datalab_python_sdk-0.5.0.dist-info/METADATA,sha256=PNRCAWHCTcyU4wCisIwljO6h6qj9EYAB0okbQcc6r00,2238
|
|
9
|
+
datalab_python_sdk-0.5.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
10
|
+
datalab_python_sdk-0.5.0.dist-info/entry_points.txt,sha256=rqWxO21UV57JxxyeF20iBTxHpzBYOA69hCHIqwdn-Ok,48
|
|
11
|
+
datalab_python_sdk-0.5.0.dist-info/licenses/LICENSE,sha256=XGjT-Uz6t7UZzr3JCZfilsGcZEgMm_1aONcRunohxWg,1064
|
|
12
|
+
datalab_python_sdk-0.5.0.dist-info/RECORD,,
|
datalab_sdk/__init__.py
CHANGED
|
@@ -15,6 +15,7 @@ from .models import (
|
|
|
15
15
|
ConvertOptions,
|
|
16
16
|
ExtractOptions,
|
|
17
17
|
SegmentOptions,
|
|
18
|
+
CustomProcessorOptions,
|
|
18
19
|
CustomPipelineOptions,
|
|
19
20
|
TrackChangesOptions,
|
|
20
21
|
OCROptions,
|
|
@@ -25,6 +26,14 @@ from .models import (
|
|
|
25
26
|
WorkflowExecution,
|
|
26
27
|
InputConfig,
|
|
27
28
|
UploadedFileMetadata,
|
|
29
|
+
ExtractionSchema,
|
|
30
|
+
PipelineProcessor,
|
|
31
|
+
PipelineConfig,
|
|
32
|
+
PipelineVersion,
|
|
33
|
+
PipelineExecution,
|
|
34
|
+
PipelineExecutionStepResult,
|
|
35
|
+
CustomProcessor,
|
|
36
|
+
CustomProcessorVersion,
|
|
28
37
|
)
|
|
29
38
|
from .settings import settings
|
|
30
39
|
|
|
@@ -42,6 +51,7 @@ __all__ = [
|
|
|
42
51
|
"ConvertOptions",
|
|
43
52
|
"ExtractOptions",
|
|
44
53
|
"SegmentOptions",
|
|
54
|
+
"CustomProcessorOptions",
|
|
45
55
|
"CustomPipelineOptions",
|
|
46
56
|
"TrackChangesOptions",
|
|
47
57
|
"OCROptions",
|
|
@@ -52,4 +62,12 @@ __all__ = [
|
|
|
52
62
|
"WorkflowExecution",
|
|
53
63
|
"InputConfig",
|
|
54
64
|
"UploadedFileMetadata",
|
|
65
|
+
"ExtractionSchema",
|
|
66
|
+
"PipelineProcessor",
|
|
67
|
+
"PipelineConfig",
|
|
68
|
+
"PipelineVersion",
|
|
69
|
+
"PipelineExecution",
|
|
70
|
+
"PipelineExecutionStepResult",
|
|
71
|
+
"CustomProcessor",
|
|
72
|
+
"CustomProcessorVersion",
|
|
55
73
|
]
|
datalab_sdk/client.py
CHANGED
|
@@ -36,6 +36,7 @@ from datalab_sdk.models import (
|
|
|
36
36
|
ConvertOptions,
|
|
37
37
|
ExtractOptions,
|
|
38
38
|
SegmentOptions,
|
|
39
|
+
CustomProcessorOptions,
|
|
39
40
|
CustomPipelineOptions,
|
|
40
41
|
TrackChangesOptions,
|
|
41
42
|
OCROptions,
|
|
@@ -46,6 +47,14 @@ from datalab_sdk.models import (
|
|
|
46
47
|
WorkflowExecution,
|
|
47
48
|
InputConfig,
|
|
48
49
|
UploadedFileMetadata,
|
|
50
|
+
ExtractionSchema,
|
|
51
|
+
PipelineProcessor,
|
|
52
|
+
PipelineConfig,
|
|
53
|
+
PipelineVersion,
|
|
54
|
+
PipelineExecution,
|
|
55
|
+
PipelineExecutionStepResult,
|
|
56
|
+
CustomProcessor,
|
|
57
|
+
CustomProcessorVersion,
|
|
49
58
|
)
|
|
50
59
|
from datalab_sdk.settings import settings
|
|
51
60
|
|
|
@@ -512,15 +521,15 @@ class AsyncDatalabClient:
|
|
|
512
521
|
poll_interval: int = 1,
|
|
513
522
|
) -> Union[ConversionResult, FileResult]:
|
|
514
523
|
"""
|
|
515
|
-
Extract structured data from a document using a JSON schema
|
|
524
|
+
Extract structured data from a document using a JSON schema or saved extraction schema
|
|
516
525
|
|
|
517
|
-
Provide a
|
|
518
|
-
|
|
526
|
+
Provide a page_schema for inline extraction, or a schema_id to use a saved
|
|
527
|
+
extraction schema. These are mutually exclusive.
|
|
519
528
|
|
|
520
529
|
Args:
|
|
521
530
|
file_path: Path to the file to extract from
|
|
522
531
|
file_url: URL of the file to extract from
|
|
523
|
-
options: Extraction options (must include page_schema)
|
|
532
|
+
options: Extraction options (must include page_schema or schema_id)
|
|
524
533
|
save_output: Optional path to save output files
|
|
525
534
|
stream_response_to: Optional path to stream raw JSON response to disk
|
|
526
535
|
max_polls: Maximum number of polling attempts
|
|
@@ -536,7 +545,17 @@ class AsyncDatalabClient:
|
|
|
536
545
|
raise ValueError(f"Directory does not exist: {resolved_stream_response_to.parent}")
|
|
537
546
|
|
|
538
547
|
if options is None:
|
|
539
|
-
raise ValueError("options must be provided with page_schema")
|
|
548
|
+
raise ValueError("options must be provided with page_schema or schema_id")
|
|
549
|
+
|
|
550
|
+
has_page_schema = bool(options.page_schema)
|
|
551
|
+
has_schema_id = bool(options.schema_id)
|
|
552
|
+
|
|
553
|
+
if has_page_schema and has_schema_id:
|
|
554
|
+
raise ValueError("page_schema and schema_id are mutually exclusive. Provide one or the other.")
|
|
555
|
+
if not has_page_schema and not has_schema_id:
|
|
556
|
+
raise ValueError("Either page_schema or schema_id must be provided in options.")
|
|
557
|
+
if options.schema_version is not None and not has_schema_id:
|
|
558
|
+
raise ValueError("schema_version can only be used with schema_id.")
|
|
540
559
|
|
|
541
560
|
has_file = file_path is not None or file_url is not None
|
|
542
561
|
has_checkpoint = options.checkpoint_id is not None
|
|
@@ -635,23 +654,23 @@ class AsyncDatalabClient:
|
|
|
635
654
|
|
|
636
655
|
return result
|
|
637
656
|
|
|
638
|
-
async def
|
|
657
|
+
async def run_custom_processor(
|
|
639
658
|
self,
|
|
640
659
|
file_path: Optional[Union[str, Path]] = None,
|
|
641
660
|
file_url: Optional[str] = None,
|
|
642
|
-
options: Optional[
|
|
661
|
+
options: Optional[CustomProcessorOptions] = None,
|
|
643
662
|
save_output: Optional[Union[str, Path]] = None,
|
|
644
663
|
stream_response_to: Optional[Union[str, Path]] = None,
|
|
645
664
|
max_polls: int = 300,
|
|
646
665
|
poll_interval: int = 1,
|
|
647
666
|
) -> Union[ConversionResult, FileResult]:
|
|
648
667
|
"""
|
|
649
|
-
Execute a custom
|
|
668
|
+
Execute a custom processor on a document
|
|
650
669
|
|
|
651
670
|
Args:
|
|
652
671
|
file_path: Path to the file to process
|
|
653
672
|
file_url: URL of the file to process
|
|
654
|
-
options: Custom
|
|
673
|
+
options: Custom processor options (must include pipeline_id)
|
|
655
674
|
save_output: Optional path to save output files
|
|
656
675
|
stream_response_to: Optional path to stream raw JSON response to disk
|
|
657
676
|
max_polls: Maximum number of polling attempts
|
|
@@ -670,7 +689,7 @@ class AsyncDatalabClient:
|
|
|
670
689
|
raise ValueError("options must be provided with pipeline_id")
|
|
671
690
|
|
|
672
691
|
result_data = await self._submit_and_poll(
|
|
673
|
-
"/api/v1/custom-
|
|
692
|
+
"/api/v1/custom-processor",
|
|
674
693
|
data=self.get_form_params(
|
|
675
694
|
file_path=file_path, file_url=file_url, options=options
|
|
676
695
|
),
|
|
@@ -691,6 +710,36 @@ class AsyncDatalabClient:
|
|
|
691
710
|
|
|
692
711
|
return result
|
|
693
712
|
|
|
713
|
+
async def run_custom_pipeline(
|
|
714
|
+
self,
|
|
715
|
+
file_path: Optional[Union[str, Path]] = None,
|
|
716
|
+
file_url: Optional[str] = None,
|
|
717
|
+
options: Optional[CustomProcessorOptions] = None,
|
|
718
|
+
save_output: Optional[Union[str, Path]] = None,
|
|
719
|
+
stream_response_to: Optional[Union[str, Path]] = None,
|
|
720
|
+
max_polls: int = 300,
|
|
721
|
+
poll_interval: int = 1,
|
|
722
|
+
) -> Union[ConversionResult, FileResult]:
|
|
723
|
+
"""Execute a custom processor on a document
|
|
724
|
+
|
|
725
|
+
.. deprecated::
|
|
726
|
+
Use run_custom_processor() instead.
|
|
727
|
+
"""
|
|
728
|
+
warnings.warn(
|
|
729
|
+
"run_custom_pipeline() is deprecated. Use run_custom_processor() instead.",
|
|
730
|
+
DeprecationWarning,
|
|
731
|
+
stacklevel=2,
|
|
732
|
+
)
|
|
733
|
+
return await self.run_custom_processor(
|
|
734
|
+
file_path=file_path,
|
|
735
|
+
file_url=file_url,
|
|
736
|
+
options=options,
|
|
737
|
+
save_output=save_output,
|
|
738
|
+
stream_response_to=stream_response_to,
|
|
739
|
+
max_polls=max_polls,
|
|
740
|
+
poll_interval=poll_interval,
|
|
741
|
+
)
|
|
742
|
+
|
|
694
743
|
async def track_changes(
|
|
695
744
|
self,
|
|
696
745
|
file_path: Optional[Union[str, Path]] = None,
|
|
@@ -1567,6 +1616,546 @@ class AsyncDatalabClient:
|
|
|
1567
1616
|
"message": response.get("message", f"File {file_id} deleted successfully"),
|
|
1568
1617
|
}
|
|
1569
1618
|
|
|
1619
|
+
# --- Extraction Schema methods ---
|
|
1620
|
+
|
|
1621
|
+
async def create_extraction_schema(
|
|
1622
|
+
self,
|
|
1623
|
+
name: str,
|
|
1624
|
+
schema_json: Dict[str, Any],
|
|
1625
|
+
description: Optional[str] = None,
|
|
1626
|
+
) -> ExtractionSchema:
|
|
1627
|
+
"""
|
|
1628
|
+
Create a new extraction schema
|
|
1629
|
+
|
|
1630
|
+
Args:
|
|
1631
|
+
name: Name for the schema (max 200 characters)
|
|
1632
|
+
schema_json: JSON schema for extraction (must contain 'properties' key)
|
|
1633
|
+
description: Optional description
|
|
1634
|
+
"""
|
|
1635
|
+
payload: Dict[str, Any] = {"name": name, "schema_json": schema_json}
|
|
1636
|
+
if description is not None:
|
|
1637
|
+
payload["description"] = description
|
|
1638
|
+
|
|
1639
|
+
response = await self._make_request("POST", "/api/v1/extraction_schemas", json=payload)
|
|
1640
|
+
return self._build_extraction_schema(response)
|
|
1641
|
+
|
|
1642
|
+
async def list_extraction_schemas(
|
|
1643
|
+
self,
|
|
1644
|
+
limit: int = 50,
|
|
1645
|
+
offset: int = 0,
|
|
1646
|
+
include_archived: bool = False,
|
|
1647
|
+
) -> Dict[str, Any]:
|
|
1648
|
+
"""
|
|
1649
|
+
List extraction schemas for the authenticated user's team
|
|
1650
|
+
|
|
1651
|
+
Args:
|
|
1652
|
+
limit: Maximum number of schemas to return (default: 50, max: 200)
|
|
1653
|
+
offset: Offset for pagination (default: 0)
|
|
1654
|
+
include_archived: Include archived schemas (default: False)
|
|
1655
|
+
"""
|
|
1656
|
+
params = f"limit={limit}&offset={offset}&include_archived={str(include_archived).lower()}"
|
|
1657
|
+
response = await self._make_request("GET", f"/api/v1/extraction_schemas?{params}")
|
|
1658
|
+
return {
|
|
1659
|
+
"schemas": [self._build_extraction_schema(s) for s in response.get("schemas", [])],
|
|
1660
|
+
"total": response.get("total", 0),
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
async def get_extraction_schema(self, schema_id: str) -> ExtractionSchema:
|
|
1664
|
+
"""
|
|
1665
|
+
Get an extraction schema by its schema_id
|
|
1666
|
+
|
|
1667
|
+
Args:
|
|
1668
|
+
schema_id: Schema ID string (e.g. sch_k8Hx9mP2nQ4v)
|
|
1669
|
+
"""
|
|
1670
|
+
response = await self._make_request("GET", f"/api/v1/extraction_schemas/{schema_id}")
|
|
1671
|
+
return self._build_extraction_schema(response)
|
|
1672
|
+
|
|
1673
|
+
async def update_extraction_schema(
|
|
1674
|
+
self,
|
|
1675
|
+
schema_id: str,
|
|
1676
|
+
name: Optional[str] = None,
|
|
1677
|
+
description: Optional[str] = None,
|
|
1678
|
+
schema_json: Optional[Dict[str, Any]] = None,
|
|
1679
|
+
archived: Optional[bool] = None,
|
|
1680
|
+
create_new_version: bool = False,
|
|
1681
|
+
) -> ExtractionSchema:
|
|
1682
|
+
"""
|
|
1683
|
+
Update an extraction schema
|
|
1684
|
+
|
|
1685
|
+
Args:
|
|
1686
|
+
schema_id: Schema ID string (e.g. sch_k8Hx9mP2nQ4v)
|
|
1687
|
+
name: New name (max 200 characters)
|
|
1688
|
+
description: New description
|
|
1689
|
+
schema_json: New JSON schema (must contain 'properties' key)
|
|
1690
|
+
archived: Set archived status
|
|
1691
|
+
create_new_version: If True, bump version and save current state to history
|
|
1692
|
+
"""
|
|
1693
|
+
payload: Dict[str, Any] = {}
|
|
1694
|
+
if name is not None:
|
|
1695
|
+
payload["name"] = name
|
|
1696
|
+
if description is not None:
|
|
1697
|
+
payload["description"] = description
|
|
1698
|
+
if schema_json is not None:
|
|
1699
|
+
payload["schema_json"] = schema_json
|
|
1700
|
+
if archived is not None:
|
|
1701
|
+
payload["archived"] = archived
|
|
1702
|
+
if create_new_version:
|
|
1703
|
+
payload["create_new_version"] = True
|
|
1704
|
+
|
|
1705
|
+
response = await self._make_request("PUT", f"/api/v1/extraction_schemas/{schema_id}", json=payload)
|
|
1706
|
+
return self._build_extraction_schema(response)
|
|
1707
|
+
|
|
1708
|
+
async def delete_extraction_schema(self, schema_id: str) -> ExtractionSchema:
|
|
1709
|
+
"""
|
|
1710
|
+
Delete (archive) an extraction schema
|
|
1711
|
+
|
|
1712
|
+
Args:
|
|
1713
|
+
schema_id: Schema ID string (e.g. sch_k8Hx9mP2nQ4v)
|
|
1714
|
+
"""
|
|
1715
|
+
response = await self._make_request("DELETE", f"/api/v1/extraction_schemas/{schema_id}")
|
|
1716
|
+
return self._build_extraction_schema(response)
|
|
1717
|
+
|
|
1718
|
+
@staticmethod
|
|
1719
|
+
def _build_extraction_schema(data: Dict[str, Any]) -> ExtractionSchema:
|
|
1720
|
+
return ExtractionSchema(
|
|
1721
|
+
id=data.get("id"),
|
|
1722
|
+
schema_id=data["schema_id"],
|
|
1723
|
+
name=data["name"],
|
|
1724
|
+
description=data.get("description"),
|
|
1725
|
+
schema_json=data["schema_json"],
|
|
1726
|
+
version=data.get("version", 1),
|
|
1727
|
+
version_history=data.get("version_history"),
|
|
1728
|
+
archived=data.get("archived", False),
|
|
1729
|
+
created=data.get("created"),
|
|
1730
|
+
updated=data.get("updated"),
|
|
1731
|
+
)
|
|
1732
|
+
|
|
1733
|
+
# --- Pipeline CRUD methods ---
|
|
1734
|
+
|
|
1735
|
+
async def create_pipeline(
|
|
1736
|
+
self,
|
|
1737
|
+
steps: list[PipelineProcessor],
|
|
1738
|
+
) -> PipelineConfig:
|
|
1739
|
+
"""
|
|
1740
|
+
Create a new pipeline
|
|
1741
|
+
|
|
1742
|
+
Args:
|
|
1743
|
+
steps: Ordered list of PipelineProcessor objects
|
|
1744
|
+
"""
|
|
1745
|
+
payload = {"steps": [s.to_dict() for s in steps]}
|
|
1746
|
+
response = await self._make_request("POST", "/api/v1/pipelines", json=payload)
|
|
1747
|
+
return self._build_pipeline_config(response)
|
|
1748
|
+
|
|
1749
|
+
async def list_pipelines(
|
|
1750
|
+
self,
|
|
1751
|
+
saved_only: bool = True,
|
|
1752
|
+
include_archived: bool = False,
|
|
1753
|
+
limit: int = 50,
|
|
1754
|
+
offset: int = 0,
|
|
1755
|
+
) -> Dict[str, Any]:
|
|
1756
|
+
"""
|
|
1757
|
+
List pipelines for the authenticated user's team
|
|
1758
|
+
|
|
1759
|
+
Args:
|
|
1760
|
+
saved_only: Only return saved pipelines (default: True)
|
|
1761
|
+
include_archived: Include archived pipelines (default: False)
|
|
1762
|
+
limit: Maximum number to return (default: 50, max: 200)
|
|
1763
|
+
offset: Offset for pagination (default: 0)
|
|
1764
|
+
"""
|
|
1765
|
+
params = (
|
|
1766
|
+
f"saved_only={str(saved_only).lower()}"
|
|
1767
|
+
f"&include_archived={str(include_archived).lower()}"
|
|
1768
|
+
f"&limit={limit}&offset={offset}"
|
|
1769
|
+
)
|
|
1770
|
+
response = await self._make_request("GET", f"/api/v1/pipelines?{params}")
|
|
1771
|
+
return {
|
|
1772
|
+
"pipelines": [self._build_pipeline_config(p) for p in response.get("pipelines", [])],
|
|
1773
|
+
"total": response.get("total", 0),
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
async def get_pipeline(self, pipeline_id: str) -> PipelineConfig:
|
|
1777
|
+
"""
|
|
1778
|
+
Get a pipeline by its pipeline_id
|
|
1779
|
+
|
|
1780
|
+
Args:
|
|
1781
|
+
pipeline_id: Pipeline ID string (e.g. pl_k8Hx9mP2nQ4v)
|
|
1782
|
+
"""
|
|
1783
|
+
response = await self._make_request("GET", f"/api/v1/pipelines/{pipeline_id}")
|
|
1784
|
+
return self._build_pipeline_config(response)
|
|
1785
|
+
|
|
1786
|
+
async def update_pipeline(
|
|
1787
|
+
self,
|
|
1788
|
+
pipeline_id: str,
|
|
1789
|
+
steps: list[PipelineProcessor],
|
|
1790
|
+
) -> PipelineConfig:
|
|
1791
|
+
"""
|
|
1792
|
+
Update pipeline steps (auto-save path for draft edits)
|
|
1793
|
+
|
|
1794
|
+
Args:
|
|
1795
|
+
pipeline_id: Pipeline ID string
|
|
1796
|
+
steps: New ordered list of PipelineProcessor objects
|
|
1797
|
+
"""
|
|
1798
|
+
payload = {"steps": [s.to_dict() for s in steps]}
|
|
1799
|
+
response = await self._make_request("PUT", f"/api/v1/pipelines/{pipeline_id}", json=payload)
|
|
1800
|
+
return self._build_pipeline_config(response)
|
|
1801
|
+
|
|
1802
|
+
async def save_pipeline(
|
|
1803
|
+
self,
|
|
1804
|
+
pipeline_id: str,
|
|
1805
|
+
name: str = "",
|
|
1806
|
+
) -> PipelineConfig:
|
|
1807
|
+
"""
|
|
1808
|
+
Name and promote a pipeline to saved status
|
|
1809
|
+
|
|
1810
|
+
Args:
|
|
1811
|
+
pipeline_id: Pipeline ID string
|
|
1812
|
+
name: Display name (auto-generated if empty)
|
|
1813
|
+
"""
|
|
1814
|
+
response = await self._make_request(
|
|
1815
|
+
"PUT", f"/api/v1/pipelines/{pipeline_id}/save", json={"name": name}
|
|
1816
|
+
)
|
|
1817
|
+
return self._build_pipeline_config(response)
|
|
1818
|
+
|
|
1819
|
+
async def archive_pipeline(self, pipeline_id: str) -> Dict[str, Any]:
|
|
1820
|
+
"""Archive a pipeline, hiding it from the default list"""
|
|
1821
|
+
return await self._make_request("POST", f"/api/v1/pipelines/{pipeline_id}/archive")
|
|
1822
|
+
|
|
1823
|
+
async def unarchive_pipeline(self, pipeline_id: str) -> Dict[str, Any]:
|
|
1824
|
+
"""Unarchive a pipeline, restoring it to the default list"""
|
|
1825
|
+
return await self._make_request("POST", f"/api/v1/pipelines/{pipeline_id}/unarchive")
|
|
1826
|
+
|
|
1827
|
+
# --- Pipeline Versioning methods ---
|
|
1828
|
+
|
|
1829
|
+
async def create_pipeline_version(
|
|
1830
|
+
self,
|
|
1831
|
+
pipeline_id: str,
|
|
1832
|
+
description: Optional[str] = None,
|
|
1833
|
+
) -> PipelineVersion:
|
|
1834
|
+
"""
|
|
1835
|
+
Create a new version snapshot of the pipeline's current steps
|
|
1836
|
+
|
|
1837
|
+
Args:
|
|
1838
|
+
pipeline_id: Pipeline ID string
|
|
1839
|
+
description: Optional description for this version
|
|
1840
|
+
"""
|
|
1841
|
+
payload: Dict[str, Any] = {}
|
|
1842
|
+
if description is not None:
|
|
1843
|
+
payload["description"] = description
|
|
1844
|
+
response = await self._make_request(
|
|
1845
|
+
"POST", f"/api/v1/pipelines/{pipeline_id}/versions", json=payload
|
|
1846
|
+
)
|
|
1847
|
+
return PipelineVersion(
|
|
1848
|
+
id=response.get("id"),
|
|
1849
|
+
version=response["version"],
|
|
1850
|
+
steps=response.get("steps", []),
|
|
1851
|
+
description=response.get("description"),
|
|
1852
|
+
created=response.get("created"),
|
|
1853
|
+
)
|
|
1854
|
+
|
|
1855
|
+
async def list_pipeline_versions(self, pipeline_id: str) -> Dict[str, Any]:
|
|
1856
|
+
"""List all versions of a pipeline, newest first"""
|
|
1857
|
+
response = await self._make_request("GET", f"/api/v1/pipelines/{pipeline_id}/versions")
|
|
1858
|
+
return {
|
|
1859
|
+
"versions": [
|
|
1860
|
+
PipelineVersion(
|
|
1861
|
+
id=v.get("id"),
|
|
1862
|
+
version=v["version"],
|
|
1863
|
+
steps=v.get("steps", []),
|
|
1864
|
+
description=v.get("description"),
|
|
1865
|
+
created=v.get("created"),
|
|
1866
|
+
)
|
|
1867
|
+
for v in response.get("versions", [])
|
|
1868
|
+
],
|
|
1869
|
+
"total": response.get("total", 0),
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
async def discard_pipeline_draft(
|
|
1873
|
+
self,
|
|
1874
|
+
pipeline_id: str,
|
|
1875
|
+
version: Optional[int] = None,
|
|
1876
|
+
) -> PipelineConfig:
|
|
1877
|
+
"""
|
|
1878
|
+
Discard draft changes and revert to a published version
|
|
1879
|
+
|
|
1880
|
+
Args:
|
|
1881
|
+
pipeline_id: Pipeline ID string
|
|
1882
|
+
version: Version to revert to (default: active published version)
|
|
1883
|
+
"""
|
|
1884
|
+
payload: Dict[str, Any] = {}
|
|
1885
|
+
if version is not None:
|
|
1886
|
+
payload["version"] = version
|
|
1887
|
+
response = await self._make_request(
|
|
1888
|
+
"POST", f"/api/v1/pipelines/{pipeline_id}/discard", json=payload
|
|
1889
|
+
)
|
|
1890
|
+
return self._build_pipeline_config(response)
|
|
1891
|
+
|
|
1892
|
+
async def get_pipeline_rate(self, pipeline_id: str) -> Dict[str, Any]:
|
|
1893
|
+
"""
|
|
1894
|
+
Get the per-page rate for a pipeline
|
|
1895
|
+
|
|
1896
|
+
Returns dict with rate_per_1000_pages_cents and rate_breakdown.
|
|
1897
|
+
"""
|
|
1898
|
+
return await self._make_request("GET", f"/api/v1/pipelines/{pipeline_id}/rate")
|
|
1899
|
+
|
|
1900
|
+
# --- Pipeline Execution methods ---
|
|
1901
|
+
|
|
1902
|
+
async def run_pipeline(
|
|
1903
|
+
self,
|
|
1904
|
+
pipeline_id: str,
|
|
1905
|
+
file_path: Optional[Union[str, Path]] = None,
|
|
1906
|
+
file_url: Optional[str] = None,
|
|
1907
|
+
page_range: Optional[str] = None,
|
|
1908
|
+
output_format: Optional[str] = None,
|
|
1909
|
+
run_evals: bool = False,
|
|
1910
|
+
skip_cache: bool = False,
|
|
1911
|
+
webhook_url: Optional[str] = None,
|
|
1912
|
+
version: Optional[int] = None,
|
|
1913
|
+
max_polls: int = 1,
|
|
1914
|
+
poll_interval: int = 1,
|
|
1915
|
+
) -> PipelineExecution:
|
|
1916
|
+
"""
|
|
1917
|
+
Execute a pipeline on a file
|
|
1918
|
+
|
|
1919
|
+
Args:
|
|
1920
|
+
pipeline_id: Pipeline ID (pl_XXXXX)
|
|
1921
|
+
file_path: Path to the file to process
|
|
1922
|
+
file_url: URL of the file to process
|
|
1923
|
+
page_range: Page range to process (e.g. '0,2-4')
|
|
1924
|
+
output_format: Output format (json, html, markdown, chunks)
|
|
1925
|
+
run_evals: Whether to run evaluation steps
|
|
1926
|
+
skip_cache: Skip executor cache
|
|
1927
|
+
webhook_url: URL to POST when complete
|
|
1928
|
+
version: Pipeline version to execute (0=draft, omit=active)
|
|
1929
|
+
max_polls: Maximum polling attempts after submission (default: 1)
|
|
1930
|
+
poll_interval: Seconds between polls
|
|
1931
|
+
"""
|
|
1932
|
+
form_data = self.get_form_params(file_path=file_path, file_url=file_url)
|
|
1933
|
+
if page_range is not None:
|
|
1934
|
+
form_data.add_field("page_range", page_range)
|
|
1935
|
+
if output_format is not None:
|
|
1936
|
+
form_data.add_field("output_format", output_format)
|
|
1937
|
+
if run_evals:
|
|
1938
|
+
form_data.add_field("run_evals", str(run_evals))
|
|
1939
|
+
if skip_cache:
|
|
1940
|
+
form_data.add_field("skip_cache", str(skip_cache))
|
|
1941
|
+
if webhook_url is not None:
|
|
1942
|
+
form_data.add_field("webhook_url", webhook_url)
|
|
1943
|
+
if version is not None:
|
|
1944
|
+
form_data.add_field("version", str(version))
|
|
1945
|
+
|
|
1946
|
+
response = await self._submit_with_retry(
|
|
1947
|
+
f"/api/v1/pipelines/{pipeline_id}/run", data=form_data
|
|
1948
|
+
)
|
|
1949
|
+
execution = self._build_pipeline_execution(response)
|
|
1950
|
+
|
|
1951
|
+
# Poll if requested
|
|
1952
|
+
if max_polls > 1 and execution.status not in ("completed", "completed_with_errors", "failed"):
|
|
1953
|
+
return await self.get_pipeline_execution(
|
|
1954
|
+
execution.execution_id, max_polls=max_polls - 1, poll_interval=poll_interval
|
|
1955
|
+
)
|
|
1956
|
+
return execution
|
|
1957
|
+
|
|
1958
|
+
async def get_pipeline_execution(
|
|
1959
|
+
self,
|
|
1960
|
+
execution_id: str,
|
|
1961
|
+
max_polls: int = 1,
|
|
1962
|
+
poll_interval: int = 1,
|
|
1963
|
+
) -> PipelineExecution:
|
|
1964
|
+
"""
|
|
1965
|
+
Get the status of a pipeline execution, optionally polling until completion
|
|
1966
|
+
|
|
1967
|
+
Args:
|
|
1968
|
+
execution_id: Execution ID (pex_XXXXX)
|
|
1969
|
+
max_polls: Maximum polling attempts (default: 1 for single check)
|
|
1970
|
+
poll_interval: Seconds between polls
|
|
1971
|
+
"""
|
|
1972
|
+
for i in range(max_polls):
|
|
1973
|
+
response = await self._make_request(
|
|
1974
|
+
"GET", f"/api/v1/pipelines/executions/{execution_id}"
|
|
1975
|
+
)
|
|
1976
|
+
execution = self._build_pipeline_execution(response)
|
|
1977
|
+
|
|
1978
|
+
if execution.status in ("completed", "completed_with_errors", "failed"):
|
|
1979
|
+
return execution
|
|
1980
|
+
|
|
1981
|
+
if i < max_polls - 1:
|
|
1982
|
+
await asyncio.sleep(poll_interval)
|
|
1983
|
+
|
|
1984
|
+
return execution
|
|
1985
|
+
|
|
1986
|
+
async def list_pipeline_executions(
|
|
1987
|
+
self,
|
|
1988
|
+
pipeline_id: str,
|
|
1989
|
+
limit: int = 20,
|
|
1990
|
+
offset: int = 0,
|
|
1991
|
+
) -> Dict[str, Any]:
|
|
1992
|
+
"""List recent executions for a pipeline"""
|
|
1993
|
+
response = await self._make_request(
|
|
1994
|
+
"GET", f"/api/v1/pipelines/{pipeline_id}/executions?limit={limit}&offset={offset}"
|
|
1995
|
+
)
|
|
1996
|
+
return {
|
|
1997
|
+
"executions": [
|
|
1998
|
+
self._build_pipeline_execution(e) for e in response.get("executions", [])
|
|
1999
|
+
],
|
|
2000
|
+
"total": response.get("total", 0),
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
async def get_step_result(
|
|
2004
|
+
self,
|
|
2005
|
+
execution_id: str,
|
|
2006
|
+
step_index: int,
|
|
2007
|
+
) -> Dict[str, Any]:
|
|
2008
|
+
"""
|
|
2009
|
+
Fetch the result of a specific pipeline execution step
|
|
2010
|
+
|
|
2011
|
+
Args:
|
|
2012
|
+
execution_id: Execution ID (pex_XXXXX)
|
|
2013
|
+
step_index: Zero-based step index
|
|
2014
|
+
"""
|
|
2015
|
+
return await self._make_request(
|
|
2016
|
+
"GET", f"/api/v1/pipelines/executions/{execution_id}/steps/{step_index}/result"
|
|
2017
|
+
)
|
|
2018
|
+
|
|
2019
|
+
@staticmethod
|
|
2020
|
+
def _build_pipeline_config(data: Dict[str, Any]) -> PipelineConfig:
|
|
2021
|
+
return PipelineConfig(
|
|
2022
|
+
id=data.get("id"),
|
|
2023
|
+
pipeline_id=data["pipeline_id"],
|
|
2024
|
+
name=data.get("name"),
|
|
2025
|
+
steps=data.get("steps", []),
|
|
2026
|
+
is_saved=data.get("is_saved", False),
|
|
2027
|
+
archived=data.get("archived", False),
|
|
2028
|
+
active_version=data.get("active_version", 0),
|
|
2029
|
+
created=data.get("created"),
|
|
2030
|
+
updated=data.get("updated"),
|
|
2031
|
+
)
|
|
2032
|
+
|
|
2033
|
+
@staticmethod
|
|
2034
|
+
def _build_pipeline_execution(data: Dict[str, Any]) -> PipelineExecution:
|
|
2035
|
+
steps = [
|
|
2036
|
+
PipelineExecutionStepResult(
|
|
2037
|
+
step_index=s["step_index"],
|
|
2038
|
+
step_type=s["step_type"],
|
|
2039
|
+
status=s["status"],
|
|
2040
|
+
lookup_key=s.get("lookup_key"),
|
|
2041
|
+
result_url=s.get("result_url"),
|
|
2042
|
+
started_at=s.get("started_at"),
|
|
2043
|
+
finished_at=s.get("finished_at"),
|
|
2044
|
+
error_message=s.get("error_message"),
|
|
2045
|
+
checkpoint_id=s.get("checkpoint_id"),
|
|
2046
|
+
)
|
|
2047
|
+
for s in data.get("steps", [])
|
|
2048
|
+
]
|
|
2049
|
+
return PipelineExecution(
|
|
2050
|
+
execution_id=data["execution_id"],
|
|
2051
|
+
pipeline_id=data.get("pipeline_id", ""),
|
|
2052
|
+
pipeline_version=data.get("pipeline_version", 0),
|
|
2053
|
+
status=data.get("status", "pending"),
|
|
2054
|
+
steps=steps,
|
|
2055
|
+
started_at=data.get("started_at"),
|
|
2056
|
+
completed_at=data.get("completed_at"),
|
|
2057
|
+
created=data.get("created"),
|
|
2058
|
+
config_snapshot=data.get("config_snapshot"),
|
|
2059
|
+
input_config=data.get("input_config"),
|
|
2060
|
+
rate_breakdown=data.get("rate_breakdown"),
|
|
2061
|
+
)
|
|
2062
|
+
|
|
2063
|
+
# --- Custom Processor Management methods ---
|
|
2064
|
+
|
|
2065
|
+
async def list_custom_processors(
|
|
2066
|
+
self,
|
|
2067
|
+
limit: int = 50,
|
|
2068
|
+
offset: int = 0,
|
|
2069
|
+
) -> Dict[str, Any]:
|
|
2070
|
+
"""
|
|
2071
|
+
List custom processors for the authenticated user's team
|
|
2072
|
+
|
|
2073
|
+
Args:
|
|
2074
|
+
limit: Maximum number to return (default: 50)
|
|
2075
|
+
offset: Offset for pagination (default: 0)
|
|
2076
|
+
"""
|
|
2077
|
+
response = await self._make_request(
|
|
2078
|
+
"GET", f"/api/v1/custom_processors?limit={limit}&offset={offset}"
|
|
2079
|
+
)
|
|
2080
|
+
processors = [
|
|
2081
|
+
CustomProcessor(
|
|
2082
|
+
processor_id=p["processor_id"],
|
|
2083
|
+
name=p.get("name"),
|
|
2084
|
+
status=p.get("status", ""),
|
|
2085
|
+
success=p.get("success"),
|
|
2086
|
+
active_version=p.get("active_version", 0),
|
|
2087
|
+
max_version=p.get("max_version", 0),
|
|
2088
|
+
iteration_in_progress=p.get("iteration_in_progress", False),
|
|
2089
|
+
pipeline_id=p.get("pipeline_id"),
|
|
2090
|
+
created_at=p.get("created_at"),
|
|
2091
|
+
completed_at=p.get("completed_at"),
|
|
2092
|
+
error_message=p.get("error_message"),
|
|
2093
|
+
eval_rubric_id=p.get("eval_rubric_id"),
|
|
2094
|
+
)
|
|
2095
|
+
for p in response.get("pipelines", [])
|
|
2096
|
+
]
|
|
2097
|
+
return {"processors": processors}
|
|
2098
|
+
|
|
2099
|
+
async def get_custom_processor_status(self, lookup_key: str) -> Dict[str, Any]:
|
|
2100
|
+
"""
|
|
2101
|
+
Check the status of a custom processor request
|
|
2102
|
+
|
|
2103
|
+
Args:
|
|
2104
|
+
lookup_key: The lookup key returned when the processor was submitted
|
|
2105
|
+
"""
|
|
2106
|
+
return await self._make_request("GET", f"/api/v1/custom_processors/{lookup_key}")
|
|
2107
|
+
|
|
2108
|
+
async def list_custom_processor_versions(self, processor_id: str) -> Dict[str, Any]:
|
|
2109
|
+
"""
|
|
2110
|
+
List versions of a custom processor
|
|
2111
|
+
|
|
2112
|
+
Args:
|
|
2113
|
+
processor_id: Processor ID (cp_XXXXX)
|
|
2114
|
+
"""
|
|
2115
|
+
response = await self._make_request(
|
|
2116
|
+
"GET", f"/api/v1/custom_processors/{processor_id}/versions"
|
|
2117
|
+
)
|
|
2118
|
+
versions = [
|
|
2119
|
+
CustomProcessorVersion(
|
|
2120
|
+
version=v["version"],
|
|
2121
|
+
request_description=v.get("request_description", ""),
|
|
2122
|
+
created_at=v.get("created_at"),
|
|
2123
|
+
runtime=v.get("runtime"),
|
|
2124
|
+
is_active=v.get("is_active", False),
|
|
2125
|
+
)
|
|
2126
|
+
for v in response.get("versions", [])
|
|
2127
|
+
]
|
|
2128
|
+
return {"versions": versions}
|
|
2129
|
+
|
|
2130
|
+
async def set_active_processor_version(
|
|
2131
|
+
self,
|
|
2132
|
+
processor_id: str,
|
|
2133
|
+
version: int,
|
|
2134
|
+
) -> Dict[str, Any]:
|
|
2135
|
+
"""
|
|
2136
|
+
Set the active version of a custom processor
|
|
2137
|
+
|
|
2138
|
+
Args:
|
|
2139
|
+
processor_id: Processor ID (cp_XXXXX)
|
|
2140
|
+
version: Version number to activate
|
|
2141
|
+
"""
|
|
2142
|
+
form_data = aiohttp.FormData()
|
|
2143
|
+
form_data.add_field("version", str(version))
|
|
2144
|
+
return await self._submit_with_retry(
|
|
2145
|
+
f"/api/v1/custom_processors/{processor_id}/set_active", data=form_data
|
|
2146
|
+
)
|
|
2147
|
+
|
|
2148
|
+
async def archive_custom_processor(self, processor_id: str) -> Dict[str, Any]:
|
|
2149
|
+
"""
|
|
2150
|
+
Archive a custom processor
|
|
2151
|
+
|
|
2152
|
+
Args:
|
|
2153
|
+
processor_id: Processor ID (cp_XXXXX)
|
|
2154
|
+
"""
|
|
2155
|
+
return await self._make_request(
|
|
2156
|
+
"POST", f"/api/v1/custom_processors/{processor_id}/archive"
|
|
2157
|
+
)
|
|
2158
|
+
|
|
1570
2159
|
|
|
1571
2160
|
class DatalabClient:
|
|
1572
2161
|
"""Synchronous wrapper around AsyncDatalabClient"""
|
|
@@ -1646,12 +2235,12 @@ class DatalabClient:
|
|
|
1646
2235
|
poll_interval: int = 1,
|
|
1647
2236
|
) -> Union[ConversionResult, FileResult]:
|
|
1648
2237
|
"""
|
|
1649
|
-
Extract structured data
|
|
2238
|
+
Extract structured data using a JSON schema or saved extraction schema (sync version)
|
|
1650
2239
|
|
|
1651
2240
|
Args:
|
|
1652
2241
|
file_path: Path to the file to extract from
|
|
1653
2242
|
file_url: URL of the file to extract from
|
|
1654
|
-
options: Extraction options (must include page_schema)
|
|
2243
|
+
options: Extraction options (must include page_schema or schema_id)
|
|
1655
2244
|
save_output: Optional path to save output files
|
|
1656
2245
|
stream_response_to: Optional path to stream raw JSON response to disk
|
|
1657
2246
|
max_polls: Maximum number of polling attempts
|
|
@@ -1703,30 +2292,30 @@ class DatalabClient:
|
|
|
1703
2292
|
)
|
|
1704
2293
|
)
|
|
1705
2294
|
|
|
1706
|
-
def
|
|
2295
|
+
def run_custom_processor(
|
|
1707
2296
|
self,
|
|
1708
2297
|
file_path: Optional[Union[str, Path]] = None,
|
|
1709
2298
|
file_url: Optional[str] = None,
|
|
1710
|
-
options: Optional[
|
|
2299
|
+
options: Optional[CustomProcessorOptions] = None,
|
|
1711
2300
|
save_output: Optional[Union[str, Path]] = None,
|
|
1712
2301
|
stream_response_to: Optional[Union[str, Path]] = None,
|
|
1713
2302
|
max_polls: int = 300,
|
|
1714
2303
|
poll_interval: int = 1,
|
|
1715
2304
|
) -> Union[ConversionResult, FileResult]:
|
|
1716
2305
|
"""
|
|
1717
|
-
Execute a custom
|
|
2306
|
+
Execute a custom processor on a document (sync version)
|
|
1718
2307
|
|
|
1719
2308
|
Args:
|
|
1720
2309
|
file_path: Path to the file to process
|
|
1721
2310
|
file_url: URL of the file to process
|
|
1722
|
-
options: Custom
|
|
2311
|
+
options: Custom processor options (must include pipeline_id)
|
|
1723
2312
|
save_output: Optional path to save output files
|
|
1724
2313
|
stream_response_to: Optional path to stream raw JSON response to disk
|
|
1725
2314
|
max_polls: Maximum number of polling attempts
|
|
1726
2315
|
poll_interval: Seconds between polling attempts
|
|
1727
2316
|
"""
|
|
1728
2317
|
return self._run_async(
|
|
1729
|
-
self._async_client.
|
|
2318
|
+
self._async_client.run_custom_processor(
|
|
1730
2319
|
file_path=file_path,
|
|
1731
2320
|
file_url=file_url,
|
|
1732
2321
|
options=options,
|
|
@@ -1737,6 +2326,36 @@ class DatalabClient:
|
|
|
1737
2326
|
)
|
|
1738
2327
|
)
|
|
1739
2328
|
|
|
2329
|
+
def run_custom_pipeline(
|
|
2330
|
+
self,
|
|
2331
|
+
file_path: Optional[Union[str, Path]] = None,
|
|
2332
|
+
file_url: Optional[str] = None,
|
|
2333
|
+
options: Optional[CustomProcessorOptions] = None,
|
|
2334
|
+
save_output: Optional[Union[str, Path]] = None,
|
|
2335
|
+
stream_response_to: Optional[Union[str, Path]] = None,
|
|
2336
|
+
max_polls: int = 300,
|
|
2337
|
+
poll_interval: int = 1,
|
|
2338
|
+
) -> Union[ConversionResult, FileResult]:
|
|
2339
|
+
"""Execute a custom processor on a document (sync version)
|
|
2340
|
+
|
|
2341
|
+
.. deprecated::
|
|
2342
|
+
Use run_custom_processor() instead.
|
|
2343
|
+
"""
|
|
2344
|
+
warnings.warn(
|
|
2345
|
+
"run_custom_pipeline() is deprecated. Use run_custom_processor() instead.",
|
|
2346
|
+
DeprecationWarning,
|
|
2347
|
+
stacklevel=2,
|
|
2348
|
+
)
|
|
2349
|
+
return self.run_custom_processor(
|
|
2350
|
+
file_path=file_path,
|
|
2351
|
+
file_url=file_url,
|
|
2352
|
+
options=options,
|
|
2353
|
+
save_output=save_output,
|
|
2354
|
+
stream_response_to=stream_response_to,
|
|
2355
|
+
max_polls=max_polls,
|
|
2356
|
+
poll_interval=poll_interval,
|
|
2357
|
+
)
|
|
2358
|
+
|
|
1740
2359
|
def track_changes(
|
|
1741
2360
|
self,
|
|
1742
2361
|
file_path: Optional[Union[str, Path]] = None,
|
|
@@ -2051,3 +2670,208 @@ class DatalabClient:
|
|
|
2051
2670
|
return self._run_async(
|
|
2052
2671
|
self._async_client.delete_workflow(workflow_id=workflow_id)
|
|
2053
2672
|
)
|
|
2673
|
+
|
|
2674
|
+
# --- Extraction Schema methods (sync) ---
|
|
2675
|
+
|
|
2676
|
+
def create_extraction_schema(
|
|
2677
|
+
self, name: str, schema_json: Dict[str, Any], description: Optional[str] = None,
|
|
2678
|
+
) -> ExtractionSchema:
|
|
2679
|
+
"""Create a new extraction schema (sync version)"""
|
|
2680
|
+
return self._run_async(
|
|
2681
|
+
self._async_client.create_extraction_schema(
|
|
2682
|
+
name=name, schema_json=schema_json, description=description,
|
|
2683
|
+
)
|
|
2684
|
+
)
|
|
2685
|
+
|
|
2686
|
+
def list_extraction_schemas(
|
|
2687
|
+
self, limit: int = 50, offset: int = 0, include_archived: bool = False,
|
|
2688
|
+
) -> Dict[str, Any]:
|
|
2689
|
+
"""List extraction schemas (sync version)"""
|
|
2690
|
+
return self._run_async(
|
|
2691
|
+
self._async_client.list_extraction_schemas(
|
|
2692
|
+
limit=limit, offset=offset, include_archived=include_archived,
|
|
2693
|
+
)
|
|
2694
|
+
)
|
|
2695
|
+
|
|
2696
|
+
def get_extraction_schema(self, schema_id: str) -> ExtractionSchema:
|
|
2697
|
+
"""Get an extraction schema by ID (sync version)"""
|
|
2698
|
+
return self._run_async(self._async_client.get_extraction_schema(schema_id=schema_id))
|
|
2699
|
+
|
|
2700
|
+
def update_extraction_schema(
|
|
2701
|
+
self,
|
|
2702
|
+
schema_id: str,
|
|
2703
|
+
name: Optional[str] = None,
|
|
2704
|
+
description: Optional[str] = None,
|
|
2705
|
+
schema_json: Optional[Dict[str, Any]] = None,
|
|
2706
|
+
archived: Optional[bool] = None,
|
|
2707
|
+
create_new_version: bool = False,
|
|
2708
|
+
) -> ExtractionSchema:
|
|
2709
|
+
"""Update an extraction schema (sync version)"""
|
|
2710
|
+
return self._run_async(
|
|
2711
|
+
self._async_client.update_extraction_schema(
|
|
2712
|
+
schema_id=schema_id, name=name, description=description,
|
|
2713
|
+
schema_json=schema_json, archived=archived,
|
|
2714
|
+
create_new_version=create_new_version,
|
|
2715
|
+
)
|
|
2716
|
+
)
|
|
2717
|
+
|
|
2718
|
+
def delete_extraction_schema(self, schema_id: str) -> ExtractionSchema:
|
|
2719
|
+
"""Delete (archive) an extraction schema (sync version)"""
|
|
2720
|
+
return self._run_async(self._async_client.delete_extraction_schema(schema_id=schema_id))
|
|
2721
|
+
|
|
2722
|
+
# --- Pipeline methods (sync) ---
|
|
2723
|
+
|
|
2724
|
+
def create_pipeline(self, steps: list[PipelineProcessor]) -> PipelineConfig:
|
|
2725
|
+
"""Create a new pipeline (sync version)"""
|
|
2726
|
+
return self._run_async(self._async_client.create_pipeline(steps=steps))
|
|
2727
|
+
|
|
2728
|
+
def list_pipelines(
|
|
2729
|
+
self, saved_only: bool = True, include_archived: bool = False,
|
|
2730
|
+
limit: int = 50, offset: int = 0,
|
|
2731
|
+
) -> Dict[str, Any]:
|
|
2732
|
+
"""List pipelines (sync version)"""
|
|
2733
|
+
return self._run_async(
|
|
2734
|
+
self._async_client.list_pipelines(
|
|
2735
|
+
saved_only=saved_only, include_archived=include_archived,
|
|
2736
|
+
limit=limit, offset=offset,
|
|
2737
|
+
)
|
|
2738
|
+
)
|
|
2739
|
+
|
|
2740
|
+
def get_pipeline(self, pipeline_id: str) -> PipelineConfig:
|
|
2741
|
+
"""Get a pipeline by ID (sync version)"""
|
|
2742
|
+
return self._run_async(self._async_client.get_pipeline(pipeline_id=pipeline_id))
|
|
2743
|
+
|
|
2744
|
+
def update_pipeline(self, pipeline_id: str, steps: list[PipelineProcessor]) -> PipelineConfig:
|
|
2745
|
+
"""Update pipeline steps (sync version)"""
|
|
2746
|
+
return self._run_async(
|
|
2747
|
+
self._async_client.update_pipeline(pipeline_id=pipeline_id, steps=steps)
|
|
2748
|
+
)
|
|
2749
|
+
|
|
2750
|
+
def save_pipeline(self, pipeline_id: str, name: str = "") -> PipelineConfig:
|
|
2751
|
+
"""Save and name a pipeline (sync version)"""
|
|
2752
|
+
return self._run_async(
|
|
2753
|
+
self._async_client.save_pipeline(pipeline_id=pipeline_id, name=name)
|
|
2754
|
+
)
|
|
2755
|
+
|
|
2756
|
+
def archive_pipeline(self, pipeline_id: str) -> Dict[str, Any]:
|
|
2757
|
+
"""Archive a pipeline (sync version)"""
|
|
2758
|
+
return self._run_async(self._async_client.archive_pipeline(pipeline_id=pipeline_id))
|
|
2759
|
+
|
|
2760
|
+
def unarchive_pipeline(self, pipeline_id: str) -> Dict[str, Any]:
|
|
2761
|
+
"""Unarchive a pipeline (sync version)"""
|
|
2762
|
+
return self._run_async(self._async_client.unarchive_pipeline(pipeline_id=pipeline_id))
|
|
2763
|
+
|
|
2764
|
+
def create_pipeline_version(
|
|
2765
|
+
self, pipeline_id: str, description: Optional[str] = None,
|
|
2766
|
+
) -> PipelineVersion:
|
|
2767
|
+
"""Create a pipeline version snapshot (sync version)"""
|
|
2768
|
+
return self._run_async(
|
|
2769
|
+
self._async_client.create_pipeline_version(
|
|
2770
|
+
pipeline_id=pipeline_id, description=description,
|
|
2771
|
+
)
|
|
2772
|
+
)
|
|
2773
|
+
|
|
2774
|
+
def list_pipeline_versions(self, pipeline_id: str) -> Dict[str, Any]:
|
|
2775
|
+
"""List pipeline versions (sync version)"""
|
|
2776
|
+
return self._run_async(self._async_client.list_pipeline_versions(pipeline_id=pipeline_id))
|
|
2777
|
+
|
|
2778
|
+
def discard_pipeline_draft(
|
|
2779
|
+
self, pipeline_id: str, version: Optional[int] = None,
|
|
2780
|
+
) -> PipelineConfig:
|
|
2781
|
+
"""Discard draft and revert to a published version (sync version)"""
|
|
2782
|
+
return self._run_async(
|
|
2783
|
+
self._async_client.discard_pipeline_draft(
|
|
2784
|
+
pipeline_id=pipeline_id, version=version,
|
|
2785
|
+
)
|
|
2786
|
+
)
|
|
2787
|
+
|
|
2788
|
+
def get_pipeline_rate(self, pipeline_id: str) -> Dict[str, Any]:
|
|
2789
|
+
"""Get pipeline per-page rate (sync version)"""
|
|
2790
|
+
return self._run_async(self._async_client.get_pipeline_rate(pipeline_id=pipeline_id))
|
|
2791
|
+
|
|
2792
|
+
def run_pipeline(
|
|
2793
|
+
self,
|
|
2794
|
+
pipeline_id: str,
|
|
2795
|
+
file_path: Optional[Union[str, Path]] = None,
|
|
2796
|
+
file_url: Optional[str] = None,
|
|
2797
|
+
page_range: Optional[str] = None,
|
|
2798
|
+
output_format: Optional[str] = None,
|
|
2799
|
+
run_evals: bool = False,
|
|
2800
|
+
skip_cache: bool = False,
|
|
2801
|
+
webhook_url: Optional[str] = None,
|
|
2802
|
+
version: Optional[int] = None,
|
|
2803
|
+
max_polls: int = 1,
|
|
2804
|
+
poll_interval: int = 1,
|
|
2805
|
+
) -> PipelineExecution:
|
|
2806
|
+
"""Execute a pipeline on a file (sync version)"""
|
|
2807
|
+
return self._run_async(
|
|
2808
|
+
self._async_client.run_pipeline(
|
|
2809
|
+
pipeline_id=pipeline_id, file_path=file_path, file_url=file_url,
|
|
2810
|
+
page_range=page_range, output_format=output_format,
|
|
2811
|
+
run_evals=run_evals, skip_cache=skip_cache,
|
|
2812
|
+
webhook_url=webhook_url, version=version,
|
|
2813
|
+
max_polls=max_polls, poll_interval=poll_interval,
|
|
2814
|
+
)
|
|
2815
|
+
)
|
|
2816
|
+
|
|
2817
|
+
def get_pipeline_execution(
|
|
2818
|
+
self, execution_id: str, max_polls: int = 1, poll_interval: int = 1,
|
|
2819
|
+
) -> PipelineExecution:
|
|
2820
|
+
"""Get pipeline execution status (sync version)"""
|
|
2821
|
+
return self._run_async(
|
|
2822
|
+
self._async_client.get_pipeline_execution(
|
|
2823
|
+
execution_id=execution_id, max_polls=max_polls, poll_interval=poll_interval,
|
|
2824
|
+
)
|
|
2825
|
+
)
|
|
2826
|
+
|
|
2827
|
+
def list_pipeline_executions(
|
|
2828
|
+
self, pipeline_id: str, limit: int = 20, offset: int = 0,
|
|
2829
|
+
) -> Dict[str, Any]:
|
|
2830
|
+
"""List pipeline executions (sync version)"""
|
|
2831
|
+
return self._run_async(
|
|
2832
|
+
self._async_client.list_pipeline_executions(
|
|
2833
|
+
pipeline_id=pipeline_id, limit=limit, offset=offset,
|
|
2834
|
+
)
|
|
2835
|
+
)
|
|
2836
|
+
|
|
2837
|
+
def get_step_result(self, execution_id: str, step_index: int) -> Dict[str, Any]:
|
|
2838
|
+
"""Get a pipeline execution step result (sync version)"""
|
|
2839
|
+
return self._run_async(
|
|
2840
|
+
self._async_client.get_step_result(
|
|
2841
|
+
execution_id=execution_id, step_index=step_index,
|
|
2842
|
+
)
|
|
2843
|
+
)
|
|
2844
|
+
|
|
2845
|
+
# --- Custom Processor Management methods (sync) ---
|
|
2846
|
+
|
|
2847
|
+
def list_custom_processors(self, limit: int = 50, offset: int = 0) -> Dict[str, Any]:
|
|
2848
|
+
"""List custom processors (sync version)"""
|
|
2849
|
+
return self._run_async(
|
|
2850
|
+
self._async_client.list_custom_processors(limit=limit, offset=offset)
|
|
2851
|
+
)
|
|
2852
|
+
|
|
2853
|
+
def get_custom_processor_status(self, lookup_key: str) -> Dict[str, Any]:
|
|
2854
|
+
"""Check custom processor request status (sync version)"""
|
|
2855
|
+
return self._run_async(
|
|
2856
|
+
self._async_client.get_custom_processor_status(lookup_key=lookup_key)
|
|
2857
|
+
)
|
|
2858
|
+
|
|
2859
|
+
def list_custom_processor_versions(self, processor_id: str) -> Dict[str, Any]:
|
|
2860
|
+
"""List custom processor versions (sync version)"""
|
|
2861
|
+
return self._run_async(
|
|
2862
|
+
self._async_client.list_custom_processor_versions(processor_id=processor_id)
|
|
2863
|
+
)
|
|
2864
|
+
|
|
2865
|
+
def set_active_processor_version(self, processor_id: str, version: int) -> Dict[str, Any]:
|
|
2866
|
+
"""Set active processor version (sync version)"""
|
|
2867
|
+
return self._run_async(
|
|
2868
|
+
self._async_client.set_active_processor_version(
|
|
2869
|
+
processor_id=processor_id, version=version,
|
|
2870
|
+
)
|
|
2871
|
+
)
|
|
2872
|
+
|
|
2873
|
+
def archive_custom_processor(self, processor_id: str) -> Dict[str, Any]:
|
|
2874
|
+
"""Archive a custom processor (sync version)"""
|
|
2875
|
+
return self._run_async(
|
|
2876
|
+
self._async_client.archive_custom_processor(processor_id=processor_id)
|
|
2877
|
+
)
|
datalab_sdk/models.py
CHANGED
|
@@ -53,6 +53,7 @@ class ConvertOptions(ProcessingOptions):
|
|
|
53
53
|
add_block_ids: bool = False # add block IDs to HTML output
|
|
54
54
|
include_markdown_in_chunks: bool = False # include markdown field in chunks/JSON output
|
|
55
55
|
token_efficient_markdown: bool = False # optimize markdown for LLM token usage
|
|
56
|
+
eval_rubric_id: Optional[int] = None # run evaluation rubric after conversion
|
|
56
57
|
|
|
57
58
|
def to_form_data(self) -> Dict[str, Any]:
|
|
58
59
|
"""Convert to form data format for API requests"""
|
|
@@ -78,13 +79,23 @@ class ConvertOptions(ProcessingOptions):
|
|
|
78
79
|
class ExtractOptions(ProcessingOptions):
|
|
79
80
|
"""Options for structured data extraction via /extract endpoint"""
|
|
80
81
|
|
|
81
|
-
page_schema: str = "" #
|
|
82
|
+
page_schema: str = "" # JSON schema with 'properties' key. Mutually exclusive with schema_id.
|
|
83
|
+
schema_id: Optional[str] = None # ID of a saved extraction schema (e.g. sch_k8Hx9mP2nQ4v). Mutually exclusive with page_schema.
|
|
84
|
+
schema_version: Optional[int] = None # Version of the schema. Only valid with schema_id.
|
|
82
85
|
checkpoint_id: Optional[str] = None # From previous /convert with save_checkpoint=true
|
|
83
86
|
mode: str = "fast" # fast, balanced, accurate
|
|
84
87
|
output_format: str = "markdown" # markdown, json, html, chunks
|
|
85
88
|
save_checkpoint: bool = False
|
|
86
89
|
webhook_url: Optional[str] = None
|
|
87
90
|
|
|
91
|
+
def to_form_data(self) -> Dict[str, Any]:
|
|
92
|
+
"""Convert to form data format for API requests"""
|
|
93
|
+
form_data = super().to_form_data()
|
|
94
|
+
# When using schema_id, suppress the empty default page_schema
|
|
95
|
+
if self.schema_id and not self.page_schema:
|
|
96
|
+
form_data.pop("page_schema", None)
|
|
97
|
+
return form_data
|
|
98
|
+
|
|
88
99
|
|
|
89
100
|
@dataclass
|
|
90
101
|
class SegmentOptions(ProcessingOptions):
|
|
@@ -98,15 +109,34 @@ class SegmentOptions(ProcessingOptions):
|
|
|
98
109
|
|
|
99
110
|
|
|
100
111
|
@dataclass
|
|
101
|
-
class
|
|
102
|
-
"""Options for running a custom
|
|
112
|
+
class CustomProcessorOptions(ProcessingOptions):
|
|
113
|
+
"""Options for running a custom processor via /custom-processor endpoint"""
|
|
103
114
|
|
|
104
|
-
pipeline_id: str = "" # Required - custom
|
|
105
|
-
|
|
115
|
+
pipeline_id: str = "" # Required - custom processor ID (cp_XXXXX format)
|
|
116
|
+
version: Optional[int] = None # Specify processor version to run (default: active version)
|
|
117
|
+
run_eval: bool = False # Run evaluation rules defined for the processor
|
|
106
118
|
mode: str = "fast" # fast, balanced, accurate
|
|
107
119
|
output_format: str = "markdown" # markdown, json, html, chunks
|
|
120
|
+
paginate: bool = False # Separate pages with horizontal rules containing page numbers
|
|
121
|
+
add_block_ids: bool = False # Add data-block-id attributes to HTML elements
|
|
122
|
+
include_markdown_in_chunks: bool = False # Include markdown field in chunks/JSON output
|
|
123
|
+
disable_image_extraction: bool = False # Disable image extraction from the document
|
|
124
|
+
disable_image_captions: bool = False # Disable synthetic image captions/descriptions
|
|
108
125
|
webhook_url: Optional[str] = None
|
|
109
126
|
|
|
127
|
+
@property
|
|
128
|
+
def processor_id(self) -> str:
|
|
129
|
+
"""Alias for pipeline_id (the API wire format uses pipeline_id)"""
|
|
130
|
+
return self.pipeline_id
|
|
131
|
+
|
|
132
|
+
@processor_id.setter
|
|
133
|
+
def processor_id(self, value: str):
|
|
134
|
+
self.pipeline_id = value
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Backward-compatible alias
|
|
138
|
+
CustomPipelineOptions = CustomProcessorOptions
|
|
139
|
+
|
|
110
140
|
|
|
111
141
|
@dataclass
|
|
112
142
|
class TrackChangesOptions(ProcessingOptions):
|
|
@@ -509,3 +539,137 @@ class FileResult:
|
|
|
509
539
|
status: str
|
|
510
540
|
output_path: Path
|
|
511
541
|
error: Optional[str] = None
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
# --- Extraction Schema models ---
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
@dataclass
|
|
548
|
+
class ExtractionSchema:
|
|
549
|
+
"""Represents a saved extraction schema for structured extraction"""
|
|
550
|
+
|
|
551
|
+
schema_id: str
|
|
552
|
+
name: str
|
|
553
|
+
schema_json: Dict[str, Any]
|
|
554
|
+
id: Optional[int] = None
|
|
555
|
+
description: Optional[str] = None
|
|
556
|
+
version: int = 1
|
|
557
|
+
version_history: Optional[List[Dict[str, Any]]] = None
|
|
558
|
+
archived: bool = False
|
|
559
|
+
created: Optional[str] = None
|
|
560
|
+
updated: Optional[str] = None
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
# --- Pipeline models ---
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
@dataclass
|
|
567
|
+
class PipelineProcessor:
|
|
568
|
+
"""Configuration for a single processor within a pipeline"""
|
|
569
|
+
|
|
570
|
+
type: str # convert, extract, segment, custom
|
|
571
|
+
settings: Dict[str, Any] = field(default_factory=dict)
|
|
572
|
+
custom_processor_id: Optional[str] = None # For custom steps (cp_XXXXX)
|
|
573
|
+
eval_rubric_id: Optional[int] = None
|
|
574
|
+
|
|
575
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
576
|
+
"""Convert to dictionary for API requests"""
|
|
577
|
+
data: Dict[str, Any] = {
|
|
578
|
+
"type": self.type,
|
|
579
|
+
"settings": self.settings,
|
|
580
|
+
}
|
|
581
|
+
if self.custom_processor_id is not None:
|
|
582
|
+
data["custom_processor_id"] = self.custom_processor_id
|
|
583
|
+
if self.eval_rubric_id is not None:
|
|
584
|
+
data["eval_rubric_id"] = self.eval_rubric_id
|
|
585
|
+
return data
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
@dataclass
|
|
589
|
+
class PipelineConfig:
|
|
590
|
+
"""Represents a pipeline definition"""
|
|
591
|
+
|
|
592
|
+
pipeline_id: str # pl_XXXXX
|
|
593
|
+
steps: List[Dict[str, Any]]
|
|
594
|
+
name: Optional[str] = None
|
|
595
|
+
is_saved: bool = False
|
|
596
|
+
archived: bool = False
|
|
597
|
+
active_version: int = 0
|
|
598
|
+
id: Optional[int] = None
|
|
599
|
+
created: Optional[str] = None
|
|
600
|
+
updated: Optional[str] = None
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
@dataclass
|
|
604
|
+
class PipelineVersion:
|
|
605
|
+
"""Immutable version snapshot of a pipeline"""
|
|
606
|
+
|
|
607
|
+
version: int
|
|
608
|
+
steps: List[Dict[str, Any]]
|
|
609
|
+
description: Optional[str] = None
|
|
610
|
+
id: Optional[int] = None
|
|
611
|
+
created: Optional[str] = None
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
@dataclass
|
|
615
|
+
class PipelineExecutionStepResult:
|
|
616
|
+
"""Status of a single step within a pipeline execution"""
|
|
617
|
+
|
|
618
|
+
step_index: int
|
|
619
|
+
step_type: str
|
|
620
|
+
status: str # pending, dispatched, running, completed, failed
|
|
621
|
+
lookup_key: Optional[str] = None
|
|
622
|
+
result_url: Optional[str] = None
|
|
623
|
+
started_at: Optional[str] = None
|
|
624
|
+
finished_at: Optional[str] = None
|
|
625
|
+
error_message: Optional[str] = None
|
|
626
|
+
checkpoint_id: Optional[str] = None
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
@dataclass
|
|
630
|
+
class PipelineExecution:
|
|
631
|
+
"""Result from pipeline execution"""
|
|
632
|
+
|
|
633
|
+
execution_id: str # pex_XXXXX
|
|
634
|
+
pipeline_id: str # pl_XXXXX
|
|
635
|
+
pipeline_version: int
|
|
636
|
+
status: str # pending, running, completed, completed_with_errors, failed
|
|
637
|
+
steps: List[PipelineExecutionStepResult] = field(default_factory=list)
|
|
638
|
+
started_at: Optional[str] = None
|
|
639
|
+
completed_at: Optional[str] = None
|
|
640
|
+
created: Optional[str] = None
|
|
641
|
+
config_snapshot: Optional[Dict[str, Any]] = None
|
|
642
|
+
input_config: Optional[Dict[str, Any]] = None
|
|
643
|
+
rate_breakdown: Optional[Dict[str, Any]] = None
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
# --- Custom Processor models ---
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@dataclass
|
|
650
|
+
class CustomProcessor:
|
|
651
|
+
"""Represents a custom processor (formerly custom pipeline)"""
|
|
652
|
+
|
|
653
|
+
processor_id: str # cp_XXXXX
|
|
654
|
+
status: str # processing, completed, failed
|
|
655
|
+
name: Optional[str] = None
|
|
656
|
+
success: Optional[bool] = None
|
|
657
|
+
active_version: int = 0
|
|
658
|
+
max_version: int = 0
|
|
659
|
+
iteration_in_progress: bool = False
|
|
660
|
+
pipeline_id: Optional[str] = None # auto-created workspace pipeline (pl_XXXXX)
|
|
661
|
+
created_at: Optional[str] = None
|
|
662
|
+
completed_at: Optional[str] = None
|
|
663
|
+
error_message: Optional[str] = None
|
|
664
|
+
eval_rubric_id: Optional[int] = None
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
@dataclass
|
|
668
|
+
class CustomProcessorVersion:
|
|
669
|
+
"""Version information for a custom processor"""
|
|
670
|
+
|
|
671
|
+
version: int
|
|
672
|
+
request_description: str = ""
|
|
673
|
+
created_at: Optional[str] = None
|
|
674
|
+
runtime: Optional[float] = None
|
|
675
|
+
is_active: bool = False
|
|
@@ -1,12 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|