nominal 1.108.0__py3-none-any.whl → 1.110.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.
- CHANGELOG.md +20 -0
- nominal/core/_checklist_types.py +48 -0
- nominal/core/_utils/api_tools.py +16 -2
- nominal/core/_video_types.py +16 -0
- nominal/core/asset.py +34 -18
- nominal/core/checklist.py +11 -25
- nominal/core/client.py +25 -29
- nominal/core/data_review.py +32 -11
- nominal/core/dataset_file.py +6 -0
- nominal/core/run.py +25 -11
- nominal/core/streaming_checklist.py +25 -0
- nominal/core/video.py +71 -13
- nominal/core/video_file.py +62 -2
- nominal/experimental/migration/migration_utils.py +213 -9
- {nominal-1.108.0.dist-info → nominal-1.110.0.dist-info}/METADATA +2 -2
- {nominal-1.108.0.dist-info → nominal-1.110.0.dist-info}/RECORD +19 -16
- {nominal-1.108.0.dist-info → nominal-1.110.0.dist-info}/WHEEL +0 -0
- {nominal-1.108.0.dist-info → nominal-1.110.0.dist-info}/entry_points.txt +0 -0
- {nominal-1.108.0.dist-info → nominal-1.110.0.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.110.0](https://github.com/nominal-io/nominal-client/compare/v1.109.0...v1.110.0) (2026-01-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add data review/checklist methods to assets and runs ([#567](https://github.com/nominal-io/nominal-client/issues/567)) ([4b080d9](https://github.com/nominal-io/nominal-client/commit/4b080d96700fc51555b69a093d592cb03b92e073))
|
|
9
|
+
* add video clone capability in experimental and refactor video methods ([#576](https://github.com/nominal-io/nominal-client/issues/576)) ([f58a89c](https://github.com/nominal-io/nominal-client/commit/f58a89c1fe2220677f439fdf306f0c743e441548))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* properly handle dataset files in the new deletion states ([#590](https://github.com/nominal-io/nominal-client/issues/590)) ([e71bcc9](https://github.com/nominal-io/nominal-client/commit/e71bcc9f8fd752257dc2b33789380f0ffb7c5410))
|
|
15
|
+
|
|
16
|
+
## [1.109.0](https://github.com/nominal-io/nominal-client/compare/v1.108.0...v1.109.0) (2026-01-23)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* clone channels when cloning datasets ([#587](https://github.com/nominal-io/nominal-client/issues/587)) ([ab6de09](https://github.com/nominal-io/nominal-client/commit/ab6de0962c55e2c0a6933d6b9f94fb36f5e91273))
|
|
22
|
+
|
|
3
23
|
## [1.108.0](https://github.com/nominal-io/nominal-client/compare/v1.107.0...v1.108.0) (2026-01-22)
|
|
4
24
|
|
|
5
25
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
|
|
5
|
+
from nominal_api import scout_api
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Priority(IntEnum):
|
|
9
|
+
P0 = 0
|
|
10
|
+
P1 = 1
|
|
11
|
+
P2 = 2
|
|
12
|
+
P3 = 3
|
|
13
|
+
P4 = 4
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def _from_conjure(cls, priority: scout_api.Priority) -> Priority:
|
|
17
|
+
match priority.name:
|
|
18
|
+
case "P0":
|
|
19
|
+
return cls.P0
|
|
20
|
+
case "P1":
|
|
21
|
+
return cls.P1
|
|
22
|
+
case "P2":
|
|
23
|
+
return cls.P2
|
|
24
|
+
case "P3":
|
|
25
|
+
return cls.P3
|
|
26
|
+
case "P4":
|
|
27
|
+
return cls.P4
|
|
28
|
+
case _:
|
|
29
|
+
raise ValueError(f"unknown priority '{priority}', expected one of {list(cls)}")
|
|
30
|
+
|
|
31
|
+
def _to_conjure(self) -> scout_api.Priority:
|
|
32
|
+
match self:
|
|
33
|
+
case Priority.P0:
|
|
34
|
+
return scout_api.Priority.P0
|
|
35
|
+
case Priority.P1:
|
|
36
|
+
return scout_api.Priority.P1
|
|
37
|
+
case Priority.P2:
|
|
38
|
+
return scout_api.Priority.P2
|
|
39
|
+
case Priority.P3:
|
|
40
|
+
return scout_api.Priority.P3
|
|
41
|
+
case Priority.P4:
|
|
42
|
+
return scout_api.Priority.P4
|
|
43
|
+
case _:
|
|
44
|
+
raise ValueError(f"unknown priority '{self}', expected one of {list(Priority)}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _conjure_priority_to_priority(priority: scout_api.Priority) -> Priority:
|
|
48
|
+
return Priority._from_conjure(priority)
|
nominal/core/_utils/api_tools.py
CHANGED
|
@@ -5,13 +5,15 @@ import importlib.metadata
|
|
|
5
5
|
import logging
|
|
6
6
|
import platform
|
|
7
7
|
import sys
|
|
8
|
-
from typing import Any, Generic, Mapping, Protocol, Sequence, TypeAlias, TypedDict, TypeVar, runtime_checkable
|
|
8
|
+
from typing import Any, Generic, Literal, Mapping, Protocol, Sequence, TypeAlias, TypedDict, TypeVar, runtime_checkable
|
|
9
9
|
|
|
10
|
-
from nominal_api import scout_compute_api, scout_run_api
|
|
10
|
+
from nominal_api import scout_asset_api, scout_compute_api, scout_run_api
|
|
11
11
|
from typing_extensions import NotRequired, Self
|
|
12
12
|
|
|
13
13
|
from nominal._utils.dataclass_tools import update_dataclass
|
|
14
14
|
|
|
15
|
+
ScopeTypeSpecifier: TypeAlias = Literal["connection", "dataset", "video"]
|
|
16
|
+
|
|
15
17
|
logger = logging.getLogger(__name__)
|
|
16
18
|
|
|
17
19
|
T = TypeVar("T")
|
|
@@ -94,3 +96,15 @@ def create_api_tags(tags: Mapping[str, str] | None = None) -> dict[str, scout_co
|
|
|
94
96
|
return {}
|
|
95
97
|
|
|
96
98
|
return {key: scout_compute_api.StringConstant(literal=value) for key, value in tags.items()}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def filter_scopes(
|
|
102
|
+
scopes: Sequence[scout_asset_api.DataScope], scope_type: ScopeTypeSpecifier
|
|
103
|
+
) -> Sequence[scout_asset_api.DataScope]:
|
|
104
|
+
return [scope for scope in scopes if scope.data_source.type.lower() == scope_type]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def filter_scope_rids(scopes: Sequence[scout_asset_api.DataScope], scope_type: ScopeTypeSpecifier) -> Mapping[str, str]:
|
|
108
|
+
return {
|
|
109
|
+
scope.data_scope_name: getattr(scope.data_source, scope_type) for scope in filter_scopes(scopes, scope_type)
|
|
110
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from nominal.ts import IntegralNanosecondsUTC
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(init=True, repr=False, eq=False, order=False, unsafe_hash=False)
|
|
7
|
+
class McapVideoDetails:
|
|
8
|
+
mcap_channel_locator_topic: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(init=True, repr=False, eq=False, order=False, unsafe_hash=False)
|
|
12
|
+
class TimestampOptions:
|
|
13
|
+
starting_timestamp: IntegralNanosecondsUTC
|
|
14
|
+
ending_timestamp: IntegralNanosecondsUTC
|
|
15
|
+
scaling_factor: float
|
|
16
|
+
true_framerate: float
|
nominal/core/asset.py
CHANGED
|
@@ -4,7 +4,7 @@ import datetime
|
|
|
4
4
|
import logging
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from types import MappingProxyType
|
|
7
|
-
from typing import Iterable,
|
|
7
|
+
from typing import Iterable, Mapping, Protocol, Sequence, TypeAlias
|
|
8
8
|
|
|
9
9
|
from nominal_api import (
|
|
10
10
|
event,
|
|
@@ -15,6 +15,7 @@ from nominal_api import (
|
|
|
15
15
|
)
|
|
16
16
|
from typing_extensions import Self
|
|
17
17
|
|
|
18
|
+
from nominal.core import data_review, streaming_checklist
|
|
18
19
|
from nominal.core._clientsbunch import HasScoutParams
|
|
19
20
|
from nominal.core._event_types import EventType, SearchEventOriginType
|
|
20
21
|
from nominal.core._utils.api_tools import (
|
|
@@ -22,7 +23,10 @@ from nominal.core._utils.api_tools import (
|
|
|
22
23
|
Link,
|
|
23
24
|
LinkDict,
|
|
24
25
|
RefreshableMixin,
|
|
26
|
+
ScopeTypeSpecifier,
|
|
25
27
|
create_links,
|
|
28
|
+
filter_scope_rids,
|
|
29
|
+
filter_scopes,
|
|
26
30
|
rid_from_instance_or_string,
|
|
27
31
|
)
|
|
28
32
|
from nominal.core._utils.pagination_tools import search_runs_by_asset_paginated
|
|
@@ -35,25 +39,10 @@ from nominal.core.video import Video, _create_video, _get_video
|
|
|
35
39
|
from nominal.ts import IntegralNanosecondsDuration, IntegralNanosecondsUTC, _SecondsNanos
|
|
36
40
|
|
|
37
41
|
ScopeType: TypeAlias = Connection | Dataset | Video
|
|
38
|
-
ScopeTypeSpecifier: TypeAlias = Literal["connection", "dataset", "video"]
|
|
39
42
|
|
|
40
43
|
logger = logging.getLogger(__name__)
|
|
41
44
|
|
|
42
45
|
|
|
43
|
-
def _filter_scopes(
|
|
44
|
-
scopes: Sequence[scout_asset_api.DataScope], scope_type: ScopeTypeSpecifier
|
|
45
|
-
) -> Sequence[scout_asset_api.DataScope]:
|
|
46
|
-
return [scope for scope in scopes if scope.data_source.type.lower() == scope_type]
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _filter_scope_rids(
|
|
50
|
-
scopes: Sequence[scout_asset_api.DataScope], scope_type: ScopeTypeSpecifier
|
|
51
|
-
) -> Mapping[str, str]:
|
|
52
|
-
return {
|
|
53
|
-
scope.data_scope_name: getattr(scope.data_source, scope_type) for scope in _filter_scopes(scopes, scope_type)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
46
|
@dataclass(frozen=True)
|
|
58
47
|
class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
59
48
|
rid: str
|
|
@@ -70,6 +59,7 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
70
59
|
Video._Clients,
|
|
71
60
|
Attachment._Clients,
|
|
72
61
|
Event._Clients,
|
|
62
|
+
data_review.DataReview._Clients,
|
|
73
63
|
HasScoutParams,
|
|
74
64
|
Protocol,
|
|
75
65
|
):
|
|
@@ -94,11 +84,11 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
94
84
|
return response[self.rid]
|
|
95
85
|
|
|
96
86
|
def _list_dataset_scopes(self) -> Sequence[scout_asset_api.DataScope]:
|
|
97
|
-
return
|
|
87
|
+
return filter_scopes(self._get_latest_api().data_scopes, "dataset")
|
|
98
88
|
|
|
99
89
|
def _scope_rids(self, scope_type: ScopeTypeSpecifier) -> Mapping[str, str]:
|
|
100
90
|
asset = self._get_latest_api()
|
|
101
|
-
return
|
|
91
|
+
return filter_scope_rids(asset.data_scopes, scope_type)
|
|
102
92
|
|
|
103
93
|
def update(
|
|
104
94
|
self,
|
|
@@ -540,6 +530,32 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
540
530
|
origin_types=origin_types,
|
|
541
531
|
)
|
|
542
532
|
|
|
533
|
+
def search_data_reviews(
|
|
534
|
+
self,
|
|
535
|
+
runs: Sequence[Run | str] | None = None,
|
|
536
|
+
) -> Sequence[data_review.DataReview]:
|
|
537
|
+
"""Search for data reviews associated with this Asset. See nominal.core.client.search_data_reviews
|
|
538
|
+
for details.
|
|
539
|
+
"""
|
|
540
|
+
return list(
|
|
541
|
+
data_review._iter_search_data_reviews(
|
|
542
|
+
self._clients,
|
|
543
|
+
assets=[self.rid],
|
|
544
|
+
runs=[rid_from_instance_or_string(run) for run in (runs or [])],
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
def list_streaming_checklists(self) -> Sequence[str]:
|
|
549
|
+
"""List all Streaming Checklists associated with this Asset. See
|
|
550
|
+
nominal.core.client.list_streaming_checklists for details.
|
|
551
|
+
"""
|
|
552
|
+
return list(
|
|
553
|
+
streaming_checklist._iter_list_streaming_checklists(
|
|
554
|
+
self._clients,
|
|
555
|
+
asset_rid=self.rid,
|
|
556
|
+
)
|
|
557
|
+
)
|
|
558
|
+
|
|
543
559
|
def remove_attachments(self, attachments: Iterable[Attachment] | Iterable[str]) -> None:
|
|
544
560
|
"""Remove attachments from this asset.
|
|
545
561
|
Does not remove the attachments from Nominal.
|
nominal/core/checklist.py
CHANGED
|
@@ -2,10 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from datetime import timedelta
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Mapping, Protocol, Sequence
|
|
6
6
|
|
|
7
7
|
from nominal_api import (
|
|
8
|
-
|
|
8
|
+
event,
|
|
9
|
+
scout,
|
|
9
10
|
scout_checklistexecution_api,
|
|
10
11
|
scout_checks_api,
|
|
11
12
|
scout_datareview_api,
|
|
@@ -13,11 +14,11 @@ from nominal_api import (
|
|
|
13
14
|
)
|
|
14
15
|
from typing_extensions import Self
|
|
15
16
|
|
|
17
|
+
from nominal.core import run as core_run
|
|
16
18
|
from nominal.core._clientsbunch import HasScoutParams
|
|
17
19
|
from nominal.core._utils.api_tools import HasRid, rid_from_instance_or_string
|
|
18
20
|
from nominal.core.asset import Asset
|
|
19
21
|
from nominal.core.data_review import DataReview
|
|
20
|
-
from nominal.core.run import Run
|
|
21
22
|
from nominal.ts import _to_api_duration
|
|
22
23
|
|
|
23
24
|
|
|
@@ -30,13 +31,17 @@ class Checklist(HasRid):
|
|
|
30
31
|
labels: Sequence[str]
|
|
31
32
|
_clients: _Clients = field(repr=False)
|
|
32
33
|
|
|
33
|
-
class _Clients(
|
|
34
|
+
class _Clients(HasScoutParams, Protocol):
|
|
34
35
|
@property
|
|
35
36
|
def checklist(self) -> scout_checks_api.ChecklistService: ...
|
|
36
37
|
@property
|
|
37
38
|
def checklist_execution(self) -> scout_checklistexecution_api.ChecklistExecutionService: ...
|
|
38
39
|
@property
|
|
39
40
|
def datareview(self) -> scout_datareview_api.DataReviewService: ...
|
|
41
|
+
@property
|
|
42
|
+
def event(self) -> event.EventService: ...
|
|
43
|
+
@property
|
|
44
|
+
def run(self) -> scout.RunService: ...
|
|
40
45
|
|
|
41
46
|
@classmethod
|
|
42
47
|
def _from_conjure(cls, clients: _Clients, checklist: scout_checks_api.VersionedChecklist) -> Self:
|
|
@@ -53,7 +58,7 @@ class Checklist(HasRid):
|
|
|
53
58
|
_clients=clients,
|
|
54
59
|
)
|
|
55
60
|
|
|
56
|
-
def execute(self, run: Run | str, commit: str | None = None) -> DataReview:
|
|
61
|
+
def execute(self, run: core_run.Run | str, commit: str | None = None) -> DataReview:
|
|
57
62
|
"""Execute a checklist against a run.
|
|
58
63
|
|
|
59
64
|
Args:
|
|
@@ -152,26 +157,7 @@ class Checklist(HasRid):
|
|
|
152
157
|
"""Returns a link to the page for this checklist in the Nominal app"""
|
|
153
158
|
return f"{self._clients.app_base_url}/checklists/{self.rid}"
|
|
154
159
|
|
|
155
|
-
def preview_for_run_url(self, run: Run | str) -> str:
|
|
160
|
+
def preview_for_run_url(self, run: core_run.Run | str) -> str:
|
|
156
161
|
"""Returns a link to the page for previewing this checklist on a given run in the Nominal app"""
|
|
157
162
|
run_rid = rid_from_instance_or_string(run)
|
|
158
163
|
return f"{self.nominal_url}?previewRunRid={run_rid}"
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
Priority = Literal[0, 1, 2, 3, 4]
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
_priority_to_conjure_map: dict[Priority, scout_api.Priority] = {
|
|
165
|
-
0: scout_api.Priority.P0,
|
|
166
|
-
1: scout_api.Priority.P1,
|
|
167
|
-
2: scout_api.Priority.P2,
|
|
168
|
-
3: scout_api.Priority.P3,
|
|
169
|
-
4: scout_api.Priority.P4,
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def _conjure_priority_to_priority(priority: scout_api.Priority) -> Priority:
|
|
174
|
-
inverted_map = {v: k for k, v in _priority_to_conjure_map.items()}
|
|
175
|
-
if priority in inverted_map:
|
|
176
|
-
return inverted_map[priority]
|
|
177
|
-
raise ValueError(f"unknown priority '{priority}', expected one of {_priority_to_conjure_map.values()}")
|
nominal/core/client.py
CHANGED
|
@@ -48,11 +48,8 @@ from nominal.core._utils.multipart import (
|
|
|
48
48
|
upload_multipart_io,
|
|
49
49
|
)
|
|
50
50
|
from nominal.core._utils.pagination_tools import (
|
|
51
|
-
list_streaming_checklists_for_asset_paginated,
|
|
52
|
-
list_streaming_checklists_paginated,
|
|
53
51
|
search_assets_paginated,
|
|
54
52
|
search_checklists_paginated,
|
|
55
|
-
search_data_reviews_paginated,
|
|
56
53
|
search_datasets_paginated,
|
|
57
54
|
search_runs_by_asset_paginated,
|
|
58
55
|
search_runs_paginated,
|
|
@@ -84,7 +81,7 @@ from nominal.core.containerized_extractors import (
|
|
|
84
81
|
FileExtractionInput,
|
|
85
82
|
FileOutputFormat,
|
|
86
83
|
)
|
|
87
|
-
from nominal.core.data_review import DataReview, DataReviewBuilder
|
|
84
|
+
from nominal.core.data_review import DataReview, DataReviewBuilder, _iter_search_data_reviews
|
|
88
85
|
from nominal.core.dataset import (
|
|
89
86
|
Dataset,
|
|
90
87
|
_create_dataset,
|
|
@@ -97,6 +94,7 @@ from nominal.core.exceptions import NominalConfigError, NominalError, NominalMet
|
|
|
97
94
|
from nominal.core.filetype import FileType, FileTypes
|
|
98
95
|
from nominal.core.run import Run, _create_run
|
|
99
96
|
from nominal.core.secret import Secret
|
|
97
|
+
from nominal.core.streaming_checklist import _iter_list_streaming_checklists
|
|
100
98
|
from nominal.core.unit import Unit, _available_units
|
|
101
99
|
from nominal.core.user import User
|
|
102
100
|
from nominal.core.video import Video, _create_video
|
|
@@ -1162,21 +1160,17 @@ class NominalClient:
|
|
|
1162
1160
|
)
|
|
1163
1161
|
return list(self._iter_search_assets(query))
|
|
1164
1162
|
|
|
1165
|
-
def
|
|
1166
|
-
if asset is None:
|
|
1167
|
-
return list_streaming_checklists_paginated(self._clients.checklist_execution, self._clients.auth_header)
|
|
1168
|
-
return list_streaming_checklists_for_asset_paginated(
|
|
1169
|
-
self._clients.checklist_execution, self._clients.auth_header, asset
|
|
1170
|
-
)
|
|
1171
|
-
|
|
1172
|
-
def list_streaming_checklists(self, asset: Asset | str | None = None) -> Iterable[str]:
|
|
1163
|
+
def list_streaming_checklists(self, asset: Asset | str | None = None) -> Sequence[str]:
|
|
1173
1164
|
"""List all Streaming Checklists.
|
|
1174
1165
|
|
|
1175
1166
|
Args:
|
|
1176
1167
|
asset: if provided, only return checklists associated with the given asset.
|
|
1168
|
+
|
|
1169
|
+
Returns:
|
|
1170
|
+
All streaming checklist RIDs that match the provided conditions
|
|
1177
1171
|
"""
|
|
1178
|
-
|
|
1179
|
-
return list(self.
|
|
1172
|
+
asset_rid = None if asset is None else rid_from_instance_or_string(asset)
|
|
1173
|
+
return list(_iter_list_streaming_checklists(self._clients, asset_rid))
|
|
1180
1174
|
|
|
1181
1175
|
def data_review_builder(self) -> DataReviewBuilder:
|
|
1182
1176
|
return DataReviewBuilder([], [], [], _clients=self._clients)
|
|
@@ -1220,27 +1214,29 @@ class NominalClient:
|
|
|
1220
1214
|
responses = self._clients.event.batch_get_events(self._clients.auth_header, list(rids))
|
|
1221
1215
|
return [Event._from_conjure(self._clients, response) for response in responses]
|
|
1222
1216
|
|
|
1223
|
-
def _iter_search_data_reviews(
|
|
1224
|
-
self,
|
|
1225
|
-
assets: Sequence[Asset | str] | None = None,
|
|
1226
|
-
runs: Sequence[Run | str] | None = None,
|
|
1227
|
-
) -> Iterable[DataReview]:
|
|
1228
|
-
for review in search_data_reviews_paginated(
|
|
1229
|
-
self._clients.datareview,
|
|
1230
|
-
self._clients.auth_header,
|
|
1231
|
-
assets=[rid_from_instance_or_string(asset) for asset in assets] if assets else None,
|
|
1232
|
-
runs=[rid_from_instance_or_string(run) for run in runs] if runs else None,
|
|
1233
|
-
):
|
|
1234
|
-
yield DataReview._from_conjure(self._clients, review)
|
|
1235
|
-
|
|
1236
1217
|
def search_data_reviews(
|
|
1237
1218
|
self,
|
|
1238
1219
|
assets: Sequence[Asset | str] | None = None,
|
|
1239
1220
|
runs: Sequence[Run | str] | None = None,
|
|
1240
1221
|
) -> Sequence[DataReview]:
|
|
1241
|
-
"""Search for
|
|
1222
|
+
"""Search for data reviews meeting the specified filters.
|
|
1223
|
+
Filters are ANDed together, e.g. `(data_review.asset == asset) AND (data_review.run == run)`
|
|
1224
|
+
|
|
1225
|
+
Args:
|
|
1226
|
+
assets: List of assets that must be associated with a data review to be included.
|
|
1227
|
+
runs: List of runs that must be associated with a data review to be included.
|
|
1228
|
+
|
|
1229
|
+
Returns:
|
|
1230
|
+
All data reviews which match all of the provided conditions
|
|
1231
|
+
"""
|
|
1242
1232
|
# TODO (drake-nominal): Expose checklist_refs to users
|
|
1243
|
-
return list(
|
|
1233
|
+
return list(
|
|
1234
|
+
_iter_search_data_reviews(
|
|
1235
|
+
clients=self._clients,
|
|
1236
|
+
assets=[rid_from_instance_or_string(asset) for asset in assets] if assets else None,
|
|
1237
|
+
runs=[rid_from_instance_or_string(run) for run in runs] if runs else None,
|
|
1238
|
+
)
|
|
1239
|
+
)
|
|
1244
1240
|
|
|
1245
1241
|
def search_events(
|
|
1246
1242
|
self,
|
nominal/core/data_review.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from datetime import timedelta
|
|
5
5
|
from time import sleep
|
|
6
|
-
from typing import Protocol, Sequence
|
|
6
|
+
from typing import TYPE_CHECKING, Iterable, Protocol, Sequence
|
|
7
7
|
|
|
8
8
|
from nominal_api import (
|
|
9
9
|
event as event_api,
|
|
@@ -18,14 +18,19 @@ from nominal_api import (
|
|
|
18
18
|
)
|
|
19
19
|
from typing_extensions import Self, deprecated
|
|
20
20
|
|
|
21
|
-
from nominal.core import
|
|
21
|
+
from nominal.core._checklist_types import Priority, _conjure_priority_to_priority
|
|
22
22
|
from nominal.core._clientsbunch import HasScoutParams
|
|
23
23
|
from nominal.core._utils.api_tools import HasRid, rid_from_instance_or_string
|
|
24
|
-
from nominal.core.
|
|
24
|
+
from nominal.core._utils.pagination_tools import search_data_reviews_paginated
|
|
25
|
+
from nominal.core.event import Event
|
|
25
26
|
from nominal.core.exceptions import NominalMethodRemovedError
|
|
26
|
-
from nominal.core.run import Run
|
|
27
27
|
from nominal.ts import IntegralNanosecondsUTC, _SecondsNanos
|
|
28
28
|
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from nominal.core.asset import Asset
|
|
31
|
+
from nominal.core.checklist import Checklist
|
|
32
|
+
from nominal.core.run import Run
|
|
33
|
+
|
|
29
34
|
|
|
30
35
|
@dataclass(frozen=True)
|
|
31
36
|
class DataReview(HasRid):
|
|
@@ -66,8 +71,10 @@ class DataReview(HasRid):
|
|
|
66
71
|
_clients=clients,
|
|
67
72
|
)
|
|
68
73
|
|
|
69
|
-
def get_checklist(self) ->
|
|
70
|
-
|
|
74
|
+
def get_checklist(self) -> "Checklist":
|
|
75
|
+
from nominal.core.checklist import Checklist
|
|
76
|
+
|
|
77
|
+
return Checklist._from_conjure(
|
|
71
78
|
self._clients,
|
|
72
79
|
self._clients.checklist.get(self._clients.auth_header, self.checklist_rid, commit=self.checklist_commit),
|
|
73
80
|
)
|
|
@@ -83,7 +90,7 @@ class DataReview(HasRid):
|
|
|
83
90
|
"use 'nominal.core.DataReview.get_events()' instead",
|
|
84
91
|
)
|
|
85
92
|
|
|
86
|
-
def get_events(self) -> Sequence[
|
|
93
|
+
def get_events(self) -> Sequence[Event]:
|
|
87
94
|
"""Retrieves the list of events for the data review."""
|
|
88
95
|
data_review_response = self._clients.datareview.get(self._clients.auth_header, self.rid).check_evaluations
|
|
89
96
|
all_event_rids = [
|
|
@@ -93,7 +100,7 @@ class DataReview(HasRid):
|
|
|
93
100
|
for event_rid in check.state._generated_alerts.event_rids
|
|
94
101
|
]
|
|
95
102
|
event_response = self._clients.event.batch_get_events(self._clients.auth_header, all_event_rids)
|
|
96
|
-
return [
|
|
103
|
+
return [Event._from_conjure(self._clients, data_review_event) for data_review_event in event_response]
|
|
97
104
|
|
|
98
105
|
def reload(self) -> DataReview:
|
|
99
106
|
"""Reloads the data review from the server."""
|
|
@@ -137,7 +144,7 @@ class CheckViolation:
|
|
|
137
144
|
name: str
|
|
138
145
|
start: IntegralNanosecondsUTC
|
|
139
146
|
end: IntegralNanosecondsUTC | None
|
|
140
|
-
priority:
|
|
147
|
+
priority: Priority | None
|
|
141
148
|
|
|
142
149
|
@classmethod
|
|
143
150
|
def _from_conjure(cls, check_alert: scout_datareview_api.CheckAlert) -> CheckViolation:
|
|
@@ -147,7 +154,7 @@ class CheckViolation:
|
|
|
147
154
|
name=check_alert.name,
|
|
148
155
|
start=_SecondsNanos.from_api(check_alert.start).to_nanoseconds(),
|
|
149
156
|
end=_SecondsNanos.from_api(check_alert.end).to_nanoseconds() if check_alert.end is not None else None,
|
|
150
|
-
priority=
|
|
157
|
+
priority=_conjure_priority_to_priority(check_alert.priority)
|
|
151
158
|
if check_alert.priority is not scout_api.Priority.UNKNOWN
|
|
152
159
|
else None,
|
|
153
160
|
)
|
|
@@ -192,7 +199,7 @@ class DataReviewBuilder:
|
|
|
192
199
|
def execute_checklist(
|
|
193
200
|
self,
|
|
194
201
|
run: str | Run,
|
|
195
|
-
checklist: str |
|
|
202
|
+
checklist: str | Checklist,
|
|
196
203
|
*,
|
|
197
204
|
commit: str | None = None,
|
|
198
205
|
asset: str | Asset | None = None,
|
|
@@ -273,3 +280,17 @@ def poll_until_completed(
|
|
|
273
280
|
data_reviews: Sequence[DataReview], interval: timedelta = timedelta(seconds=2)
|
|
274
281
|
) -> Sequence[DataReview]:
|
|
275
282
|
return [review.poll_for_completion(interval) for review in data_reviews]
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _iter_search_data_reviews(
|
|
286
|
+
clients: DataReview._Clients,
|
|
287
|
+
assets: Sequence[str] | None = None,
|
|
288
|
+
runs: Sequence[str] | None = None,
|
|
289
|
+
) -> Iterable[DataReview]:
|
|
290
|
+
for review in search_data_reviews_paginated(
|
|
291
|
+
clients.datareview,
|
|
292
|
+
clients.auth_header,
|
|
293
|
+
assets=assets,
|
|
294
|
+
runs=runs,
|
|
295
|
+
):
|
|
296
|
+
yield DataReview._from_conjure(clients, review)
|
nominal/core/dataset_file.py
CHANGED
|
@@ -280,6 +280,8 @@ class IngestStatus(Enum):
|
|
|
280
280
|
SUCCESS = "SUCCESS"
|
|
281
281
|
IN_PROGRESS = "IN_PROGRESS"
|
|
282
282
|
FAILED = "FAILED"
|
|
283
|
+
DELETION_IN_PROGRESS = "DELETION_IN_PROGRESS"
|
|
284
|
+
DELETED = "DELETED"
|
|
283
285
|
|
|
284
286
|
@classmethod
|
|
285
287
|
def _from_conjure(cls, status: api.IngestStatusV2) -> IngestStatus:
|
|
@@ -289,6 +291,10 @@ class IngestStatus(Enum):
|
|
|
289
291
|
return cls.IN_PROGRESS
|
|
290
292
|
elif status.error is not None:
|
|
291
293
|
return cls.FAILED
|
|
294
|
+
elif status.deletion_in_progress is not None:
|
|
295
|
+
return cls.DELETION_IN_PROGRESS
|
|
296
|
+
elif status.deleted is not None:
|
|
297
|
+
return cls.DELETED
|
|
292
298
|
raise ValueError(f"Unknown ingest status: {status.type}")
|
|
293
299
|
|
|
294
300
|
|
nominal/core/run.py
CHANGED
|
@@ -3,16 +3,17 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from datetime import datetime, timedelta
|
|
5
5
|
from types import MappingProxyType
|
|
6
|
-
from typing import Iterable, Mapping, Protocol, Sequence, cast
|
|
6
|
+
from typing import TYPE_CHECKING, Iterable, Mapping, Protocol, Sequence, cast
|
|
7
7
|
|
|
8
8
|
from nominal_api import (
|
|
9
|
+
event,
|
|
10
|
+
scout,
|
|
9
11
|
scout_asset_api,
|
|
12
|
+
scout_assets,
|
|
10
13
|
scout_run_api,
|
|
11
14
|
)
|
|
12
15
|
from typing_extensions import Self
|
|
13
16
|
|
|
14
|
-
from nominal.core import asset as core_asset
|
|
15
|
-
from nominal.core._clientsbunch import HasScoutParams
|
|
16
17
|
from nominal.core._event_types import EventType
|
|
17
18
|
from nominal.core._utils.api_tools import (
|
|
18
19
|
HasRid,
|
|
@@ -20,16 +21,20 @@ from nominal.core._utils.api_tools import (
|
|
|
20
21
|
LinkDict,
|
|
21
22
|
RefreshableMixin,
|
|
22
23
|
create_links,
|
|
24
|
+
filter_scopes,
|
|
23
25
|
rid_from_instance_or_string,
|
|
24
26
|
)
|
|
25
|
-
from nominal.core.asset import _filter_scopes
|
|
26
27
|
from nominal.core.attachment import Attachment, _iter_get_attachments
|
|
27
28
|
from nominal.core.connection import Connection, _get_connections
|
|
28
29
|
from nominal.core.dataset import Dataset, _DatasetWrapper, _get_datasets
|
|
30
|
+
from nominal.core.datasource import DataSource
|
|
29
31
|
from nominal.core.event import Event, _create_event
|
|
30
32
|
from nominal.core.video import Video, _get_video
|
|
31
33
|
from nominal.ts import IntegralNanosecondsDuration, IntegralNanosecondsUTC, _SecondsNanos, _to_api_duration
|
|
32
34
|
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from nominal.core.asset import Asset
|
|
37
|
+
|
|
33
38
|
|
|
34
39
|
@dataclass(frozen=True)
|
|
35
40
|
class Run(HasRid, RefreshableMixin[scout_run_api.Run], _DatasetWrapper):
|
|
@@ -48,11 +53,17 @@ class Run(HasRid, RefreshableMixin[scout_run_api.Run], _DatasetWrapper):
|
|
|
48
53
|
_clients: _Clients = field(repr=False)
|
|
49
54
|
|
|
50
55
|
class _Clients(
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
Attachment._Clients,
|
|
57
|
+
DataSource._Clients,
|
|
58
|
+
Video._Clients,
|
|
53
59
|
Protocol,
|
|
54
60
|
):
|
|
55
|
-
|
|
61
|
+
@property
|
|
62
|
+
def assets(self) -> scout_assets.AssetService: ...
|
|
63
|
+
@property
|
|
64
|
+
def event(self) -> event.EventService: ...
|
|
65
|
+
@property
|
|
66
|
+
def run(self) -> scout.RunService: ...
|
|
56
67
|
|
|
57
68
|
@property
|
|
58
69
|
def nominal_url(self) -> str:
|
|
@@ -105,7 +116,7 @@ class Run(HasRid, RefreshableMixin[scout_run_api.Run], _DatasetWrapper):
|
|
|
105
116
|
if len(api_run.assets) > 1:
|
|
106
117
|
raise RuntimeError("Can't retrieve dataset scopes on multi-asset runs")
|
|
107
118
|
|
|
108
|
-
return
|
|
119
|
+
return filter_scopes(api_run.asset_data_scopes, "dataset")
|
|
109
120
|
|
|
110
121
|
def _list_datasource_rids(
|
|
111
122
|
self, datasource_type: str | None = None, property_name: str | None = None
|
|
@@ -350,13 +361,16 @@ class Run(HasRid, RefreshableMixin[scout_run_api.Run], _DatasetWrapper):
|
|
|
350
361
|
"""List a sequence of Attachments associated with this Run."""
|
|
351
362
|
return list(self._iter_list_attachments())
|
|
352
363
|
|
|
353
|
-
def _iter_list_assets(self) -> Iterable[
|
|
364
|
+
def _iter_list_assets(self) -> Iterable["Asset"]:
|
|
365
|
+
from nominal.core.asset import Asset
|
|
366
|
+
|
|
367
|
+
clients = cast(Asset._Clients, self._clients)
|
|
354
368
|
run = self._get_latest_api()
|
|
355
369
|
assets = self._clients.assets.get_assets(self._clients.auth_header, run.assets)
|
|
356
370
|
for a in assets.values():
|
|
357
|
-
yield
|
|
371
|
+
yield Asset._from_conjure(clients, a)
|
|
358
372
|
|
|
359
|
-
def list_assets(self) -> Sequence[
|
|
373
|
+
def list_assets(self) -> Sequence["Asset"]:
|
|
360
374
|
"""List assets associated with this run."""
|
|
361
375
|
return list(self._iter_list_assets())
|
|
362
376
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Iterable, Protocol
|
|
4
|
+
|
|
5
|
+
from nominal_api import scout_checklistexecution_api
|
|
6
|
+
|
|
7
|
+
from nominal.core._clientsbunch import HasScoutParams
|
|
8
|
+
from nominal.core._utils.pagination_tools import (
|
|
9
|
+
list_streaming_checklists_for_asset_paginated,
|
|
10
|
+
list_streaming_checklists_paginated,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _Clients(HasScoutParams, Protocol):
|
|
15
|
+
@property
|
|
16
|
+
def checklist_execution(self) -> scout_checklistexecution_api.ChecklistExecutionService: ...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _iter_list_streaming_checklists(
|
|
20
|
+
clients: _Clients,
|
|
21
|
+
asset_rid: str | None = None,
|
|
22
|
+
) -> Iterable[str]:
|
|
23
|
+
if asset_rid is None:
|
|
24
|
+
return list_streaming_checklists_paginated(clients.checklist_execution, clients.auth_header)
|
|
25
|
+
return list_streaming_checklists_for_asset_paginated(clients.checklist_execution, clients.auth_header, asset_rid)
|
nominal/core/video.py
CHANGED
|
@@ -8,9 +8,9 @@ from dataclasses import dataclass, field
|
|
|
8
8
|
from datetime import datetime, timedelta
|
|
9
9
|
from io import BytesIO, TextIOBase, TextIOWrapper
|
|
10
10
|
from types import MappingProxyType
|
|
11
|
-
from typing import BinaryIO, Mapping, Protocol, Sequence
|
|
11
|
+
from typing import BinaryIO, Mapping, Protocol, Sequence, overload
|
|
12
12
|
|
|
13
|
-
from nominal_api import api, ingest_api, scout_video, scout_video_api, upload_api
|
|
13
|
+
from nominal_api import api, ingest_api, scout_catalog, scout_video, scout_video_api, upload_api
|
|
14
14
|
from typing_extensions import Self
|
|
15
15
|
|
|
16
16
|
from nominal.core._clientsbunch import HasScoutParams
|
|
@@ -44,6 +44,8 @@ class Video(HasRid, RefreshableMixin[scout_video_api.Video]):
|
|
|
44
44
|
def ingest(self) -> ingest_api.IngestService: ...
|
|
45
45
|
@property
|
|
46
46
|
def video_file(self) -> scout_video.VideoFileService: ...
|
|
47
|
+
@property
|
|
48
|
+
def catalog(self) -> scout_catalog.CatalogService: ...
|
|
47
49
|
|
|
48
50
|
def poll_until_ingestion_completed(self, interval: timedelta = timedelta(seconds=1)) -> None:
|
|
49
51
|
"""Block until video ingestion has completed.
|
|
@@ -117,18 +119,38 @@ class Video(HasRid, RefreshableMixin[scout_video_api.Video]):
|
|
|
117
119
|
"""Unarchives this video, allowing it to show up in the 'All Videos' pane in the UI."""
|
|
118
120
|
self._clients.video.unarchive(self._clients.auth_header, self.rid)
|
|
119
121
|
|
|
122
|
+
@overload
|
|
123
|
+
def add_file(
|
|
124
|
+
self,
|
|
125
|
+
path: PathLike,
|
|
126
|
+
*,
|
|
127
|
+
start: datetime | IntegralNanosecondsUTC,
|
|
128
|
+
description: str | None = None,
|
|
129
|
+
) -> VideoFile: ...
|
|
130
|
+
|
|
131
|
+
@overload
|
|
120
132
|
def add_file(
|
|
121
133
|
self,
|
|
122
134
|
path: PathLike,
|
|
135
|
+
*,
|
|
136
|
+
frame_timestamps: Sequence[IntegralNanosecondsUTC],
|
|
137
|
+
description: str | None = None,
|
|
138
|
+
) -> VideoFile: ...
|
|
139
|
+
|
|
140
|
+
def add_file(
|
|
141
|
+
self,
|
|
142
|
+
path: PathLike,
|
|
143
|
+
*,
|
|
123
144
|
start: datetime | IntegralNanosecondsUTC | None = None,
|
|
124
145
|
frame_timestamps: Sequence[IntegralNanosecondsUTC] | None = None,
|
|
125
146
|
description: str | None = None,
|
|
126
147
|
) -> VideoFile:
|
|
127
|
-
"""Append to a video from a file-path to H264-encoded video data.
|
|
148
|
+
"""Append to a video from a file-path to H264-encoded video data. Only one of start or frame_timestamps
|
|
149
|
+
is allowed.
|
|
128
150
|
|
|
129
151
|
Args:
|
|
130
152
|
path: Path to the video file to add to an existing video within Nominal
|
|
131
|
-
start: Starting timestamp of the video file in absolute UTC time
|
|
153
|
+
start: Starting timestamp of the video file in absolute UTC time.
|
|
132
154
|
frame_timestamps: Per-frame absolute nanosecond timestamps. Most usecases should instead use the 'start'
|
|
133
155
|
parameter, unless precise per-frame metadata is available and desired.
|
|
134
156
|
description: Description of the video file.
|
|
@@ -141,16 +163,46 @@ class Video(HasRid, RefreshableMixin[scout_video_api.Video]):
|
|
|
141
163
|
file_type = FileType.from_video(path)
|
|
142
164
|
|
|
143
165
|
with path.open("rb") as video_file:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
166
|
+
if start is not None:
|
|
167
|
+
return self.add_from_io(
|
|
168
|
+
video_file,
|
|
169
|
+
name=path_upload_name(path, file_type),
|
|
170
|
+
start=start,
|
|
171
|
+
description=description,
|
|
172
|
+
file_type=file_type,
|
|
173
|
+
)
|
|
174
|
+
elif frame_timestamps is not None:
|
|
175
|
+
return self.add_from_io(
|
|
176
|
+
video_file,
|
|
177
|
+
name=path_upload_name(path, file_type),
|
|
178
|
+
frame_timestamps=frame_timestamps,
|
|
179
|
+
description=description,
|
|
180
|
+
file_type=file_type,
|
|
181
|
+
)
|
|
182
|
+
else: # This should never be reached due to the validation above
|
|
183
|
+
raise ValueError("Either 'start' or 'frame_timestamps' must be provided")
|
|
152
184
|
|
|
153
|
-
|
|
185
|
+
@overload
|
|
186
|
+
def add_from_io(
|
|
187
|
+
self,
|
|
188
|
+
video: BinaryIO,
|
|
189
|
+
name: str,
|
|
190
|
+
*,
|
|
191
|
+
start: datetime | IntegralNanosecondsUTC,
|
|
192
|
+
description: str | None = None,
|
|
193
|
+
file_type: tuple[str, str] | FileType = FileTypes.MP4,
|
|
194
|
+
) -> VideoFile: ...
|
|
195
|
+
|
|
196
|
+
@overload
|
|
197
|
+
def add_from_io(
|
|
198
|
+
self,
|
|
199
|
+
video: BinaryIO,
|
|
200
|
+
name: str,
|
|
201
|
+
*,
|
|
202
|
+
frame_timestamps: Sequence[IntegralNanosecondsUTC],
|
|
203
|
+
description: str | None = None,
|
|
204
|
+
file_type: tuple[str, str] | FileType = FileTypes.MP4,
|
|
205
|
+
) -> VideoFile: ...
|
|
154
206
|
|
|
155
207
|
def add_from_io(
|
|
156
208
|
self,
|
|
@@ -179,6 +231,12 @@ class Video(HasRid, RefreshableMixin[scout_video_api.Video]):
|
|
|
179
231
|
if isinstance(video, TextIOBase):
|
|
180
232
|
raise TypeError(f"video {video} must be open in binary mode, rather than text mode")
|
|
181
233
|
|
|
234
|
+
# Validation: ensure exactly one of start or frame_timestamps is provided
|
|
235
|
+
if start is None and frame_timestamps is None:
|
|
236
|
+
raise ValueError("Either 'start' or 'frame_timestamps' must be provided")
|
|
237
|
+
if start is not None and frame_timestamps is not None:
|
|
238
|
+
raise ValueError("Only one of 'start' or 'frame_timestamps' may be provided")
|
|
239
|
+
|
|
182
240
|
timestamp_manifest = _build_video_file_timestamp_manifest(
|
|
183
241
|
self._clients.auth_header, self._clients.workspace_rid, self._clients.upload, start, frame_timestamps
|
|
184
242
|
)
|
nominal/core/video_file.py
CHANGED
|
@@ -4,13 +4,14 @@ import logging
|
|
|
4
4
|
import time
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from datetime import datetime, timedelta
|
|
7
|
-
from typing import Protocol
|
|
7
|
+
from typing import Protocol, Tuple
|
|
8
8
|
|
|
9
|
-
from nominal_api import scout_video, scout_video_api
|
|
9
|
+
from nominal_api import scout_catalog, scout_video, scout_video_api
|
|
10
10
|
from typing_extensions import Self
|
|
11
11
|
|
|
12
12
|
from nominal.core._clientsbunch import HasScoutParams
|
|
13
13
|
from nominal.core._utils.api_tools import HasRid, RefreshableMixin
|
|
14
|
+
from nominal.core._video_types import McapVideoDetails, TimestampOptions
|
|
14
15
|
from nominal.core.exceptions import NominalIngestError, NominalIngestFailed
|
|
15
16
|
from nominal.ts import IntegralNanosecondsUTC, _SecondsNanos
|
|
16
17
|
|
|
@@ -28,6 +29,8 @@ class VideoFile(HasRid, RefreshableMixin[scout_video_api.VideoFile]):
|
|
|
28
29
|
class _Clients(HasScoutParams, Protocol):
|
|
29
30
|
@property
|
|
30
31
|
def video_file(self) -> scout_video.VideoFileService: ...
|
|
32
|
+
@property
|
|
33
|
+
def catalog(self) -> scout_catalog.CatalogService: ...
|
|
31
34
|
|
|
32
35
|
def archive(self) -> None:
|
|
33
36
|
"""Archive the video file, disallowing it to appear when playing back the video"""
|
|
@@ -128,6 +131,63 @@ class VideoFile(HasRid, RefreshableMixin[scout_video_api.VideoFile]):
|
|
|
128
131
|
|
|
129
132
|
time.sleep(interval.total_seconds())
|
|
130
133
|
|
|
134
|
+
def _get_file_ingest_options(self) -> Tuple[McapVideoDetails | None, TimestampOptions | None]:
|
|
135
|
+
"""Get ingest options metadata for this video file.
|
|
136
|
+
|
|
137
|
+
Retrieves metadata about the video file (such as timestamps, frame rate, and scale factor)
|
|
138
|
+
that can be used when ingesting this video into a video channel. The returned options
|
|
139
|
+
are either MCAP or MISC metadata depending on the video file type.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Video file ingest options (either McapVideoFileMetadata or MiscVideoFileMetadata).
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
ValueError: If the video file has an unexpected timestamp manifest type.
|
|
146
|
+
"""
|
|
147
|
+
api_video_file = self._get_latest_api()
|
|
148
|
+
if api_video_file.origin_metadata.timestamp_manifest.type == "mcap":
|
|
149
|
+
mcap_manifest = api_video_file.origin_metadata.timestamp_manifest.mcap
|
|
150
|
+
topic = (
|
|
151
|
+
mcap_manifest.mcap_channel_locator.topic
|
|
152
|
+
if mcap_manifest and mcap_manifest.mcap_channel_locator and mcap_manifest.mcap_channel_locator.topic
|
|
153
|
+
else ""
|
|
154
|
+
)
|
|
155
|
+
mcap_video_details = McapVideoDetails(
|
|
156
|
+
mcap_channel_locator_topic=topic,
|
|
157
|
+
)
|
|
158
|
+
return (mcap_video_details, None)
|
|
159
|
+
else:
|
|
160
|
+
# TODO(sean): We need to add support for if starting timestamp isn't present, aka we have frame timestamps
|
|
161
|
+
# from S3.
|
|
162
|
+
if api_video_file.origin_metadata.timestamp_manifest.no_manifest is None:
|
|
163
|
+
raise NotImplementedError(
|
|
164
|
+
f"Expected no_manifest timestamp manifest for non-MCAP video file, "
|
|
165
|
+
f"but got type: {api_video_file._origin_metadata._timestamp_manifest._type}"
|
|
166
|
+
)
|
|
167
|
+
if api_video_file.segment_metadata is None:
|
|
168
|
+
raise ValueError(
|
|
169
|
+
"Expected segment metadata for non-MCAP video file: %s", api_video_file.segment_metadata
|
|
170
|
+
)
|
|
171
|
+
if (
|
|
172
|
+
api_video_file.segment_metadata.max_absolute_timestamp is None
|
|
173
|
+
or api_video_file.segment_metadata.scale_factor is None
|
|
174
|
+
or api_video_file.segment_metadata.media_frame_rate is None
|
|
175
|
+
):
|
|
176
|
+
raise ValueError(
|
|
177
|
+
"Not all timestamp metadata is populated in segment metadata: %s", api_video_file.segment_metadata
|
|
178
|
+
)
|
|
179
|
+
video_file_ingest_options = TimestampOptions(
|
|
180
|
+
starting_timestamp=_SecondsNanos.from_api(
|
|
181
|
+
api_video_file.origin_metadata.timestamp_manifest.no_manifest.starting_timestamp
|
|
182
|
+
).to_nanoseconds(),
|
|
183
|
+
ending_timestamp=_SecondsNanos.from_api(
|
|
184
|
+
api_video_file.segment_metadata.max_absolute_timestamp
|
|
185
|
+
).to_nanoseconds(),
|
|
186
|
+
scaling_factor=api_video_file.segment_metadata.scale_factor,
|
|
187
|
+
true_framerate=api_video_file.segment_metadata.media_frame_rate,
|
|
188
|
+
)
|
|
189
|
+
return (None, video_file_ingest_options)
|
|
190
|
+
|
|
131
191
|
@classmethod
|
|
132
192
|
def _from_conjure(cls, clients: _Clients, video_file: scout_video_api.VideoFile) -> Self:
|
|
133
193
|
return cls(
|
|
@@ -4,7 +4,17 @@ import re
|
|
|
4
4
|
import uuid
|
|
5
5
|
from datetime import datetime, timedelta
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import (
|
|
8
|
+
Any,
|
|
9
|
+
BinaryIO,
|
|
10
|
+
Iterable,
|
|
11
|
+
Mapping,
|
|
12
|
+
Sequence,
|
|
13
|
+
TypeVar,
|
|
14
|
+
Union,
|
|
15
|
+
cast,
|
|
16
|
+
overload,
|
|
17
|
+
)
|
|
8
18
|
|
|
9
19
|
import requests
|
|
10
20
|
from conjure_python_client import ConjureBeanType, ConjureEnumType, ConjureUnionType
|
|
@@ -25,7 +35,10 @@ from nominal.core import (
|
|
|
25
35
|
from nominal.core._event_types import EventType, SearchEventOriginType
|
|
26
36
|
from nominal.core._utils.api_tools import Link, LinkDict
|
|
27
37
|
from nominal.core.attachment import Attachment
|
|
38
|
+
from nominal.core.filetype import FileTypes
|
|
28
39
|
from nominal.core.run import Run
|
|
40
|
+
from nominal.core.video import Video
|
|
41
|
+
from nominal.core.video_file import VideoFile
|
|
29
42
|
from nominal.experimental.dataset_utils import create_dataset_with_uuid
|
|
30
43
|
from nominal.experimental.migration.migration_data_config import MigrationDatasetConfig
|
|
31
44
|
from nominal.experimental.migration.migration_resources import MigrationResources
|
|
@@ -189,7 +202,10 @@ def _replace_uuids_in_obj(obj: Any, mapping: dict[str, str]) -> Any:
|
|
|
189
202
|
elif isinstance(value, str):
|
|
190
203
|
parsed_value, was_json = _convert_if_json(value)
|
|
191
204
|
if was_json:
|
|
192
|
-
new_obj[key] = json.dumps(
|
|
205
|
+
new_obj[key] = json.dumps(
|
|
206
|
+
_replace_uuids_in_obj(parsed_value, mapping),
|
|
207
|
+
separators=(",", ":"),
|
|
208
|
+
)
|
|
193
209
|
else:
|
|
194
210
|
new_obj[key] = _replace_uuids_in_obj(value, mapping)
|
|
195
211
|
else:
|
|
@@ -212,7 +228,9 @@ def _clone_conjure_objects_with_new_uuids(
|
|
|
212
228
|
|
|
213
229
|
|
|
214
230
|
@overload
|
|
215
|
-
def _clone_conjure_objects_with_new_uuids(
|
|
231
|
+
def _clone_conjure_objects_with_new_uuids(
|
|
232
|
+
objs: list[ConjureType],
|
|
233
|
+
) -> list[ConjureType]: ...
|
|
216
234
|
|
|
217
235
|
|
|
218
236
|
def _clone_conjure_objects_with_new_uuids(
|
|
@@ -303,7 +321,10 @@ def copy_workbook_template_from(
|
|
|
303
321
|
"destination_client_workspace": destination_client.get_workspace(destination_client._clients.workspace_rid).rid
|
|
304
322
|
}
|
|
305
323
|
logger.debug(
|
|
306
|
-
"Cloning workbook template: %s (rid: %s)",
|
|
324
|
+
"Cloning workbook template: %s (rid: %s)",
|
|
325
|
+
source_template.title,
|
|
326
|
+
source_template.rid,
|
|
327
|
+
extra=log_extras,
|
|
307
328
|
)
|
|
308
329
|
raw_source_template = source_template._clients.template.get(
|
|
309
330
|
source_template._clients.auth_header, source_template.rid
|
|
@@ -350,6 +371,140 @@ def copy_workbook_template_from(
|
|
|
350
371
|
return new_workbook_template
|
|
351
372
|
|
|
352
373
|
|
|
374
|
+
def copy_video_file_to_video_dataset(
|
|
375
|
+
source_video_file: VideoFile,
|
|
376
|
+
destination_video_dataset: Video,
|
|
377
|
+
) -> VideoFile | None:
|
|
378
|
+
"""Copy a video dataset file from the source to the destination dataset.
|
|
379
|
+
|
|
380
|
+
This method is specifically designed to handle video files, which may require special handling
|
|
381
|
+
due to their size and streaming nature. It retrieves the video file from the source dataset,
|
|
382
|
+
streams it, and uploads it to the destination dataset while maintaining all associated metadata.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
source_video_file: The source VideoFile to copy. Must be a video file with S3 handle.
|
|
386
|
+
destination_video_dataset: The Video dataset to create the copied file in.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
The dataset file in the new dataset.
|
|
390
|
+
"""
|
|
391
|
+
log_extras = {"destination_client_workspace": destination_video_dataset._clients.workspace_rid}
|
|
392
|
+
logger.debug("Copying video file: %s", source_video_file.name, extra=log_extras)
|
|
393
|
+
|
|
394
|
+
(mcap_video_details, timestamp_options) = source_video_file._get_file_ingest_options()
|
|
395
|
+
|
|
396
|
+
old_file_uri = source_video_file._clients.catalog.get_video_file_uri(
|
|
397
|
+
source_video_file._clients.auth_header, source_video_file.rid
|
|
398
|
+
).uri
|
|
399
|
+
|
|
400
|
+
response = requests.get(old_file_uri, stream=True)
|
|
401
|
+
response.raise_for_status()
|
|
402
|
+
|
|
403
|
+
file_name = source_video_file.name
|
|
404
|
+
file_stem = Path(file_name).stem
|
|
405
|
+
if timestamp_options is not None:
|
|
406
|
+
new_file = destination_video_dataset.add_from_io(
|
|
407
|
+
video=cast(BinaryIO, response.raw),
|
|
408
|
+
name=file_stem,
|
|
409
|
+
start=timestamp_options.starting_timestamp,
|
|
410
|
+
description=source_video_file.description,
|
|
411
|
+
)
|
|
412
|
+
new_file.update(
|
|
413
|
+
starting_timestamp=timestamp_options.starting_timestamp,
|
|
414
|
+
ending_timestamp=timestamp_options.ending_timestamp,
|
|
415
|
+
)
|
|
416
|
+
elif mcap_video_details is not None:
|
|
417
|
+
new_file = destination_video_dataset.add_mcap_from_io(
|
|
418
|
+
mcap=cast(BinaryIO, response.raw),
|
|
419
|
+
name=file_stem,
|
|
420
|
+
topic=mcap_video_details.mcap_channel_locator_topic,
|
|
421
|
+
description=source_video_file.description,
|
|
422
|
+
file_type=FileTypes.MCAP,
|
|
423
|
+
)
|
|
424
|
+
else:
|
|
425
|
+
raise ValueError(
|
|
426
|
+
"Unsupported video file ingest options for copying video file. "
|
|
427
|
+
"Expected either _mcap_video_details or _timestamp_options to be set."
|
|
428
|
+
)
|
|
429
|
+
logger.debug(
|
|
430
|
+
"New video file created %s in video dataset: %s (rid: %s)",
|
|
431
|
+
new_file.name,
|
|
432
|
+
destination_video_dataset.name,
|
|
433
|
+
destination_video_dataset.rid,
|
|
434
|
+
)
|
|
435
|
+
return new_file
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def clone_video(source_video: Video, destination_client: NominalClient) -> Video:
|
|
439
|
+
"""Clones a video, maintaining all properties and files.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
source_video (Video): The video to copy from.
|
|
443
|
+
destination_client (NominalClient): The destination client.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
The cloned video.
|
|
447
|
+
"""
|
|
448
|
+
return copy_video_from(
|
|
449
|
+
source_video=source_video,
|
|
450
|
+
destination_client=destination_client,
|
|
451
|
+
include_files=True,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def copy_video_from(
|
|
456
|
+
source_video: Video,
|
|
457
|
+
destination_client: NominalClient,
|
|
458
|
+
*,
|
|
459
|
+
new_video_name: str | None = None,
|
|
460
|
+
new_video_description: str | None = None,
|
|
461
|
+
new_video_properties: dict[str, Any] | None = None,
|
|
462
|
+
new_video_labels: Sequence[str] | None = None,
|
|
463
|
+
include_files: bool = False,
|
|
464
|
+
) -> Video:
|
|
465
|
+
"""Copy a video from the source to the destination client.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
source_video: The source Video to copy.
|
|
469
|
+
destination_client: The NominalClient to create the copied video in.
|
|
470
|
+
new_video_name: Optional new name for the copied video. If not provided, the original name is used.
|
|
471
|
+
new_video_description: Optional new description for the copied video.
|
|
472
|
+
If not provided, the original description is used.
|
|
473
|
+
new_video_properties: Optional new properties for the copied video. If not provided, the original
|
|
474
|
+
properties are used.
|
|
475
|
+
new_video_labels: Optional new labels for the copied video. If not provided, the original labels are used.
|
|
476
|
+
include_files: Whether to include files in the copied video.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
The newly created Video in the destination client.
|
|
480
|
+
"""
|
|
481
|
+
log_extras = {
|
|
482
|
+
"destination_client_workspace": destination_client.get_workspace(destination_client._clients.workspace_rid).rid
|
|
483
|
+
}
|
|
484
|
+
logger.debug(
|
|
485
|
+
"Copying dataset %s (rid: %s)",
|
|
486
|
+
source_video.name,
|
|
487
|
+
source_video.rid,
|
|
488
|
+
extra=log_extras,
|
|
489
|
+
)
|
|
490
|
+
new_video = destination_client.create_video(
|
|
491
|
+
name=new_video_name if new_video_name is not None else source_video.name,
|
|
492
|
+
description=new_video_description if new_video_description is not None else source_video.description,
|
|
493
|
+
properties=new_video_properties if new_video_properties is not None else source_video.properties,
|
|
494
|
+
labels=new_video_labels if new_video_labels is not None else source_video.labels,
|
|
495
|
+
)
|
|
496
|
+
if include_files:
|
|
497
|
+
for source_file in source_video.list_files():
|
|
498
|
+
copy_video_file_to_video_dataset(source_file, new_video)
|
|
499
|
+
logger.debug(
|
|
500
|
+
"New video created: %s (rid: %s)",
|
|
501
|
+
new_video.name,
|
|
502
|
+
new_video.rid,
|
|
503
|
+
extra=log_extras,
|
|
504
|
+
)
|
|
505
|
+
return new_video
|
|
506
|
+
|
|
507
|
+
|
|
353
508
|
def copy_file_to_dataset(
|
|
354
509
|
source_file: DatasetFile,
|
|
355
510
|
destination_dataset: Dataset,
|
|
@@ -403,7 +558,7 @@ def copy_file_to_dataset(
|
|
|
403
558
|
|
|
404
559
|
|
|
405
560
|
def clone_dataset(source_dataset: Dataset, destination_client: NominalClient) -> Dataset:
|
|
406
|
-
"""Clones a dataset, maintaining all properties and
|
|
561
|
+
"""Clones a dataset, maintaining all properties, files, and channels.
|
|
407
562
|
|
|
408
563
|
Args:
|
|
409
564
|
source_dataset (Dataset): The dataset to copy from.
|
|
@@ -412,7 +567,11 @@ def clone_dataset(source_dataset: Dataset, destination_client: NominalClient) ->
|
|
|
412
567
|
Returns:
|
|
413
568
|
The cloned dataset.
|
|
414
569
|
"""
|
|
415
|
-
return copy_dataset_from(
|
|
570
|
+
return copy_dataset_from(
|
|
571
|
+
source_dataset=source_dataset,
|
|
572
|
+
destination_client=destination_client,
|
|
573
|
+
include_files=True,
|
|
574
|
+
)
|
|
416
575
|
|
|
417
576
|
|
|
418
577
|
def copy_dataset_from(
|
|
@@ -482,10 +641,29 @@ def copy_dataset_from(
|
|
|
482
641
|
labels=dataset_labels,
|
|
483
642
|
)
|
|
484
643
|
|
|
644
|
+
if preserve_uuid:
|
|
645
|
+
channels_copied_count = 0
|
|
646
|
+
for source_channel in source_dataset.search_channels():
|
|
647
|
+
if source_channel.data_type is None:
|
|
648
|
+
logger.warning("Skipping channel %s: unknown data type", source_channel.name, extra=log_extras)
|
|
649
|
+
continue
|
|
650
|
+
new_dataset.add_channel(
|
|
651
|
+
name=source_channel.name,
|
|
652
|
+
data_type=source_channel.data_type,
|
|
653
|
+
description=source_channel.description,
|
|
654
|
+
unit=source_channel.unit,
|
|
655
|
+
)
|
|
656
|
+
channels_copied_count += 1
|
|
657
|
+
logger.info("Copied %d channels from dataset %s", channels_copied_count, source_dataset.name, extra=log_extras)
|
|
485
658
|
if include_files:
|
|
486
659
|
for source_file in source_dataset.list_files():
|
|
487
660
|
copy_file_to_dataset(source_file, new_dataset)
|
|
488
|
-
logger.debug(
|
|
661
|
+
logger.debug(
|
|
662
|
+
"New dataset created: %s (rid: %s)",
|
|
663
|
+
new_dataset.name,
|
|
664
|
+
new_dataset.rid,
|
|
665
|
+
extra=log_extras,
|
|
666
|
+
)
|
|
489
667
|
return new_dataset
|
|
490
668
|
|
|
491
669
|
|
|
@@ -551,7 +729,12 @@ def copy_event_from(
|
|
|
551
729
|
properties=new_properties or source_event.properties,
|
|
552
730
|
labels=new_labels or source_event.labels,
|
|
553
731
|
)
|
|
554
|
-
logger.debug(
|
|
732
|
+
logger.debug(
|
|
733
|
+
"New event created: %s (rid: %s)",
|
|
734
|
+
new_event.name,
|
|
735
|
+
new_event.rid,
|
|
736
|
+
extra=log_extras,
|
|
737
|
+
)
|
|
555
738
|
return new_event
|
|
556
739
|
|
|
557
740
|
|
|
@@ -643,6 +826,7 @@ def clone_asset(
|
|
|
643
826
|
dataset_config=MigrationDatasetConfig(preserve_dataset_uuid=True, include_dataset_files=True),
|
|
644
827
|
include_events=True,
|
|
645
828
|
include_runs=True,
|
|
829
|
+
include_video=True,
|
|
646
830
|
)
|
|
647
831
|
|
|
648
832
|
|
|
@@ -657,6 +841,7 @@ def copy_asset_from(
|
|
|
657
841
|
dataset_config: MigrationDatasetConfig | None = None,
|
|
658
842
|
include_events: bool = False,
|
|
659
843
|
include_runs: bool = False,
|
|
844
|
+
include_video: bool = False,
|
|
660
845
|
) -> Asset:
|
|
661
846
|
"""Copy an asset from the source to the destination client.
|
|
662
847
|
|
|
@@ -670,6 +855,7 @@ def copy_asset_from(
|
|
|
670
855
|
dataset_config: Configuration for dataset migration.
|
|
671
856
|
include_events: Whether to include events in the copied dataset.
|
|
672
857
|
include_runs: Whether to include runs in the copied asset.
|
|
858
|
+
include_video: Whether to include video in the copied asset.
|
|
673
859
|
|
|
674
860
|
Returns:
|
|
675
861
|
The new asset created.
|
|
@@ -677,7 +863,12 @@ def copy_asset_from(
|
|
|
677
863
|
log_extras = {
|
|
678
864
|
"destination_client_workspace": destination_client.get_workspace(destination_client._clients.workspace_rid).rid
|
|
679
865
|
}
|
|
680
|
-
logger.debug(
|
|
866
|
+
logger.debug(
|
|
867
|
+
"Copying asset %s (rid: %s)",
|
|
868
|
+
source_asset.name,
|
|
869
|
+
source_asset.rid,
|
|
870
|
+
extra=log_extras,
|
|
871
|
+
)
|
|
681
872
|
new_asset = destination_client.create_asset(
|
|
682
873
|
name=new_asset_name if new_asset_name is not None else source_asset.name,
|
|
683
874
|
description=new_asset_description if new_asset_description is not None else source_asset.description,
|
|
@@ -705,6 +896,18 @@ def copy_asset_from(
|
|
|
705
896
|
for source_run in source_runs:
|
|
706
897
|
copy_run_from(source_run, destination_client, new_assets=[new_asset])
|
|
707
898
|
|
|
899
|
+
if include_video:
|
|
900
|
+
for data_scope, video_dataset in source_asset.list_videos():
|
|
901
|
+
new_video_dataset = destination_client.create_video(
|
|
902
|
+
name=video_dataset.name,
|
|
903
|
+
description=video_dataset.description,
|
|
904
|
+
properties=video_dataset.properties,
|
|
905
|
+
labels=video_dataset.labels,
|
|
906
|
+
)
|
|
907
|
+
new_asset.add_video(data_scope, new_video_dataset)
|
|
908
|
+
for source_video_file in video_dataset.list_files():
|
|
909
|
+
copy_video_file_to_video_dataset(source_video_file, new_video_dataset)
|
|
910
|
+
|
|
708
911
|
logger.debug("New asset created: %s (rid: %s)", new_asset, new_asset.rid, extra=log_extras)
|
|
709
912
|
return new_asset
|
|
710
913
|
|
|
@@ -741,6 +944,7 @@ def copy_resources_to_destination_client(
|
|
|
741
944
|
dataset_config=dataset_config,
|
|
742
945
|
include_events=True,
|
|
743
946
|
include_runs=True,
|
|
947
|
+
include_video=True,
|
|
744
948
|
)
|
|
745
949
|
new_assets.append(new_asset)
|
|
746
950
|
new_data_scopes_and_datasets.extend(new_asset.list_datasets())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nominal
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.110.0
|
|
4
4
|
Summary: Automate Nominal workflows in Python
|
|
5
5
|
Project-URL: Homepage, https://nominal.io
|
|
6
6
|
Project-URL: Documentation, https://docs.nominal.io
|
|
@@ -20,7 +20,7 @@ Requires-Dist: cachetools>=6.1.0
|
|
|
20
20
|
Requires-Dist: click<9,>=8
|
|
21
21
|
Requires-Dist: conjure-python-client<4,>=3.1.0
|
|
22
22
|
Requires-Dist: ffmpeg-python>=0.2.0
|
|
23
|
-
Requires-Dist: nominal-api==0.
|
|
23
|
+
Requires-Dist: nominal-api==0.1079.0
|
|
24
24
|
Requires-Dist: nominal-streaming==0.5.8; platform_python_implementation == 'CPython' and python_version >= '3.10' and ((sys_platform == 'win32' and platform_machine == 'AMD64') or (sys_platform == 'darwin' and platform_machine == 'arm64') or (sys_platform == 'linux' and (platform_machine == 'x86_64' or platform_machine == 'armv7l')))
|
|
25
25
|
Requires-Dist: openpyxl>=0.0.0
|
|
26
26
|
Requires-Dist: pandas>=0.0.0
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
CHANGELOG.md,sha256
|
|
1
|
+
CHANGELOG.md,sha256=Om-RWDZX7EQkqisCCqMAftphnIXM3nr7RwpP58_WZXE,90706
|
|
2
2
|
LICENSE,sha256=zEGHG9mjDjaIS3I79O8mweQo-yiTbqx8jJvUPppVAwk,1067
|
|
3
3
|
README.md,sha256=KKe0dxh_pHXCtB7I9G4qWGQYvot_BZU8yW6MJyuyUHM,311
|
|
4
4
|
nominal/__init__.py,sha256=rbraORnXUrNn1hywLXM0XwSQCd9UmQt20PDYlsBalfE,2167
|
|
@@ -28,32 +28,35 @@ nominal/cli/util/verify_connection.py,sha256=KU17ejaDfKBLmLiZ3MZSVLyfrqNE7c6mFBv
|
|
|
28
28
|
nominal/config/__init__.py,sha256=wV8cq8X3J4NTJ5H_uR5THaMT_NQpWQO5qCUGEb-rPnM,3157
|
|
29
29
|
nominal/config/_config.py,sha256=yKq_H1iYJDoxRfLz2iXLbbVdoL0MTEY0FS4eVL12w0g,2004
|
|
30
30
|
nominal/core/__init__.py,sha256=1MiCC44cxHYFofP4hf2fz4EIkepK-OAhDzpPFIzHbWw,2422
|
|
31
|
+
nominal/core/_checklist_types.py,sha256=YcjvuUbQDIOYqqM5H1Eto9ws9Ivm4cPWEaeEF2Uwn1o,1361
|
|
31
32
|
nominal/core/_clientsbunch.py,sha256=sn-ajYsJ2FuZGLiEYiPwkYCxLKr13w35aqWHE3TX5us,8633
|
|
32
33
|
nominal/core/_constants.py,sha256=SrxgaSqAEB1MvTSrorgGam3eO29iCmRr6VIdajxX3gI,56
|
|
33
34
|
nominal/core/_event_types.py,sha256=Cq_8x-zv_5EDvRo9UTbaOpenAy92bTfQxlsEuHPOhtE,3706
|
|
34
35
|
nominal/core/_types.py,sha256=FktMmcQ5_rD2rbXv8_p-WISzSo8T2NtO-exsLm-iadU,122
|
|
35
|
-
nominal/core/
|
|
36
|
+
nominal/core/_video_types.py,sha256=Cdl0sZxX3cyYtCXzsnnLWjK38hHp3_orMe6oiUU_dyc,465
|
|
37
|
+
nominal/core/asset.py,sha256=S41KS_c14tFcxFLJzU3bnt958KpMSI_U524QcMCiSEE,23609
|
|
36
38
|
nominal/core/attachment.py,sha256=yOtDUdkLY5MT_Rk9kUlr1yupIJN7a5pt5sJWx4RLQV8,4355
|
|
37
39
|
nominal/core/bounds.py,sha256=742BWmGL3FBryRAjoiJRg2N6aVinjYkQLxN7kfnJ40Q,581
|
|
38
40
|
nominal/core/channel.py,sha256=e0RtbjrXYMAqyt8noe610iIFzv30z2P7oqrwq08atD8,19558
|
|
39
|
-
nominal/core/checklist.py,sha256=
|
|
40
|
-
nominal/core/client.py,sha256=
|
|
41
|
+
nominal/core/checklist.py,sha256=TXEm9qNYCG6lU5NB5P3RAe-XmXdj1Tcsdbx_c5_spXI,6663
|
|
42
|
+
nominal/core/client.py,sha256=RjMQCU8DmvHcp7lypVCFLY54caoTXu76EwN-oxaFjsw,68091
|
|
41
43
|
nominal/core/connection.py,sha256=LYllr3a1H2xp8-i4MaX1M7yK8X-HnwuIkciyK9XgLtQ,5175
|
|
42
44
|
nominal/core/containerized_extractors.py,sha256=fUz3-NHoNWYKqOCD15gLwGXDKVfdsW-x_kpXnkOI3BE,10224
|
|
43
|
-
nominal/core/data_review.py,sha256=
|
|
45
|
+
nominal/core/data_review.py,sha256=8pyoJiP-6KCSSB4NE_LKjp1JfenEigHTmEVdF1xF1bA,11674
|
|
44
46
|
nominal/core/dataset.py,sha256=LqofzNAlOd3S_3Aaw6b7DoY50rj6GyMHbUClIA2TmpY,46792
|
|
45
|
-
nominal/core/dataset_file.py,sha256=
|
|
47
|
+
nominal/core/dataset_file.py,sha256=1cvEsf3IXGCOIr5kWIBBSwfHpZMAY-BEUEtewR6RjNc,16789
|
|
46
48
|
nominal/core/datasource.py,sha256=i9db5OHNdfJyb3BJmhz2uJ398MfW8zx3Ek9pw5vYn3c,18739
|
|
47
49
|
nominal/core/event.py,sha256=8trZXyuAqRlKedgcqSgDIimXAAJBmEfDLyHkOOBwUC0,7762
|
|
48
50
|
nominal/core/exceptions.py,sha256=GUpwXRgdYamLl6684FE8ttCRHkBx6WEhOZ3NPE-ybD4,2671
|
|
49
51
|
nominal/core/filetype.py,sha256=R8goHGW4SP0iO6AoQiUil2tNVuDgaQoHclftRbw44oc,5558
|
|
50
52
|
nominal/core/log.py,sha256=z3hI3CIEyMwpUSWjwBsJ6a3JNGzBbsmrVusSU6uI7CY,3885
|
|
51
|
-
nominal/core/run.py,sha256=
|
|
53
|
+
nominal/core/run.py,sha256=1mRMl4bfmhd0MUR-JkvgqqkJYU8_RDNtKX1Qh8xtNtY,18308
|
|
52
54
|
nominal/core/secret.py,sha256=Ckq48m60i7rktxL9GY-nxHU5v8gHv9F1-JN7_MSf4bM,2863
|
|
55
|
+
nominal/core/streaming_checklist.py,sha256=t7cilpW79hUQ86fJxiAr4Hocy9CdpLLP4azonjOi22o,844
|
|
53
56
|
nominal/core/unit.py,sha256=Wa-Bvu0hD-nzxVaQJSnn5YqAfnhUd2kWw2SswXnbMHY,3161
|
|
54
57
|
nominal/core/user.py,sha256=FV333TN4pQzcLh5b2CfxvBnnXyB1TrOP8Ppx1-XdaiE,481
|
|
55
|
-
nominal/core/video.py,sha256=
|
|
56
|
-
nominal/core/video_file.py,sha256=
|
|
58
|
+
nominal/core/video.py,sha256=p5H46V-esDEEg-j6X0zKrX3_xe5yYA6PSM4MmW6z3a8,18126
|
|
59
|
+
nominal/core/video_file.py,sha256=haq5Gf6V4HCP7iq-wObq5voGEx96ApJ2Ju3FPcTsv4U,8887
|
|
57
60
|
nominal/core/workbook.py,sha256=lJo9ZaYm0TevAyIs239ZA-_1WUriTkj8i1lxvxH9TJw,8902
|
|
58
61
|
nominal/core/workbook_template.py,sha256=PBgQjEDVVQdZMlVea99BbhHdAr_bawknSvNKhNtDAq0,7125
|
|
59
62
|
nominal/core/workspace.py,sha256=_FmMu86xzIcxMt8_82oRSe3N4ss3law-rk0I0s8GMCQ,512
|
|
@@ -64,7 +67,7 @@ nominal/core/_stream/write_stream.py,sha256=Xd4VnWU9NANHu7hzknylv_d7qWoIiAOqzVtX
|
|
|
64
67
|
nominal/core/_stream/write_stream_base.py,sha256=AxK3fAq3IBjNXZkxYFVXu3dGNWLCBhgknroMEyXqVJo,3787
|
|
65
68
|
nominal/core/_utils/README.md,sha256=kWPQDc6kn-PjXFUsIH9u2nOA3RdGSXCOlxqeJSmUsPA,160
|
|
66
69
|
nominal/core/_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
67
|
-
nominal/core/_utils/api_tools.py,sha256=
|
|
70
|
+
nominal/core/_utils/api_tools.py,sha256=rFL_YsdsEhxHi8v1boDM1NCk1olngRSJjYLUd-9cG-s,3539
|
|
68
71
|
nominal/core/_utils/multipart.py,sha256=0dA2XcTHuOQIyS0139O8WZiCjwePaD1sYDUmTgmWG9w,10243
|
|
69
72
|
nominal/core/_utils/multipart_downloader.py,sha256=16OJEPqxCwOnfjptYdrlwQVuSUQYoe9_iiW60ZSjWos,13859
|
|
70
73
|
nominal/core/_utils/networking.py,sha256=n9ZqYtnpwPCjz9C-4eixsTkrhFh-DW6lknBJlHckHhg,8200
|
|
@@ -91,7 +94,7 @@ nominal/experimental/logging/rich_log_handler.py,sha256=8yz_VtxNgJg2oiesnXz2iXoB
|
|
|
91
94
|
nominal/experimental/migration/__init__.py,sha256=E2IgWJLwJ5bN6jbl8k5nHECKFx5aT11jKAzVYcyXn3o,460
|
|
92
95
|
nominal/experimental/migration/migration_data_config.py,sha256=sPwZjyLmL-_pHvDZvQspxrfW6yNZhEsQjDVwKA8IaXM,522
|
|
93
96
|
nominal/experimental/migration/migration_resources.py,sha256=Tf_7kNBeSaY8z2fTF7DAxk-9q3a7F8xXFVvxI8tTc9c,415
|
|
94
|
-
nominal/experimental/migration/migration_utils.py,sha256=
|
|
97
|
+
nominal/experimental/migration/migration_utils.py,sha256=w__-709d_CDX1XrASAADYTiXb4T_Vd7lJ7sBC_f8Lto,38965
|
|
95
98
|
nominal/experimental/rust_streaming/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
96
99
|
nominal/experimental/rust_streaming/rust_write_stream.py,sha256=oQ6ixwm8ct8ZDc_qNB7AucDt8o5-_aBVlW2fFCQ_nmA,1541
|
|
97
100
|
nominal/experimental/stream_v2/__init__.py,sha256=W39vK46pssx5sXvmsImMuJiEPs7iGtwrbYBI0bWnXCY,2313
|
|
@@ -110,8 +113,8 @@ nominal/thirdparty/polars/polars_export_handler.py,sha256=hGCSwXX9dC4MG01CmmjlTb
|
|
|
110
113
|
nominal/thirdparty/tdms/__init__.py,sha256=6n2ImFr2Wiil6JM1P5Q7Mpr0VzLcnDkmup_ftNpPq-s,142
|
|
111
114
|
nominal/thirdparty/tdms/_tdms.py,sha256=m4gxbpxB9MTLi2FuYvGlbUGSyDAZKFxbM3ia2x1wIz0,8746
|
|
112
115
|
nominal/ts/__init__.py,sha256=hmd0ENvDhxRnzDKGLxIub6QG8LpcxCgcyAct029CaEs,21442
|
|
113
|
-
nominal-1.
|
|
114
|
-
nominal-1.
|
|
115
|
-
nominal-1.
|
|
116
|
-
nominal-1.
|
|
117
|
-
nominal-1.
|
|
116
|
+
nominal-1.110.0.dist-info/METADATA,sha256=3QUEWDIsDP87VsQbNKDgcBE238-wwKpGJFl4NSKGMi8,2307
|
|
117
|
+
nominal-1.110.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
118
|
+
nominal-1.110.0.dist-info/entry_points.txt,sha256=-mCLhxgg9R_lm5efT7vW9wuBH12izvY322R0a3TYxbE,66
|
|
119
|
+
nominal-1.110.0.dist-info/licenses/LICENSE,sha256=zEGHG9mjDjaIS3I79O8mweQo-yiTbqx8jJvUPppVAwk,1067
|
|
120
|
+
nominal-1.110.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|