nominal 1.96.0__tar.gz → 1.98.0__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.
- {nominal-1.96.0 → nominal-1.98.0}/CHANGELOG.md +19 -0
- {nominal-1.96.0 → nominal-1.98.0}/PKG-INFO +1 -1
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/__init__.py +4 -1
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/_utils/multipart.py +43 -19
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/client.py +52 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/dataset_file.py +113 -1
- {nominal-1.96.0 → nominal-1.98.0}/pyproject.toml +1 -1
- {nominal-1.96.0 → nominal-1.98.0}/.gitignore +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/LICENSE +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/README.md +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/__main__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/_utils/README.md +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/_utils/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/_utils/dataclass_tools.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/_utils/deprecation_tools.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/_utils/iterator_tools.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/_utils/streaming_tools.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/_utils/timing_tools.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/cli/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/cli/__main__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/cli/attachment.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/cli/auth.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/cli/config.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/cli/dataset.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/cli/download.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/cli/mis.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/cli/run.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/cli/util/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/cli/util/click_log_handler.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/cli/util/global_decorators.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/cli/util/verify_connection.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/config/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/config/_config.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/_clientsbunch.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/_constants.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/_stream/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/_stream/batch_processor.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/_stream/batch_processor_proto.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/_stream/write_stream.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/_stream/write_stream_base.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/_utils/README.md +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/_utils/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/_utils/api_tools.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/_utils/multipart_downloader.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/_utils/networking.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/_utils/pagination_tools.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/_utils/query_tools.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/_utils/queueing.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/asset.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/attachment.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/bounds.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/channel.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/checklist.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/connection.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/containerized_extractors.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/data_review.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/dataset.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/datasource.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/event.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/exceptions.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/filetype.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/log.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/run.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/secret.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/unit.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/user.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/video.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/video_file.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/workbook.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/workbook_template.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/core/workspace.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/exceptions/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/compute/README.md +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/compute/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/compute/_buckets.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/compute/dsl/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/compute/dsl/_enum_expr_impls.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/compute/dsl/_numeric_expr_impls.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/compute/dsl/_range_expr_impls.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/compute/dsl/exprs.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/compute/dsl/params.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/logging/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/logging/click_log_handler.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/logging/nominal_log_handler.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/logging/rich_log_handler.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/rust_streaming/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/rust_streaming/rust_write_stream.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/stream_v2/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/stream_v2/_serializer.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/stream_v2/_write_stream.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/video_processing/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/video_processing/resolution.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/experimental/video_processing/video_conversion.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/nominal.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/py.typed +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/thirdparty/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/thirdparty/matlab/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/thirdparty/matlab/_matlab.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/thirdparty/pandas/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/thirdparty/pandas/_pandas.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/thirdparty/polars/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/thirdparty/polars/polars_export_handler.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/thirdparty/tdms/__init__.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/thirdparty/tdms/_tdms.py +0 -0
- {nominal-1.96.0 → nominal-1.98.0}/nominal/ts/__init__.py +0 -0
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.98.0](https://github.com/nominal-io/nominal-client/compare/v1.97.0...v1.98.0) (2025-12-04)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* as_files_ingested iterator for dataset files ([#533](https://github.com/nominal-io/nominal-client/issues/533)) ([7afc0f6](https://github.com/nominal-io/nominal-client/commit/7afc0f61d24f930d37d55c8e0840ebd18b8711b6))
|
|
9
|
+
|
|
10
|
+
## [1.97.0](https://github.com/nominal-io/nominal-client/compare/v1.96.0...v1.97.0) (2025-12-04)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* add create_workbook_template ([#535](https://github.com/nominal-io/nominal-client/issues/535)) ([9c98975](https://github.com/nominal-io/nominal-client/commit/9c989753828c39e417bafed3db2981444132aeac))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* add per-part retries during multipart uploads ([#537](https://github.com/nominal-io/nominal-client/issues/537)) ([ffdbc4a](https://github.com/nominal-io/nominal-client/commit/ffdbc4ac6f82562fb9eff3fed4b254b784aa89bc))
|
|
21
|
+
|
|
3
22
|
## [1.96.0](https://github.com/nominal-io/nominal-client/compare/v1.95.0...v1.96.0) (2025-12-03)
|
|
4
23
|
|
|
5
24
|
|
|
@@ -17,7 +17,7 @@ from nominal.core.containerized_extractors import (
|
|
|
17
17
|
)
|
|
18
18
|
from nominal.core.data_review import CheckViolation, DataReview, DataReviewBuilder
|
|
19
19
|
from nominal.core.dataset import Dataset, poll_until_ingestion_completed
|
|
20
|
-
from nominal.core.dataset_file import DatasetFile
|
|
20
|
+
from nominal.core.dataset_file import DatasetFile, IngestWaitType, as_files_ingested, wait_for_files_to_ingest
|
|
21
21
|
from nominal.core.datasource import DataSource
|
|
22
22
|
from nominal.core.event import Event, EventType
|
|
23
23
|
from nominal.core.filetype import FileType, FileTypes
|
|
@@ -33,6 +33,7 @@ from nominal.core.workbook_template import WorkbookTemplate
|
|
|
33
33
|
from nominal.core.workspace import Workspace
|
|
34
34
|
|
|
35
35
|
__all__ = [
|
|
36
|
+
"as_files_ingested",
|
|
36
37
|
"Asset",
|
|
37
38
|
"Attachment",
|
|
38
39
|
"Bounds",
|
|
@@ -53,6 +54,7 @@ __all__ = [
|
|
|
53
54
|
"FileExtractionInput",
|
|
54
55
|
"FileType",
|
|
55
56
|
"FileTypes",
|
|
57
|
+
"IngestWaitType",
|
|
56
58
|
"LinkDict",
|
|
57
59
|
"LogPoint",
|
|
58
60
|
"NominalClient",
|
|
@@ -67,6 +69,7 @@ __all__ = [
|
|
|
67
69
|
"UserPassAuth",
|
|
68
70
|
"Video",
|
|
69
71
|
"VideoFile",
|
|
72
|
+
"wait_for_files_to_ingest",
|
|
70
73
|
"Workbook",
|
|
71
74
|
"WorkbookTemplate",
|
|
72
75
|
"WorkbookType",
|
|
@@ -27,29 +27,53 @@ def _sign_and_upload_part_job(
|
|
|
27
27
|
upload_id: str,
|
|
28
28
|
q: Queue[bytes],
|
|
29
29
|
part: int,
|
|
30
|
+
num_retries: int = 3,
|
|
30
31
|
) -> requests.Response:
|
|
31
32
|
data = q.get()
|
|
33
|
+
|
|
32
34
|
try:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
last_ex: Exception | None = None
|
|
36
|
+
for attempt in range(num_retries):
|
|
37
|
+
try:
|
|
38
|
+
log_extras = {"key": key, "part": part, "upload_id": upload_id, "attempt": attempt + 1}
|
|
39
|
+
|
|
40
|
+
logger.debug("Signing part %d for upload", part, extra=log_extras)
|
|
41
|
+
sign_response = upload_client.sign_part(auth_header, key, part, upload_id)
|
|
42
|
+
logger.debug(
|
|
43
|
+
"Successfully signed part %d for upload",
|
|
44
|
+
part,
|
|
45
|
+
extra={"response.url": sign_response.url, **log_extras},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
logger.debug("Pushing part %d for multipart upload", extra=log_extras)
|
|
49
|
+
put_response = requests.put(
|
|
50
|
+
sign_response.url,
|
|
51
|
+
data=data,
|
|
52
|
+
headers=sign_response.headers,
|
|
53
|
+
verify=upload_client._verify,
|
|
54
|
+
)
|
|
55
|
+
logger.debug(
|
|
56
|
+
"Finished pushing part %d for multipart upload with status %d",
|
|
57
|
+
part,
|
|
58
|
+
put_response.status_code,
|
|
59
|
+
extra={"response.url": put_response.url, **log_extras},
|
|
60
|
+
)
|
|
61
|
+
put_response.raise_for_status()
|
|
62
|
+
return put_response
|
|
63
|
+
except Exception as ex:
|
|
64
|
+
logger.warning(
|
|
65
|
+
"Failed to upload part %d: %s",
|
|
66
|
+
part,
|
|
67
|
+
ex,
|
|
68
|
+
extra=log_extras,
|
|
69
|
+
)
|
|
70
|
+
last_ex = ex
|
|
71
|
+
|
|
72
|
+
raise (
|
|
73
|
+
last_ex
|
|
74
|
+
if last_ex
|
|
75
|
+
else RuntimeError(f"Unknown error uploading part {part} for upload_id={upload_id} and key={key}")
|
|
47
76
|
)
|
|
48
|
-
put_response.raise_for_status()
|
|
49
|
-
return put_response
|
|
50
|
-
except Exception as e:
|
|
51
|
-
logger.exception("error uploading part", exc_info=e, extra={"key": key, "upload_id": upload_id, "part": part})
|
|
52
|
-
raise e
|
|
53
77
|
finally:
|
|
54
78
|
q.task_done()
|
|
55
79
|
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import enum
|
|
4
4
|
import logging
|
|
5
|
+
import uuid
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
from datetime import datetime, timedelta
|
|
7
8
|
from io import TextIOBase
|
|
@@ -21,10 +22,12 @@ from nominal_api import (
|
|
|
21
22
|
scout_catalog,
|
|
22
23
|
scout_checks_api,
|
|
23
24
|
scout_datasource_connection_api,
|
|
25
|
+
scout_layout_api,
|
|
24
26
|
scout_notebook_api,
|
|
25
27
|
scout_run_api,
|
|
26
28
|
scout_template_api,
|
|
27
29
|
scout_video_api,
|
|
30
|
+
scout_workbookcommon_api,
|
|
28
31
|
secrets_api,
|
|
29
32
|
storage_datasource_api,
|
|
30
33
|
)
|
|
@@ -1478,3 +1481,52 @@ class NominalClient:
|
|
|
1478
1481
|
"nominal.core.NominalClient.create_workbook_from_template",
|
|
1479
1482
|
"use 'nominal.core.WorkbookTemplate.create_workbook' instead",
|
|
1480
1483
|
)
|
|
1484
|
+
|
|
1485
|
+
def create_workbook_template(
|
|
1486
|
+
self,
|
|
1487
|
+
title: str,
|
|
1488
|
+
*,
|
|
1489
|
+
description: str | None = None,
|
|
1490
|
+
labels: list[str] | None = None,
|
|
1491
|
+
properties: dict[str, str] | None = None,
|
|
1492
|
+
commit_message: str | None = None,
|
|
1493
|
+
workspace: Workspace | str | None = None,
|
|
1494
|
+
) -> WorkbookTemplate:
|
|
1495
|
+
"""Create an empty workbook template.
|
|
1496
|
+
|
|
1497
|
+
Args:
|
|
1498
|
+
title: Title of the workbook template
|
|
1499
|
+
description: Description of the workbook template
|
|
1500
|
+
labels: Labels to attach to the workbook template
|
|
1501
|
+
properties: Properties to attach to the workbook template
|
|
1502
|
+
commit_message: An optional message to include with the creation of the template
|
|
1503
|
+
workspace: Workspace to create the workbook template in.
|
|
1504
|
+
|
|
1505
|
+
Returns:
|
|
1506
|
+
The created WorkbookTemplate
|
|
1507
|
+
"""
|
|
1508
|
+
request = scout_template_api.CreateTemplateRequest(
|
|
1509
|
+
title=title,
|
|
1510
|
+
description=description if description is not None else "",
|
|
1511
|
+
labels=labels if labels is not None else [],
|
|
1512
|
+
properties=properties if properties is not None else {},
|
|
1513
|
+
is_published=False,
|
|
1514
|
+
layout=scout_layout_api.WorkbookLayout(
|
|
1515
|
+
v1=scout_layout_api.WorkbookLayoutV1(
|
|
1516
|
+
root_panel=scout_layout_api.Panel(
|
|
1517
|
+
tabbed=scout_layout_api.TabbedPanel(
|
|
1518
|
+
v1=scout_layout_api.TabbedPanelV1(
|
|
1519
|
+
id=str(uuid.uuid4()),
|
|
1520
|
+
tabs=[],
|
|
1521
|
+
)
|
|
1522
|
+
)
|
|
1523
|
+
)
|
|
1524
|
+
)
|
|
1525
|
+
),
|
|
1526
|
+
content=scout_workbookcommon_api.WorkbookContent(channel_variables={}, charts={}),
|
|
1527
|
+
message=commit_message if commit_message is not None else "Initial blank workbook template",
|
|
1528
|
+
workspace=self._workspace_rid_for_search(workspace or WorkspaceSearchType.ALL),
|
|
1529
|
+
)
|
|
1530
|
+
|
|
1531
|
+
template = self._clients.template.create(self._clients.auth_header, request)
|
|
1532
|
+
return WorkbookTemplate._from_conjure(self._clients, template)
|
|
@@ -6,7 +6,7 @@ import pathlib
|
|
|
6
6
|
import time
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
8
|
from enum import Enum
|
|
9
|
-
from typing import Mapping, Protocol, Sequence
|
|
9
|
+
from typing import Iterable, Mapping, Protocol, Sequence
|
|
10
10
|
from urllib.parse import unquote, urlparse
|
|
11
11
|
|
|
12
12
|
from nominal_api import api, ingest_api, scout_catalog
|
|
@@ -287,3 +287,115 @@ class IngestStatus(Enum):
|
|
|
287
287
|
elif status.error is not None:
|
|
288
288
|
return cls.FAILED
|
|
289
289
|
raise ValueError(f"Unknown ingest status: {status.type}")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class IngestWaitType(Enum):
|
|
293
|
+
FIRST_COMPLETED = "FIRST_COMPLETED"
|
|
294
|
+
FIRST_EXCEPTION = "FIRST_EXCEPTION"
|
|
295
|
+
ALL_COMPLETED = "ALL_COMPLETED"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def wait_for_files_to_ingest(
|
|
299
|
+
files: Sequence[DatasetFile],
|
|
300
|
+
*,
|
|
301
|
+
poll_interval: datetime.timedelta = datetime.timedelta(seconds=1),
|
|
302
|
+
timeout: datetime.timedelta | None = None,
|
|
303
|
+
return_when: IngestWaitType = IngestWaitType.ALL_COMPLETED,
|
|
304
|
+
) -> tuple[Sequence[DatasetFile], Sequence[DatasetFile]]:
|
|
305
|
+
"""Blocks until all of the dataset files have completed their ingestion (or other specified conditions)
|
|
306
|
+
in a similar fashion to `concurrent.futures.wait`.
|
|
307
|
+
|
|
308
|
+
Any files that are already ingested (successfully or with errors) will be returned as "done", whereas any
|
|
309
|
+
files still ingesting by the time of this function's exit will be returned as "not done".
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
files: Dataset files to monitor for ingestion completion.
|
|
313
|
+
poll_interval: Interval to sleep between polling the remaining files under watch.
|
|
314
|
+
timeout: If given, the maximum time to wait before returning
|
|
315
|
+
return_when: Condition for this function to exit. By default, this function will block until all files
|
|
316
|
+
have completed their ingestion (successfully or unsuccessfully), but this can be changed to return
|
|
317
|
+
upon the first completed or first failing ingest. This behavior mirrors that of
|
|
318
|
+
`concurrent.futures.wait`.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Returns a tuple of (done, not done) dataset files.
|
|
322
|
+
"""
|
|
323
|
+
start_time = datetime.datetime.now()
|
|
324
|
+
done: list[DatasetFile] = []
|
|
325
|
+
not_done: list[DatasetFile] = [*files]
|
|
326
|
+
has_failed = False
|
|
327
|
+
|
|
328
|
+
while not_done and (timeout is None or datetime.datetime.now() - start_time < timeout):
|
|
329
|
+
logger.info("Polling for ingestion completion for %d files (%d total)", len(not_done), len(files))
|
|
330
|
+
|
|
331
|
+
next_not_done = []
|
|
332
|
+
for file in not_done:
|
|
333
|
+
latest_api = file._get_latest_api()
|
|
334
|
+
latest_file = file._refresh_from_api(latest_api)
|
|
335
|
+
match file.ingest_status:
|
|
336
|
+
case IngestStatus.SUCCESS:
|
|
337
|
+
done.append(latest_file)
|
|
338
|
+
case IngestStatus.FAILED:
|
|
339
|
+
logger.warning(
|
|
340
|
+
"Dataset file %s from dataset %s failed to ingest! Error message: %s",
|
|
341
|
+
latest_file.id,
|
|
342
|
+
latest_file.dataset_rid,
|
|
343
|
+
latest_api.ingest_status.error.message if latest_api.ingest_status.error else "",
|
|
344
|
+
)
|
|
345
|
+
done.append(latest_file)
|
|
346
|
+
has_failed = True
|
|
347
|
+
case IngestStatus.IN_PROGRESS:
|
|
348
|
+
next_not_done.append(latest_file)
|
|
349
|
+
|
|
350
|
+
not_done = next_not_done
|
|
351
|
+
|
|
352
|
+
if has_failed and return_when is IngestWaitType.FIRST_EXCEPTION:
|
|
353
|
+
break
|
|
354
|
+
elif done and return_when is IngestWaitType.FIRST_COMPLETED:
|
|
355
|
+
break
|
|
356
|
+
elif not not_done:
|
|
357
|
+
break
|
|
358
|
+
|
|
359
|
+
if timeout is not None and datetime.datetime.now() - start_time < timeout:
|
|
360
|
+
logger.info(
|
|
361
|
+
"Sleeping for %f seconds while awaiting ingestion for %d files (%d total)... ",
|
|
362
|
+
len(not_done),
|
|
363
|
+
len(files),
|
|
364
|
+
poll_interval.total_seconds(),
|
|
365
|
+
)
|
|
366
|
+
time.sleep(poll_interval.total_seconds())
|
|
367
|
+
|
|
368
|
+
return done, not_done
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def as_files_ingested(
|
|
372
|
+
files: Sequence[DatasetFile],
|
|
373
|
+
*,
|
|
374
|
+
poll_interval: datetime.timedelta = datetime.timedelta(seconds=1),
|
|
375
|
+
) -> Iterable[DatasetFile]:
|
|
376
|
+
"""Iterates over DatasetFiles as they complete their ingestion in a similar fashion to
|
|
377
|
+
`concurrent.futures.as_completed`.
|
|
378
|
+
|
|
379
|
+
Any files that are already ingested (successfully or with errors) will immediately be yielded.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
files: Dataset files to monitor for ingestion completion.
|
|
383
|
+
poll_interval: Interval to sleep between polling the remaining files under watch.
|
|
384
|
+
|
|
385
|
+
Yields:
|
|
386
|
+
Yields DatasetFiles as they are ingested. Due to the polling mechanics, the files are not yielded in
|
|
387
|
+
strictly sorted order based on their ingestion completion time. Ensure to check the `ingest_status` of
|
|
388
|
+
yielded dataset files if important.
|
|
389
|
+
"""
|
|
390
|
+
to_poll: Sequence[DatasetFile] = [*files]
|
|
391
|
+
while to_poll:
|
|
392
|
+
logger.info("Awaiting ingestion for %d files (%d total)", len(to_poll), len(files))
|
|
393
|
+
done, not_done = wait_for_files_to_ingest(
|
|
394
|
+
to_poll, poll_interval=poll_interval, return_when=IngestWaitType.FIRST_COMPLETED
|
|
395
|
+
)
|
|
396
|
+
for file in done:
|
|
397
|
+
yield file
|
|
398
|
+
|
|
399
|
+
to_poll = not_done
|
|
400
|
+
if to_poll:
|
|
401
|
+
time.sleep(poll_interval.total_seconds())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|