nominal 1.101.0__py3-none-any.whl → 1.102.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 +15 -0
- nominal/core/__init__.py +3 -1
- nominal/core/_event_types.py +100 -0
- nominal/core/_utils/query_tools.py +52 -1
- nominal/core/asset.py +87 -41
- nominal/core/client.py +21 -50
- nominal/core/event.py +9 -82
- nominal/core/run.py +6 -3
- nominal/experimental/migration/migration_utils.py +107 -8
- {nominal-1.101.0.dist-info → nominal-1.102.0.dist-info}/METADATA +1 -1
- {nominal-1.101.0.dist-info → nominal-1.102.0.dist-info}/RECORD +14 -13
- {nominal-1.101.0.dist-info → nominal-1.102.0.dist-info}/WHEEL +0 -0
- {nominal-1.101.0.dist-info → nominal-1.102.0.dist-info}/entry_points.txt +0 -0
- {nominal-1.101.0.dist-info → nominal-1.102.0.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.102.0](https://github.com/nominal-io/nominal-client/compare/v1.101.0...v1.102.0) (2026-01-05)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add run creation on an asset ([#558](https://github.com/nominal-io/nominal-client/issues/558)) ([9c0718a](https://github.com/nominal-io/nominal-client/commit/9c0718ae2706d4192087fb787e99f76d702a18c9))
|
|
9
|
+
* add util to search events by origin type and add clone/copy events ([#555](https://github.com/nominal-io/nominal-client/issues/555)) ([bc427d6](https://github.com/nominal-io/nominal-client/commit/bc427d6b97a777e6802dc8139c8d389582558a77))
|
|
10
|
+
* allow unarchiving a run ([#560](https://github.com/nominal-io/nominal-client/issues/560)) ([0221a56](https://github.com/nominal-io/nominal-client/commit/0221a56c73d133581567cc5183fda237607d79b5))
|
|
11
|
+
* clean up and deprecate old video creation methods in NominalClient ([#561](https://github.com/nominal-io/nominal-client/issues/561)) ([e61919c](https://github.com/nominal-io/nominal-client/commit/e61919ca338a5e0de818471431d750246d87977c))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* update how workspaces are selected in migration_utils ([#559](https://github.com/nominal-io/nominal-client/issues/559)) ([4379121](https://github.com/nominal-io/nominal-client/commit/4379121f44411eed19248ee16540672bbed743a0))
|
|
17
|
+
|
|
3
18
|
## [1.101.0](https://github.com/nominal-io/nominal-client/compare/v1.100.0...v1.101.0) (2025-12-23)
|
|
4
19
|
|
|
5
20
|
|
nominal/core/__init__.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from nominal.core._event_types import EventType, SearchEventOriginType
|
|
1
2
|
from nominal.core._stream.write_stream import WriteStream
|
|
2
3
|
from nominal.core._utils.api_tools import LinkDict
|
|
3
4
|
from nominal.core.asset import Asset
|
|
@@ -19,7 +20,7 @@ from nominal.core.data_review import CheckViolation, DataReview, DataReviewBuild
|
|
|
19
20
|
from nominal.core.dataset import Dataset, poll_until_ingestion_completed
|
|
20
21
|
from nominal.core.dataset_file import DatasetFile, IngestWaitType, as_files_ingested, wait_for_files_to_ingest
|
|
21
22
|
from nominal.core.datasource import DataSource
|
|
22
|
-
from nominal.core.event import Event
|
|
23
|
+
from nominal.core.event import Event
|
|
23
24
|
from nominal.core.filetype import FileType, FileTypes
|
|
24
25
|
from nominal.core.log import LogPoint
|
|
25
26
|
from nominal.core.run import Run
|
|
@@ -60,6 +61,7 @@ __all__ = [
|
|
|
60
61
|
"NominalClient",
|
|
61
62
|
"poll_until_ingestion_completed",
|
|
62
63
|
"Run",
|
|
64
|
+
"SearchEventOriginType",
|
|
63
65
|
"Secret",
|
|
64
66
|
"TagDetails",
|
|
65
67
|
"TimestampMetadata",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Iterable, NamedTuple
|
|
5
|
+
|
|
6
|
+
from nominal_api import event
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EventType(Enum):
|
|
10
|
+
INFO = "INFO"
|
|
11
|
+
FLAG = "FLAG"
|
|
12
|
+
ERROR = "ERROR"
|
|
13
|
+
SUCCESS = "SUCCESS"
|
|
14
|
+
UNKNOWN = "UNKNOWN"
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_api_event_type(cls, event: event.EventType) -> EventType:
|
|
18
|
+
if event.name == "INFO":
|
|
19
|
+
return cls.INFO
|
|
20
|
+
elif event.name == "FLAG":
|
|
21
|
+
return cls.FLAG
|
|
22
|
+
elif event.name == "ERROR":
|
|
23
|
+
return cls.ERROR
|
|
24
|
+
elif event.name == "SUCCESS":
|
|
25
|
+
return cls.SUCCESS
|
|
26
|
+
else:
|
|
27
|
+
return cls.UNKNOWN
|
|
28
|
+
|
|
29
|
+
def _to_api_event_type(self) -> event.EventType:
|
|
30
|
+
if self.name == "INFO":
|
|
31
|
+
return event.EventType.INFO
|
|
32
|
+
elif self.name == "FLAG":
|
|
33
|
+
return event.EventType.FLAG
|
|
34
|
+
elif self.name == "ERROR":
|
|
35
|
+
return event.EventType.ERROR
|
|
36
|
+
elif self.name == "SUCCESS":
|
|
37
|
+
return event.EventType.SUCCESS
|
|
38
|
+
else:
|
|
39
|
+
return event.EventType.UNKNOWN
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class EventCreationType(Enum):
|
|
43
|
+
MANUAL = "MANUAL"
|
|
44
|
+
BY_EXTERNAL_RESOURCE = "BY_EXTERNAL_RESOURCE"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SearchEventOriginType(NamedTuple):
|
|
48
|
+
name: str
|
|
49
|
+
creation_type: EventCreationType
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_api_origin_type(cls, event: event.SearchEventOriginType) -> SearchEventOriginType:
|
|
53
|
+
if event.name == "WORKBOOK":
|
|
54
|
+
return SearchEventOriginTypes.WORKBOOK
|
|
55
|
+
elif event.name == "TEMPLATE":
|
|
56
|
+
return SearchEventOriginTypes.TEMPLATE
|
|
57
|
+
elif event.name == "API":
|
|
58
|
+
return SearchEventOriginTypes.API
|
|
59
|
+
elif event.name == "DATA_REVIEW":
|
|
60
|
+
return SearchEventOriginTypes.DATA_REVIEW
|
|
61
|
+
elif event.name == "PROCEDURE":
|
|
62
|
+
return SearchEventOriginTypes.PROCEDURE
|
|
63
|
+
elif event.name == "STREAMING_CHECKLIST":
|
|
64
|
+
return SearchEventOriginTypes.STREAMING_CHECKLIST
|
|
65
|
+
else:
|
|
66
|
+
raise ValueError(f"Unexpected Event Origin {event.name}")
|
|
67
|
+
|
|
68
|
+
def _to_api_search_event_origin_type(self) -> event.SearchEventOriginType:
|
|
69
|
+
if self.name == "WORKBOOK":
|
|
70
|
+
return event.SearchEventOriginType.WORKBOOK
|
|
71
|
+
elif self.name == "TEMPLATE":
|
|
72
|
+
return event.SearchEventOriginType.TEMPLATE
|
|
73
|
+
elif self.name == "API":
|
|
74
|
+
return event.SearchEventOriginType.API
|
|
75
|
+
elif self.name == "DATA_REVIEW":
|
|
76
|
+
return event.SearchEventOriginType.DATA_REVIEW
|
|
77
|
+
elif self.name == "PROCEDURE":
|
|
78
|
+
return event.SearchEventOriginType.PROCEDURE
|
|
79
|
+
elif self.name == "STREAMING_CHECKLIST":
|
|
80
|
+
return event.SearchEventOriginType.STREAMING_CHECKLIST
|
|
81
|
+
else:
|
|
82
|
+
raise ValueError(f"Unexpected Event Origin {self.name}")
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def get_manual_origin_types(cls) -> Iterable[SearchEventOriginType]:
|
|
86
|
+
"""Return all origin types that are manually created."""
|
|
87
|
+
return [
|
|
88
|
+
origin_type
|
|
89
|
+
for origin_type in SearchEventOriginTypes.__dict__.values()
|
|
90
|
+
if isinstance(origin_type, SearchEventOriginType) and origin_type.creation_type == EventCreationType.MANUAL
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class SearchEventOriginTypes:
|
|
95
|
+
WORKBOOK = SearchEventOriginType("WORKBOOK", EventCreationType.MANUAL)
|
|
96
|
+
TEMPLATE = SearchEventOriginType("TEMPLATE", EventCreationType.MANUAL)
|
|
97
|
+
API = SearchEventOriginType("API", EventCreationType.MANUAL)
|
|
98
|
+
DATA_REVIEW = SearchEventOriginType("DATA_REVIEW", EventCreationType.BY_EXTERNAL_RESOURCE)
|
|
99
|
+
PROCEDURE = SearchEventOriginType("PROCEDURE", EventCreationType.BY_EXTERNAL_RESOURCE)
|
|
100
|
+
STREAMING_CHECKLIST = SearchEventOriginType("STREAMING_CHECKLIST", EventCreationType.BY_EXTERNAL_RESOURCE)
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from typing import Mapping, Sequence
|
|
4
|
+
from typing import Iterable, Mapping, Sequence
|
|
5
5
|
|
|
6
6
|
from nominal_api import (
|
|
7
7
|
api,
|
|
8
8
|
authentication_api,
|
|
9
|
+
event,
|
|
9
10
|
ingest_api,
|
|
10
11
|
scout_asset_api,
|
|
11
12
|
scout_catalog,
|
|
@@ -350,3 +351,53 @@ def create_search_workbook_templates_query(
|
|
|
350
351
|
queries.append(scout_template_api.SearchTemplatesQuery(is_published=published))
|
|
351
352
|
|
|
352
353
|
return scout_template_api.SearchTemplatesQuery(and_=queries)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _create_search_events_query( # noqa: PLR0912
|
|
357
|
+
search_text: str | None = None,
|
|
358
|
+
after: str | datetime | IntegralNanosecondsUTC | None = None,
|
|
359
|
+
before: str | datetime | IntegralNanosecondsUTC | None = None,
|
|
360
|
+
asset_rids: Iterable[str] | None = None,
|
|
361
|
+
labels: Iterable[str] | None = None,
|
|
362
|
+
properties: Mapping[str, str] | None = None,
|
|
363
|
+
created_by_rid: str | None = None,
|
|
364
|
+
workbook_rid: str | None = None,
|
|
365
|
+
data_review_rid: str | None = None,
|
|
366
|
+
assignee_rid: str | None = None,
|
|
367
|
+
event_type: event.EventType | None = None,
|
|
368
|
+
origin_types: Iterable[event.SearchEventOriginType] | None = None,
|
|
369
|
+
workspace_rid: str | None = None,
|
|
370
|
+
) -> event.SearchQuery:
|
|
371
|
+
queries = []
|
|
372
|
+
if search_text is not None:
|
|
373
|
+
queries.append(event.SearchQuery(search_text=search_text))
|
|
374
|
+
if after is not None:
|
|
375
|
+
queries.append(event.SearchQuery(after=_SecondsNanos.from_flexible(after).to_api()))
|
|
376
|
+
if before is not None:
|
|
377
|
+
queries.append(event.SearchQuery(before=_SecondsNanos.from_flexible(before).to_api()))
|
|
378
|
+
if asset_rids:
|
|
379
|
+
for asset in asset_rids:
|
|
380
|
+
queries.append(event.SearchQuery(asset=asset))
|
|
381
|
+
if labels:
|
|
382
|
+
for label in labels:
|
|
383
|
+
queries.append(event.SearchQuery(label=label))
|
|
384
|
+
if properties:
|
|
385
|
+
for name, value in properties.items():
|
|
386
|
+
queries.append(event.SearchQuery(property=api.Property(name=name, value=value)))
|
|
387
|
+
if created_by_rid:
|
|
388
|
+
queries.append(event.SearchQuery(created_by=created_by_rid))
|
|
389
|
+
if workbook_rid is not None:
|
|
390
|
+
queries.append(event.SearchQuery(workbook=workbook_rid))
|
|
391
|
+
if data_review_rid is not None:
|
|
392
|
+
queries.append(event.SearchQuery(data_review=data_review_rid))
|
|
393
|
+
if assignee_rid is not None:
|
|
394
|
+
queries.append(event.SearchQuery(assignee=assignee_rid))
|
|
395
|
+
if event_type is not None:
|
|
396
|
+
queries.append(event.SearchQuery(event_type=event_type))
|
|
397
|
+
if origin_types is not None:
|
|
398
|
+
origin_type_filter = event.OriginTypesFilter(api.SetOperator.OR, list(origin_types))
|
|
399
|
+
queries.append(event.SearchQuery(origin_types=origin_type_filter))
|
|
400
|
+
if workspace_rid is not None:
|
|
401
|
+
queries.append(event.SearchQuery(workspace=workspace_rid))
|
|
402
|
+
|
|
403
|
+
return event.SearchQuery(and_=queries)
|
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, Literal, Mapping, Protocol, Sequence, TypeAlias
|
|
7
|
+
from typing import Iterable, Literal, Mapping, Protocol, Sequence, TypeAlias
|
|
8
8
|
|
|
9
9
|
from nominal_api import (
|
|
10
10
|
event,
|
|
@@ -16,13 +16,21 @@ from nominal_api import (
|
|
|
16
16
|
from typing_extensions import Self
|
|
17
17
|
|
|
18
18
|
from nominal.core._clientsbunch import HasScoutParams
|
|
19
|
-
from nominal.core.
|
|
19
|
+
from nominal.core._event_types import EventType, SearchEventOriginType
|
|
20
|
+
from nominal.core._utils.api_tools import (
|
|
21
|
+
HasRid,
|
|
22
|
+
Link,
|
|
23
|
+
LinkDict,
|
|
24
|
+
RefreshableMixin,
|
|
25
|
+
create_links,
|
|
26
|
+
rid_from_instance_or_string,
|
|
27
|
+
)
|
|
20
28
|
from nominal.core._utils.pagination_tools import search_runs_by_asset_paginated
|
|
21
29
|
from nominal.core.attachment import Attachment, _iter_get_attachments
|
|
22
30
|
from nominal.core.connection import Connection, _get_connections
|
|
23
31
|
from nominal.core.dataset import Dataset, _create_dataset, _DatasetWrapper, _get_datasets
|
|
24
32
|
from nominal.core.datasource import DataSource
|
|
25
|
-
from nominal.core.event import Event,
|
|
33
|
+
from nominal.core.event import Event, _create_event, _search_events
|
|
26
34
|
from nominal.core.video import Video, _create_video, _get_video
|
|
27
35
|
from nominal.ts import IntegralNanosecondsDuration, IntegralNanosecondsUTC, _SecondsNanos
|
|
28
36
|
|
|
@@ -38,6 +46,14 @@ def _filter_scopes(
|
|
|
38
46
|
return [scope for scope in scopes if scope.data_source.type.lower() == scope_type]
|
|
39
47
|
|
|
40
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
|
+
|
|
41
57
|
@dataclass(frozen=True)
|
|
42
58
|
class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
43
59
|
rid: str
|
|
@@ -80,13 +96,9 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
80
96
|
def _list_dataset_scopes(self) -> Sequence[scout_asset_api.DataScope]:
|
|
81
97
|
return _filter_scopes(self._get_latest_api().data_scopes, "dataset")
|
|
82
98
|
|
|
83
|
-
def
|
|
99
|
+
def _scope_rids(self, scope_type: ScopeTypeSpecifier) -> Mapping[str, str]:
|
|
84
100
|
asset = self._get_latest_api()
|
|
85
|
-
return
|
|
86
|
-
scope.data_scope_name: cast(str, getattr(scope.data_source, stype))
|
|
87
|
-
for scope in asset.data_scopes
|
|
88
|
-
if scope.data_source.type.lower() == stype
|
|
89
|
-
}
|
|
101
|
+
return _filter_scope_rids(asset.data_scopes, scope_type)
|
|
90
102
|
|
|
91
103
|
def update(
|
|
92
104
|
self,
|
|
@@ -153,23 +165,25 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
153
165
|
"""
|
|
154
166
|
return (*self.list_datasets(), *self.list_connections(), *self.list_videos())
|
|
155
167
|
|
|
156
|
-
def
|
|
168
|
+
def remove_data_scopes(
|
|
157
169
|
self,
|
|
158
170
|
*,
|
|
159
|
-
|
|
160
|
-
|
|
171
|
+
names: Sequence[str] | None = None,
|
|
172
|
+
scopes: Sequence[ScopeType | str] | None = None,
|
|
161
173
|
) -> None:
|
|
162
|
-
|
|
163
|
-
data_sources = data_sources or []
|
|
164
|
-
|
|
165
|
-
if isinstance(data_sources, str):
|
|
166
|
-
raise RuntimeError("Expect `data_sources` to be a sequence, not a string")
|
|
174
|
+
"""Remove data scopes from this asset.
|
|
167
175
|
|
|
168
|
-
|
|
176
|
+
Args:
|
|
177
|
+
names: Names of datascopes to remove
|
|
178
|
+
scopes: Rids or instances of scope types (dataset, video, connection) to remove.
|
|
179
|
+
"""
|
|
180
|
+
scope_names_to_remove = names or []
|
|
181
|
+
data_scopes_to_remove = scopes or []
|
|
169
182
|
|
|
183
|
+
scope_rids_to_remove = {rid_from_instance_or_string(ds) for ds in data_scopes_to_remove}
|
|
170
184
|
conjure_asset = self._get_latest_api()
|
|
171
185
|
|
|
172
|
-
|
|
186
|
+
data_scopes_to_keep = [
|
|
173
187
|
scout_asset_api.CreateAssetDataScope(
|
|
174
188
|
data_scope_name=ds.data_scope_name,
|
|
175
189
|
data_source=ds.data_source,
|
|
@@ -177,31 +191,21 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
177
191
|
offset=ds.offset,
|
|
178
192
|
)
|
|
179
193
|
for ds in conjure_asset.data_scopes
|
|
180
|
-
if ds.data_scope_name not in
|
|
181
|
-
and (
|
|
194
|
+
if ds.data_scope_name not in scope_names_to_remove
|
|
195
|
+
and all(
|
|
196
|
+
rid not in scope_rids_to_remove
|
|
197
|
+
for rid in (ds.data_source.dataset, ds.data_source.connection, ds.data_source.video)
|
|
198
|
+
)
|
|
182
199
|
]
|
|
183
200
|
|
|
184
|
-
|
|
201
|
+
updated_asset = self._clients.assets.update_asset(
|
|
185
202
|
self._clients.auth_header,
|
|
186
203
|
scout_asset_api.UpdateAssetRequest(
|
|
187
|
-
data_scopes=
|
|
204
|
+
data_scopes=data_scopes_to_keep,
|
|
188
205
|
),
|
|
189
206
|
self.rid,
|
|
190
207
|
)
|
|
191
|
-
self._refresh_from_api(
|
|
192
|
-
|
|
193
|
-
def remove_data_scopes(
|
|
194
|
-
self,
|
|
195
|
-
*,
|
|
196
|
-
names: Sequence[str] | None = None,
|
|
197
|
-
scopes: Sequence[ScopeType | str] | None = None,
|
|
198
|
-
) -> None:
|
|
199
|
-
"""Remove data scopes from this asset.
|
|
200
|
-
|
|
201
|
-
`names` are scope names.
|
|
202
|
-
`scopes` are rids or scope objects.
|
|
203
|
-
"""
|
|
204
|
-
self._remove_data_sources(data_scope_names=names, data_sources=scopes)
|
|
208
|
+
self._refresh_from_api(updated_asset)
|
|
205
209
|
|
|
206
210
|
def add_dataset(
|
|
207
211
|
self,
|
|
@@ -382,6 +386,46 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
382
386
|
labels=labels,
|
|
383
387
|
)
|
|
384
388
|
|
|
389
|
+
def create_run(
|
|
390
|
+
self,
|
|
391
|
+
name: str,
|
|
392
|
+
start: datetime.datetime | IntegralNanosecondsUTC,
|
|
393
|
+
end: datetime.datetime | IntegralNanosecondsUTC | None,
|
|
394
|
+
*,
|
|
395
|
+
description: str | None = None,
|
|
396
|
+
properties: Mapping[str, str] | None = None,
|
|
397
|
+
labels: Sequence[str] = (),
|
|
398
|
+
links: Sequence[str | Link | LinkDict] = (),
|
|
399
|
+
attachments: Iterable[Attachment] | Iterable[str] = (),
|
|
400
|
+
) -> Run:
|
|
401
|
+
"""Create a run associated with this Asset for a given span of time.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
name: Name of the run.
|
|
405
|
+
start: Starting timestamp of the run.
|
|
406
|
+
end: Ending timestamp of the run, or None for an unbounded run.
|
|
407
|
+
description: Optionally, a human readable description of the run to create.
|
|
408
|
+
properties: Key-value pairs to use as properties on the created run.
|
|
409
|
+
labels: Sequence of labels to use on the created run.
|
|
410
|
+
links: Link metadata to add to the created run.
|
|
411
|
+
attachments: Attachments to associate with the created run.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
Returns the created run
|
|
415
|
+
"""
|
|
416
|
+
return _create_run(
|
|
417
|
+
self._clients,
|
|
418
|
+
name=name,
|
|
419
|
+
start=start,
|
|
420
|
+
end=end,
|
|
421
|
+
description=description,
|
|
422
|
+
properties=properties,
|
|
423
|
+
labels=labels,
|
|
424
|
+
links=links,
|
|
425
|
+
attachments=attachments,
|
|
426
|
+
asset_rids=[self.rid],
|
|
427
|
+
)
|
|
428
|
+
|
|
385
429
|
def get_dataset(self, data_scope_name: str) -> Dataset:
|
|
386
430
|
"""Retrieve a dataset by data scope name, or raise ValueError if one is not found."""
|
|
387
431
|
dataset = self.get_data_scope(data_scope_name)
|
|
@@ -410,7 +454,7 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
410
454
|
"""List the datasets associated with this asset.
|
|
411
455
|
Returns (data_scope_name, dataset) pairs for each dataset.
|
|
412
456
|
"""
|
|
413
|
-
scope_rid = self.
|
|
457
|
+
scope_rid = self._scope_rids(scope_type="dataset")
|
|
414
458
|
if not scope_rid:
|
|
415
459
|
return []
|
|
416
460
|
|
|
@@ -428,7 +472,7 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
428
472
|
"""List the connections associated with this asset.
|
|
429
473
|
Returns (data_scope_name, connection) pairs for each connection.
|
|
430
474
|
"""
|
|
431
|
-
scope_rid = self.
|
|
475
|
+
scope_rid = self._scope_rids(scope_type="connection")
|
|
432
476
|
connections_meta = _get_connections(self._clients, list(scope_rid.values()))
|
|
433
477
|
return [
|
|
434
478
|
(scope, Connection._from_conjure(self._clients, connection))
|
|
@@ -439,7 +483,7 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
439
483
|
"""List the videos associated with this asset.
|
|
440
484
|
Returns (data_scope_name, dataset) pairs for each video.
|
|
441
485
|
"""
|
|
442
|
-
scope_rid = self.
|
|
486
|
+
scope_rid = self._scope_rids(scope_type="video")
|
|
443
487
|
return [
|
|
444
488
|
(scope, Video._from_conjure(self._clients, _get_video(self._clients, rid)))
|
|
445
489
|
for (scope, rid) in scope_rid.items()
|
|
@@ -477,6 +521,7 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
477
521
|
data_review_rid: str | None = None,
|
|
478
522
|
assignee_rid: str | None = None,
|
|
479
523
|
event_type: EventType | None = None,
|
|
524
|
+
origin_types: Iterable[SearchEventOriginType] | None = None,
|
|
480
525
|
) -> Sequence[Event]:
|
|
481
526
|
"""Search for events associated with this Asset. See nominal.core.event._search_events for details."""
|
|
482
527
|
return _search_events(
|
|
@@ -492,6 +537,7 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
492
537
|
data_review_rid=data_review_rid,
|
|
493
538
|
assignee_rid=assignee_rid,
|
|
494
539
|
event_type=event_type,
|
|
540
|
+
origin_types=origin_types,
|
|
495
541
|
)
|
|
496
542
|
|
|
497
543
|
def remove_attachments(self, attachments: Iterable[Attachment] | Iterable[str]) -> None:
|
|
@@ -528,4 +574,4 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
528
574
|
|
|
529
575
|
|
|
530
576
|
# Moving to bottom to deal with circular dependencies
|
|
531
|
-
from nominal.core.run import Run # noqa: E402
|
|
577
|
+
from nominal.core.run import Run, _create_run # noqa: E402
|
nominal/core/client.py
CHANGED
|
@@ -36,6 +36,7 @@ from nominal._utils.deprecation_tools import warn_on_deprecated_argument
|
|
|
36
36
|
from nominal.config import NominalConfig, _config
|
|
37
37
|
from nominal.core._clientsbunch import ClientsBunch
|
|
38
38
|
from nominal.core._constants import DEFAULT_API_BASE_URL
|
|
39
|
+
from nominal.core._event_types import EventType
|
|
39
40
|
from nominal.core._utils.api_tools import (
|
|
40
41
|
Link,
|
|
41
42
|
LinkDict,
|
|
@@ -43,7 +44,6 @@ from nominal.core._utils.api_tools import (
|
|
|
43
44
|
rid_from_instance_or_string,
|
|
44
45
|
)
|
|
45
46
|
from nominal.core._utils.multipart import (
|
|
46
|
-
path_upload_name,
|
|
47
47
|
upload_multipart_io,
|
|
48
48
|
)
|
|
49
49
|
from nominal.core._utils.pagination_tools import (
|
|
@@ -91,8 +91,8 @@ from nominal.core.dataset import (
|
|
|
91
91
|
_get_datasets,
|
|
92
92
|
)
|
|
93
93
|
from nominal.core.datasource import DataSource
|
|
94
|
-
from nominal.core.event import Event,
|
|
95
|
-
from nominal.core.exceptions import NominalConfigError, NominalError,
|
|
94
|
+
from nominal.core.event import Event, _create_event, _search_events
|
|
95
|
+
from nominal.core.exceptions import NominalConfigError, NominalError, NominalMethodRemovedError
|
|
96
96
|
from nominal.core.filetype import FileType, FileTypes
|
|
97
97
|
from nominal.core.run import Run, _create_run
|
|
98
98
|
from nominal.core.secret import Secret
|
|
@@ -732,7 +732,7 @@ class NominalClient:
|
|
|
732
732
|
|
|
733
733
|
return dataset
|
|
734
734
|
|
|
735
|
-
def
|
|
735
|
+
def create_video(
|
|
736
736
|
self,
|
|
737
737
|
name: str,
|
|
738
738
|
*,
|
|
@@ -762,6 +762,8 @@ class NominalClient:
|
|
|
762
762
|
)
|
|
763
763
|
return Video._from_conjure(self._clients, response)
|
|
764
764
|
|
|
765
|
+
create_empty_video = create_video
|
|
766
|
+
|
|
765
767
|
def get_video(self, rid: str) -> Video:
|
|
766
768
|
"""Retrieve a video by its RID."""
|
|
767
769
|
response = self._clients.video.get(self._clients.auth_header, rid)
|
|
@@ -957,6 +959,10 @@ class NominalClient:
|
|
|
957
959
|
response = self._clients.connection.get_connection(self._clients.auth_header, rid)
|
|
958
960
|
return Connection._from_conjure(self._clients, response)
|
|
959
961
|
|
|
962
|
+
@deprecated(
|
|
963
|
+
"`create_video_from_mcap` is deprecated and will be removed in a future version. "
|
|
964
|
+
"Create a new video with `create_video` and then `add_mcap` to upload a file to the video."
|
|
965
|
+
)
|
|
960
966
|
def create_video_from_mcap(
|
|
961
967
|
self,
|
|
962
968
|
path: Path | str,
|
|
@@ -977,18 +983,14 @@ class NominalClient:
|
|
|
977
983
|
if name is None:
|
|
978
984
|
name = path.name
|
|
979
985
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
name=name,
|
|
984
|
-
topic=topic,
|
|
985
|
-
file_type=FileTypes.MCAP,
|
|
986
|
-
description=description,
|
|
987
|
-
labels=labels,
|
|
988
|
-
properties=properties,
|
|
989
|
-
file_name=path_upload_name(path, FileTypes.MCAP),
|
|
990
|
-
)
|
|
986
|
+
video = self.create_video(name, description=description, labels=labels, properties=properties)
|
|
987
|
+
video.add_mcap(path, topic, description)
|
|
988
|
+
return video
|
|
991
989
|
|
|
990
|
+
@deprecated(
|
|
991
|
+
"`create_video_from_mcap_io` is deprecated and will be removed in a future version. "
|
|
992
|
+
"Create a new video with `create_video` and then `add_mcap_from_io` to upload a file to the video."
|
|
993
|
+
)
|
|
992
994
|
def create_video_from_mcap_io(
|
|
993
995
|
self,
|
|
994
996
|
mcap: BinaryIO,
|
|
@@ -1007,40 +1009,9 @@ class NominalClient:
|
|
|
1007
1009
|
|
|
1008
1010
|
If name is None, the name of the file will be used.
|
|
1009
1011
|
"""
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
if file_name is None:
|
|
1014
|
-
file_name = name
|
|
1015
|
-
|
|
1016
|
-
file_type = FileType(*file_type)
|
|
1017
|
-
s3_path = upload_multipart_io(
|
|
1018
|
-
self._clients.auth_header, self._clients.workspace_rid, mcap, file_name, file_type, self._clients.upload
|
|
1019
|
-
)
|
|
1020
|
-
request = ingest_api.IngestRequest(
|
|
1021
|
-
options=ingest_api.IngestOptions(
|
|
1022
|
-
video=ingest_api.VideoOpts(
|
|
1023
|
-
source=ingest_api.IngestSource(s3=ingest_api.S3IngestSource(s3_path)),
|
|
1024
|
-
target=ingest_api.VideoIngestTarget(
|
|
1025
|
-
new=ingest_api.NewVideoIngestDestination(
|
|
1026
|
-
title=name,
|
|
1027
|
-
description=description,
|
|
1028
|
-
properties={} if properties is None else dict(properties),
|
|
1029
|
-
labels=list(labels),
|
|
1030
|
-
workspace=self._clients.workspace_rid,
|
|
1031
|
-
marking_rids=[],
|
|
1032
|
-
)
|
|
1033
|
-
),
|
|
1034
|
-
timestamp_manifest=scout_video_api.VideoFileTimestampManifest(
|
|
1035
|
-
mcap=scout_video_api.McapTimestampManifest(api.McapChannelLocator(topic=topic))
|
|
1036
|
-
),
|
|
1037
|
-
)
|
|
1038
|
-
)
|
|
1039
|
-
)
|
|
1040
|
-
response = self._clients.ingest.ingest(self._clients.auth_header, request)
|
|
1041
|
-
if response.details.video is None:
|
|
1042
|
-
raise NominalIngestError("error ingesting mcap video: no video created")
|
|
1043
|
-
return self.get_video(response.details.video.video_rid)
|
|
1012
|
+
video = self.create_video(name, description=description, labels=labels, properties=properties)
|
|
1013
|
+
video.add_mcap_from_io(mcap, file_name or name, topic, description, file_type)
|
|
1014
|
+
return video
|
|
1044
1015
|
|
|
1045
1016
|
def create_streaming_connection(
|
|
1046
1017
|
self,
|
|
@@ -1514,7 +1485,7 @@ class NominalClient:
|
|
|
1514
1485
|
properties: A mapping of key-value pairs that must ALL be present on an workbook to be included.
|
|
1515
1486
|
created_by: Searches for workbook templates with the given creator's rid
|
|
1516
1487
|
archived: Searches for workbook templates that are archived if true
|
|
1517
|
-
published: Searches
|
|
1488
|
+
published: Searches for workbook templates that have been published if true
|
|
1518
1489
|
|
|
1519
1490
|
Returns:
|
|
1520
1491
|
All workbook templates which match all of the provided conditions
|
nominal/core/event.py
CHANGED
|
@@ -3,16 +3,18 @@ from __future__ import annotations
|
|
|
3
3
|
import warnings
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from datetime import datetime, timedelta
|
|
6
|
-
from enum import Enum
|
|
7
6
|
from typing import Iterable, Mapping, Protocol, Sequence
|
|
8
7
|
|
|
9
|
-
from nominal_api import
|
|
8
|
+
from nominal_api import event
|
|
10
9
|
from typing_extensions import Self
|
|
11
10
|
|
|
12
11
|
from nominal.core import asset as core_asset
|
|
13
12
|
from nominal.core._clientsbunch import HasScoutParams
|
|
13
|
+
from nominal.core._event_types import EventType as EventType # noqa: PLC0414
|
|
14
|
+
from nominal.core._event_types import SearchEventOriginType as SearchEventOriginType # noqa: PLC0414
|
|
14
15
|
from nominal.core._utils.api_tools import HasRid, RefreshableMixin, rid_from_instance_or_string
|
|
15
16
|
from nominal.core._utils.pagination_tools import search_events_paginated
|
|
17
|
+
from nominal.core._utils.query_tools import _create_search_events_query
|
|
16
18
|
from nominal.ts import IntegralNanosecondsDuration, IntegralNanosecondsUTC, _SecondsNanos, _to_api_duration
|
|
17
19
|
|
|
18
20
|
|
|
@@ -124,39 +126,6 @@ class Event(HasRid, RefreshableMixin[event.Event]):
|
|
|
124
126
|
)
|
|
125
127
|
|
|
126
128
|
|
|
127
|
-
class EventType(Enum):
|
|
128
|
-
INFO = "INFO"
|
|
129
|
-
FLAG = "FLAG"
|
|
130
|
-
ERROR = "ERROR"
|
|
131
|
-
SUCCESS = "SUCCESS"
|
|
132
|
-
UNKNOWN = "UNKNOWN"
|
|
133
|
-
|
|
134
|
-
@classmethod
|
|
135
|
-
def from_api_event_type(cls, event: event.EventType) -> EventType:
|
|
136
|
-
if event.name == "INFO":
|
|
137
|
-
return cls.INFO
|
|
138
|
-
elif event.name == "FLAG":
|
|
139
|
-
return cls.FLAG
|
|
140
|
-
elif event.name == "ERROR":
|
|
141
|
-
return cls.ERROR
|
|
142
|
-
elif event.name == "SUCCESS":
|
|
143
|
-
return cls.SUCCESS
|
|
144
|
-
else:
|
|
145
|
-
return cls.UNKNOWN
|
|
146
|
-
|
|
147
|
-
def _to_api_event_type(self) -> event.EventType:
|
|
148
|
-
if self.name == "INFO":
|
|
149
|
-
return event.EventType.INFO
|
|
150
|
-
elif self.name == "FLAG":
|
|
151
|
-
return event.EventType.FLAG
|
|
152
|
-
elif self.name == "ERROR":
|
|
153
|
-
return event.EventType.ERROR
|
|
154
|
-
elif self.name == "SUCCESS":
|
|
155
|
-
return event.EventType.SUCCESS
|
|
156
|
-
else:
|
|
157
|
-
return event.EventType.UNKNOWN
|
|
158
|
-
|
|
159
|
-
|
|
160
129
|
def _create_event(
|
|
161
130
|
clients: Event._Clients,
|
|
162
131
|
*,
|
|
@@ -203,6 +172,7 @@ def _search_events(
|
|
|
203
172
|
data_review_rid: str | None = None,
|
|
204
173
|
assignee_rid: str | None = None,
|
|
205
174
|
event_type: EventType | None = None,
|
|
175
|
+
origin_types: Iterable[SearchEventOriginType] | None = None,
|
|
206
176
|
workspace_rid: str | None = None,
|
|
207
177
|
) -> Sequence[Event]:
|
|
208
178
|
query = _create_search_events_query(
|
|
@@ -216,53 +186,10 @@ def _search_events(
|
|
|
216
186
|
workbook_rid=workbook_rid,
|
|
217
187
|
data_review_rid=data_review_rid,
|
|
218
188
|
assignee_rid=assignee_rid,
|
|
219
|
-
event_type=event_type,
|
|
189
|
+
event_type=event_type._to_api_event_type() if event_type else None,
|
|
190
|
+
origin_types=[origin_type._to_api_search_event_origin_type() for origin_type in origin_types]
|
|
191
|
+
if origin_types
|
|
192
|
+
else None,
|
|
220
193
|
workspace_rid=workspace_rid,
|
|
221
194
|
)
|
|
222
195
|
return list(_iter_search_events(clients, query))
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def _create_search_events_query( # noqa: PLR0912
|
|
226
|
-
search_text: str | None = None,
|
|
227
|
-
after: str | datetime | IntegralNanosecondsUTC | None = None,
|
|
228
|
-
before: str | datetime | IntegralNanosecondsUTC | None = None,
|
|
229
|
-
asset_rids: Iterable[str] | None = None,
|
|
230
|
-
labels: Iterable[str] | None = None,
|
|
231
|
-
properties: Mapping[str, str] | None = None,
|
|
232
|
-
created_by_rid: str | None = None,
|
|
233
|
-
workbook_rid: str | None = None,
|
|
234
|
-
data_review_rid: str | None = None,
|
|
235
|
-
assignee_rid: str | None = None,
|
|
236
|
-
event_type: EventType | None = None,
|
|
237
|
-
workspace_rid: str | None = None,
|
|
238
|
-
) -> event.SearchQuery:
|
|
239
|
-
queries = []
|
|
240
|
-
if search_text is not None:
|
|
241
|
-
queries.append(event.SearchQuery(search_text=search_text))
|
|
242
|
-
if after is not None:
|
|
243
|
-
queries.append(event.SearchQuery(after=_SecondsNanos.from_flexible(after).to_api()))
|
|
244
|
-
if before is not None:
|
|
245
|
-
queries.append(event.SearchQuery(before=_SecondsNanos.from_flexible(before).to_api()))
|
|
246
|
-
if asset_rids:
|
|
247
|
-
for asset in asset_rids:
|
|
248
|
-
queries.append(event.SearchQuery(asset=asset))
|
|
249
|
-
if labels:
|
|
250
|
-
for label in labels:
|
|
251
|
-
queries.append(event.SearchQuery(label=label))
|
|
252
|
-
if properties:
|
|
253
|
-
for name, value in properties.items():
|
|
254
|
-
queries.append(event.SearchQuery(property=api.Property(name=name, value=value)))
|
|
255
|
-
if created_by_rid:
|
|
256
|
-
queries.append(event.SearchQuery(created_by=created_by_rid))
|
|
257
|
-
if workbook_rid is not None:
|
|
258
|
-
queries.append(event.SearchQuery(workbook=workbook_rid))
|
|
259
|
-
if data_review_rid is not None:
|
|
260
|
-
queries.append(event.SearchQuery(data_review=data_review_rid))
|
|
261
|
-
if assignee_rid is not None:
|
|
262
|
-
queries.append(event.SearchQuery(assignee=assignee_rid))
|
|
263
|
-
if event_type is not None:
|
|
264
|
-
queries.append(event.SearchQuery(event_type=event_type._to_api_event_type()))
|
|
265
|
-
if workspace_rid is not None:
|
|
266
|
-
queries.append(event.SearchQuery(workspace=workspace_rid))
|
|
267
|
-
|
|
268
|
-
return event.SearchQuery(and_=queries)
|
nominal/core/run.py
CHANGED
|
@@ -13,6 +13,7 @@ from typing_extensions import Self
|
|
|
13
13
|
|
|
14
14
|
from nominal.core import asset as core_asset
|
|
15
15
|
from nominal.core._clientsbunch import HasScoutParams
|
|
16
|
+
from nominal.core._event_types import EventType
|
|
16
17
|
from nominal.core._utils.api_tools import (
|
|
17
18
|
HasRid,
|
|
18
19
|
Link,
|
|
@@ -25,7 +26,7 @@ from nominal.core.asset import _filter_scopes
|
|
|
25
26
|
from nominal.core.attachment import Attachment, _iter_get_attachments
|
|
26
27
|
from nominal.core.connection import Connection, _get_connections
|
|
27
28
|
from nominal.core.dataset import Dataset, _DatasetWrapper, _get_datasets
|
|
28
|
-
from nominal.core.event import Event,
|
|
29
|
+
from nominal.core.event import Event, _create_event
|
|
29
30
|
from nominal.core.video import Video, _get_video
|
|
30
31
|
from nominal.ts import IntegralNanosecondsDuration, IntegralNanosecondsUTC, _SecondsNanos, _to_api_duration
|
|
31
32
|
|
|
@@ -372,11 +373,13 @@ class Run(HasRid, RefreshableMixin[scout_run_api.Run], _DatasetWrapper):
|
|
|
372
373
|
def archive(self) -> None:
|
|
373
374
|
"""Archive this run.
|
|
374
375
|
Archived runs are not deleted, but are hidden from the UI.
|
|
375
|
-
|
|
376
|
-
NOTE: currently, it is not possible (yet) to unarchive a run once archived.
|
|
377
376
|
"""
|
|
378
377
|
self._clients.run.archive_run(self._clients.auth_header, self.rid)
|
|
379
378
|
|
|
379
|
+
def unarchive(self) -> None:
|
|
380
|
+
"""Unarchive this run, allowing it to appear on the UI."""
|
|
381
|
+
self._clients.run.unarchive_run(self._clients.auth_header, self.rid)
|
|
382
|
+
|
|
380
383
|
@classmethod
|
|
381
384
|
def _from_conjure(cls, clients: _Clients, run: scout_run_api.Run) -> Self:
|
|
382
385
|
return cls(
|
|
@@ -2,8 +2,9 @@ import json
|
|
|
2
2
|
import logging
|
|
3
3
|
import re
|
|
4
4
|
import uuid
|
|
5
|
+
from datetime import datetime, timedelta
|
|
5
6
|
from pathlib import Path
|
|
6
|
-
from typing import Any, BinaryIO, Mapping, Sequence, TypeVar, Union, cast, overload
|
|
7
|
+
from typing import Any, BinaryIO, Iterable, Mapping, Sequence, TypeVar, Union, cast, overload
|
|
7
8
|
|
|
8
9
|
import requests
|
|
9
10
|
from conjure_python_client import ConjureBeanType, ConjureEnumType, ConjureUnionType
|
|
@@ -11,7 +12,21 @@ from conjure_python_client._serde.decoder import ConjureDecoder
|
|
|
11
12
|
from conjure_python_client._serde.encoder import ConjureEncoder
|
|
12
13
|
from nominal_api import scout_layout_api, scout_template_api, scout_workbookcommon_api
|
|
13
14
|
|
|
14
|
-
from nominal.core import
|
|
15
|
+
from nominal.core import (
|
|
16
|
+
Asset,
|
|
17
|
+
Dataset,
|
|
18
|
+
DatasetFile,
|
|
19
|
+
Event,
|
|
20
|
+
FileType,
|
|
21
|
+
NominalClient,
|
|
22
|
+
Workbook,
|
|
23
|
+
WorkbookTemplate,
|
|
24
|
+
)
|
|
25
|
+
from nominal.core._event_types import EventType, SearchEventOriginType
|
|
26
|
+
from nominal.ts import (
|
|
27
|
+
IntegralNanosecondsDuration,
|
|
28
|
+
IntegralNanosecondsUTC,
|
|
29
|
+
)
|
|
15
30
|
|
|
16
31
|
logger = logging.getLogger(__name__)
|
|
17
32
|
|
|
@@ -278,7 +293,9 @@ def copy_workbook_template_from(
|
|
|
278
293
|
Returns:
|
|
279
294
|
The newly created WorkbookTemplate in the target workspace.
|
|
280
295
|
"""
|
|
281
|
-
log_extras = {
|
|
296
|
+
log_extras = {
|
|
297
|
+
"destination_client_workspace": destination_client.get_workspace(destination_client._clients.workspace_rid).rid
|
|
298
|
+
}
|
|
282
299
|
logger.debug(
|
|
283
300
|
"Cloning workbook template: %s (rid: %s)", source_template.title, source_template.rid, extra=log_extras
|
|
284
301
|
)
|
|
@@ -315,7 +332,7 @@ def copy_workbook_template_from(
|
|
|
315
332
|
layout=new_template_layout,
|
|
316
333
|
content=new_workbook_content,
|
|
317
334
|
commit_message="Cloned from template",
|
|
318
|
-
workspace_rid=destination_client.get_workspace().rid,
|
|
335
|
+
workspace_rid=destination_client.get_workspace(destination_client._clients.workspace_rid).rid,
|
|
319
336
|
)
|
|
320
337
|
logger.debug(
|
|
321
338
|
"New workbook template created %s from %s (rid: %s)",
|
|
@@ -418,7 +435,9 @@ def copy_dataset_from(
|
|
|
418
435
|
Returns:
|
|
419
436
|
The newly created Dataset in the destination client.
|
|
420
437
|
"""
|
|
421
|
-
log_extras = {
|
|
438
|
+
log_extras = {
|
|
439
|
+
"destination_client_workspace": destination_client.get_workspace(destination_client._clients.workspace_rid).rid
|
|
440
|
+
}
|
|
422
441
|
logger.debug(
|
|
423
442
|
"Copying dataset %s (rid: %s)",
|
|
424
443
|
source_dataset.name,
|
|
@@ -438,6 +457,72 @@ def copy_dataset_from(
|
|
|
438
457
|
return new_dataset
|
|
439
458
|
|
|
440
459
|
|
|
460
|
+
def clone_event(source_event: Event, destination_client: NominalClient) -> Event:
|
|
461
|
+
"""Clones an event, maintaining all properties and linked assets.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
source_event (Event): The event to copy from.
|
|
465
|
+
destination_client (NominalClient): The destination client.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
The cloned event.
|
|
469
|
+
"""
|
|
470
|
+
return copy_event_from(source_event=source_event, destination_client=destination_client)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def copy_event_from(
|
|
474
|
+
source_event: Event,
|
|
475
|
+
destination_client: NominalClient,
|
|
476
|
+
*,
|
|
477
|
+
new_name: str | None = None,
|
|
478
|
+
new_type: EventType | None = None,
|
|
479
|
+
new_start: datetime | IntegralNanosecondsUTC | None = None,
|
|
480
|
+
new_duration: timedelta | IntegralNanosecondsDuration = timedelta(),
|
|
481
|
+
new_description: str | None = None,
|
|
482
|
+
new_assets: Iterable[Asset | str] = (),
|
|
483
|
+
new_properties: Mapping[str, str] | None = None,
|
|
484
|
+
new_labels: Iterable[str] = (),
|
|
485
|
+
) -> Event:
|
|
486
|
+
"""Copy an event from the source to the destination client.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
source_event: The source Event to copy.
|
|
490
|
+
destination_client: The NominalClient to create the copied event in.
|
|
491
|
+
new_name: Optional new name for the copied event. If not provided, the original name is used.
|
|
492
|
+
new_type: Optional new type for the copied event. If not provided, the original type is used.
|
|
493
|
+
new_start: Optional new start time for the copied event. If not provided, the original start time is used.
|
|
494
|
+
new_duration: Optional new duration for the copied event. If not provided, the original duration is used.
|
|
495
|
+
new_description: Optional new description for the copied event. If not provided, the original description used.
|
|
496
|
+
new_assets: Optional new assets for the copied event. If not provided, the original assets are used.
|
|
497
|
+
new_properties: Optional new properties for the copied event. If not provided, the original properties are used.
|
|
498
|
+
new_labels: Optional new labels for the copied event. If not provided, the original labels are used.
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
The newly created Event in the destination client.
|
|
502
|
+
"""
|
|
503
|
+
log_extras = {
|
|
504
|
+
"destination_client_workspace": destination_client.get_workspace(destination_client._clients.workspace_rid).rid
|
|
505
|
+
}
|
|
506
|
+
logger.debug(
|
|
507
|
+
"Copying event %s (rid: %s)",
|
|
508
|
+
source_event.name,
|
|
509
|
+
source_event.rid,
|
|
510
|
+
extra=log_extras,
|
|
511
|
+
)
|
|
512
|
+
new_event = destination_client.create_event(
|
|
513
|
+
name=new_name or source_event.name,
|
|
514
|
+
type=new_type or source_event.type,
|
|
515
|
+
start=new_start or source_event.start,
|
|
516
|
+
duration=new_duration or source_event.duration,
|
|
517
|
+
description=new_description or source_event.description,
|
|
518
|
+
assets=new_assets or source_event.asset_rids,
|
|
519
|
+
properties=new_properties or source_event.properties,
|
|
520
|
+
labels=new_labels or source_event.labels,
|
|
521
|
+
)
|
|
522
|
+
logger.debug("New event created: %s (rid: %s)", new_event.name, new_event.rid, extra=log_extras)
|
|
523
|
+
return new_event
|
|
524
|
+
|
|
525
|
+
|
|
441
526
|
def clone_asset(
|
|
442
527
|
source_asset: Asset,
|
|
443
528
|
destination_client: NominalClient,
|
|
@@ -451,7 +536,9 @@ def clone_asset(
|
|
|
451
536
|
Returns:
|
|
452
537
|
The newly created Asset in the target client.
|
|
453
538
|
"""
|
|
454
|
-
return copy_asset_from(
|
|
539
|
+
return copy_asset_from(
|
|
540
|
+
source_asset=source_asset, destination_client=destination_client, include_data=True, include_events=True
|
|
541
|
+
)
|
|
455
542
|
|
|
456
543
|
|
|
457
544
|
def copy_asset_from(
|
|
@@ -463,6 +550,7 @@ def copy_asset_from(
|
|
|
463
550
|
new_asset_properties: dict[str, Any] | None = None,
|
|
464
551
|
new_asset_labels: Sequence[str] | None = None,
|
|
465
552
|
include_data: bool = False,
|
|
553
|
+
include_events: bool = False,
|
|
466
554
|
) -> Asset:
|
|
467
555
|
"""Copy an asset from the source to the destination client.
|
|
468
556
|
|
|
@@ -474,11 +562,14 @@ def copy_asset_from(
|
|
|
474
562
|
new_asset_properties: Optional new properties for the copied asset. If not provided, original properties used.
|
|
475
563
|
new_asset_labels: Optional new labels for the copied asset. If not provided, the original labels are used.
|
|
476
564
|
include_data: Whether to include data in the copied asset.
|
|
565
|
+
include_events: Whether to include events in the copied dataset.
|
|
477
566
|
|
|
478
567
|
Returns:
|
|
479
568
|
The new asset created.
|
|
480
569
|
"""
|
|
481
|
-
log_extras = {
|
|
570
|
+
log_extras = {
|
|
571
|
+
"destination_client_workspace": destination_client.get_workspace(destination_client._clients.workspace_rid).rid
|
|
572
|
+
}
|
|
482
573
|
logger.debug("Copying asset %s (rid: %s)", source_asset.name, source_asset.rid, extra=log_extras)
|
|
483
574
|
new_asset = destination_client.create_asset(
|
|
484
575
|
name=new_asset_name if new_asset_name is not None else source_asset.name,
|
|
@@ -496,6 +587,14 @@ def copy_asset_from(
|
|
|
496
587
|
)
|
|
497
588
|
new_datasets.append(new_dataset)
|
|
498
589
|
new_asset.add_dataset(data_scope, new_dataset)
|
|
590
|
+
|
|
591
|
+
if include_events:
|
|
592
|
+
source_events = source_asset.search_events(origin_types=SearchEventOriginType.get_manual_origin_types())
|
|
593
|
+
new_events = []
|
|
594
|
+
for source_event in source_events:
|
|
595
|
+
new_event = copy_event_from(source_event, destination_client, new_assets=[new_asset])
|
|
596
|
+
new_events.append(new_event)
|
|
597
|
+
|
|
499
598
|
logger.debug("New asset created: %s (rid: %s)", new_asset, new_asset.rid, extra=log_extras)
|
|
500
599
|
return new_asset
|
|
501
600
|
|
|
@@ -518,7 +617,7 @@ def copy_resources_to_destination_client(
|
|
|
518
617
|
All of the created resources.
|
|
519
618
|
"""
|
|
520
619
|
log_extras = {
|
|
521
|
-
"destination_client_workspace": destination_client.get_workspace().rid,
|
|
620
|
+
"destination_client_workspace": destination_client.get_workspace(destination_client._clients.workspace_rid).rid,
|
|
522
621
|
}
|
|
523
622
|
|
|
524
623
|
if len(source_assets) != 1:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
CHANGELOG.md,sha256=
|
|
1
|
+
CHANGELOG.md,sha256=VVSixbZ3ayFrWHceTSkabfHQYIkY1HU3GRSszsywUXQ,85986
|
|
2
2
|
LICENSE,sha256=zEGHG9mjDjaIS3I79O8mweQo-yiTbqx8jJvUPppVAwk,1067
|
|
3
3
|
README.md,sha256=KKe0dxh_pHXCtB7I9G4qWGQYvot_BZU8yW6MJyuyUHM,311
|
|
4
4
|
nominal/__init__.py,sha256=rbraORnXUrNn1hywLXM0XwSQCd9UmQt20PDYlsBalfE,2167
|
|
@@ -27,26 +27,27 @@ nominal/cli/util/global_decorators.py,sha256=SBxhz4KbMlWDcCV08feouftd3HLnBNR-JJt
|
|
|
27
27
|
nominal/cli/util/verify_connection.py,sha256=KU17ejaDfKBLmLiZ3MZSVLyfrqNE7c6mFBvskhqQLCo,1902
|
|
28
28
|
nominal/config/__init__.py,sha256=wV8cq8X3J4NTJ5H_uR5THaMT_NQpWQO5qCUGEb-rPnM,3157
|
|
29
29
|
nominal/config/_config.py,sha256=yKq_H1iYJDoxRfLz2iXLbbVdoL0MTEY0FS4eVL12w0g,2004
|
|
30
|
-
nominal/core/__init__.py,sha256=
|
|
30
|
+
nominal/core/__init__.py,sha256=1MiCC44cxHYFofP4hf2fz4EIkepK-OAhDzpPFIzHbWw,2422
|
|
31
31
|
nominal/core/_clientsbunch.py,sha256=YwciugX7rQ9AOPHyvKuavG7b9SlX1PURRquP37nvLqE,8458
|
|
32
32
|
nominal/core/_constants.py,sha256=SrxgaSqAEB1MvTSrorgGam3eO29iCmRr6VIdajxX3gI,56
|
|
33
|
-
nominal/core/
|
|
33
|
+
nominal/core/_event_types.py,sha256=Cq_8x-zv_5EDvRo9UTbaOpenAy92bTfQxlsEuHPOhtE,3706
|
|
34
|
+
nominal/core/asset.py,sha256=-hNMGXiU1dPWfrzmOngbab-Hf6vfq2Rm_j0FP-woJ-s,23120
|
|
34
35
|
nominal/core/attachment.py,sha256=iJaDyF6JXsKxxBLA03I0WMmQF8U0bA-wRwvXMEhfWLU,4284
|
|
35
36
|
nominal/core/bounds.py,sha256=742BWmGL3FBryRAjoiJRg2N6aVinjYkQLxN7kfnJ40Q,581
|
|
36
37
|
nominal/core/channel.py,sha256=dbe8wpfMiWqHu98x66w6GOmC9Ro33Wv9AhBVx2DvtVk,18970
|
|
37
38
|
nominal/core/checklist.py,sha256=rO1RPDYV3o2miPKF7DcCiYpj6bUN-sdtZNhJkXzkfYE,7110
|
|
38
|
-
nominal/core/client.py,sha256=
|
|
39
|
+
nominal/core/client.py,sha256=34AhkJmnftU1dumVg80jPv9fnP7W5mQrkgOR8zI0VoU,68291
|
|
39
40
|
nominal/core/connection.py,sha256=ySbPN_a2takVa8wIU9mK4fB6vYLyZnN-qSmXVkLUxAY,5157
|
|
40
41
|
nominal/core/containerized_extractors.py,sha256=fUz3-NHoNWYKqOCD15gLwGXDKVfdsW-x_kpXnkOI3BE,10224
|
|
41
42
|
nominal/core/data_review.py,sha256=bEnRsd8LI4x9YOBPcF2H3h5-e12A7Gh8gQfsNUAZmPQ,7922
|
|
42
43
|
nominal/core/dataset.py,sha256=Rt20H2ekUbF0_YyF-OkJhs3KaRTqQzNNxyneRjIEOJk,46627
|
|
43
44
|
nominal/core/dataset_file.py,sha256=oENANJ17A4K63cZ8Fr7lUm_kVPyA4fL2rUsZ3oXXk2U,16396
|
|
44
45
|
nominal/core/datasource.py,sha256=k13B6u6uw5pd49SuVM3gXtATgqO_BUnqGUMGiiW6Moc,16920
|
|
45
|
-
nominal/core/event.py,sha256=
|
|
46
|
+
nominal/core/event.py,sha256=8trZXyuAqRlKedgcqSgDIimXAAJBmEfDLyHkOOBwUC0,7762
|
|
46
47
|
nominal/core/exceptions.py,sha256=GUpwXRgdYamLl6684FE8ttCRHkBx6WEhOZ3NPE-ybD4,2671
|
|
47
48
|
nominal/core/filetype.py,sha256=uzKe4iNHSv27mvz8-5EJEsvGOn3msEm_IhCj8OsCAPY,5526
|
|
48
49
|
nominal/core/log.py,sha256=z3hI3CIEyMwpUSWjwBsJ6a3JNGzBbsmrVusSU6uI7CY,3885
|
|
49
|
-
nominal/core/run.py,sha256=
|
|
50
|
+
nominal/core/run.py,sha256=IqXCP24UhdHKkss0LbXU_zAhx-7Pf2MIQI-lic_-quw,17987
|
|
50
51
|
nominal/core/secret.py,sha256=Ckq48m60i7rktxL9GY-nxHU5v8gHv9F1-JN7_MSf4bM,2863
|
|
51
52
|
nominal/core/unit.py,sha256=Wa-Bvu0hD-nzxVaQJSnn5YqAfnhUd2kWw2SswXnbMHY,3161
|
|
52
53
|
nominal/core/user.py,sha256=FV333TN4pQzcLh5b2CfxvBnnXyB1TrOP8Ppx1-XdaiE,481
|
|
@@ -67,7 +68,7 @@ nominal/core/_utils/multipart.py,sha256=0dA2XcTHuOQIyS0139O8WZiCjwePaD1sYDUmTgmW
|
|
|
67
68
|
nominal/core/_utils/multipart_downloader.py,sha256=16OJEPqxCwOnfjptYdrlwQVuSUQYoe9_iiW60ZSjWos,13859
|
|
68
69
|
nominal/core/_utils/networking.py,sha256=n9ZqYtnpwPCjz9C-4eixsTkrhFh-DW6lknBJlHckHhg,8200
|
|
69
70
|
nominal/core/_utils/pagination_tools.py,sha256=cEBY1WiA1d3cWJEM0myYF_pX8JdQ_e-5asngVXrUc_Y,12152
|
|
70
|
-
nominal/core/_utils/query_tools.py,sha256=
|
|
71
|
+
nominal/core/_utils/query_tools.py,sha256=0AuIZPtxR_BgrjBjRjc8fPjPKa9zicCe1xT9znVB_RA,16137
|
|
71
72
|
nominal/core/_utils/queueing.py,sha256=3qljc7dFI1UahlKjCaRVybM4poMCV5SayjyRPyXcPxg,3654
|
|
72
73
|
nominal/exceptions/__init__.py,sha256=W2r_GWJkZQQ6t3HooFjGRdhIgJq3fBvRV7Yn6gseoO0,415
|
|
73
74
|
nominal/experimental/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -85,7 +86,7 @@ nominal/experimental/logging/click_log_handler.py,sha256=ANLf4IGgmh95V0kJlr756wQ
|
|
|
85
86
|
nominal/experimental/logging/nominal_log_handler.py,sha256=hyTxyjsvFnE7vtyrDJpunAqADHmXekNWALwxXPIJGCk,5120
|
|
86
87
|
nominal/experimental/logging/rich_log_handler.py,sha256=8yz_VtxNgJg2oiesnXz2iXoBvQrUP5pAsYkxknOXgXA,1231
|
|
87
88
|
nominal/experimental/migration/__init__.py,sha256=E2IgWJLwJ5bN6jbl8k5nHECKFx5aT11jKAzVYcyXn3o,460
|
|
88
|
-
nominal/experimental/migration/migration_utils.py,sha256=
|
|
89
|
+
nominal/experimental/migration/migration_utils.py,sha256=Xuu9NilMxZQU_o8tqOn9WfM25Yz720HteiyN5L9Bbhs,26792
|
|
89
90
|
nominal/experimental/rust_streaming/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
90
91
|
nominal/experimental/rust_streaming/rust_write_stream.py,sha256=E-L5JtcwPWnCEm0o4_k-AVzw173sRSgElzKrgHoYwbs,1490
|
|
91
92
|
nominal/experimental/stream_v2/__init__.py,sha256=W39vK46pssx5sXvmsImMuJiEPs7iGtwrbYBI0bWnXCY,2313
|
|
@@ -104,8 +105,8 @@ nominal/thirdparty/polars/polars_export_handler.py,sha256=hGCSwXX9dC4MG01CmmjlTb
|
|
|
104
105
|
nominal/thirdparty/tdms/__init__.py,sha256=6n2ImFr2Wiil6JM1P5Q7Mpr0VzLcnDkmup_ftNpPq-s,142
|
|
105
106
|
nominal/thirdparty/tdms/_tdms.py,sha256=eiHFTUviyDPDClckNldjs_jTTSH_sdmboKDq0oIGChQ,8711
|
|
106
107
|
nominal/ts/__init__.py,sha256=hmd0ENvDhxRnzDKGLxIub6QG8LpcxCgcyAct029CaEs,21442
|
|
107
|
-
nominal-1.
|
|
108
|
-
nominal-1.
|
|
109
|
-
nominal-1.
|
|
110
|
-
nominal-1.
|
|
111
|
-
nominal-1.
|
|
108
|
+
nominal-1.102.0.dist-info/METADATA,sha256=vO-itznAaNKlS3Awvzo8S81LTu7iPKIyLrxEi2y5ZFs,2277
|
|
109
|
+
nominal-1.102.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
110
|
+
nominal-1.102.0.dist-info/entry_points.txt,sha256=-mCLhxgg9R_lm5efT7vW9wuBH12izvY322R0a3TYxbE,66
|
|
111
|
+
nominal-1.102.0.dist-info/licenses/LICENSE,sha256=zEGHG9mjDjaIS3I79O8mweQo-yiTbqx8jJvUPppVAwk,1067
|
|
112
|
+
nominal-1.102.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|