nominal 1.100.0__py3-none-any.whl → 1.101.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 +17 -0
- nominal/core/_utils/query_tools.py +1 -49
- nominal/core/asset.py +94 -11
- nominal/core/client.py +107 -46
- nominal/core/dataset.py +284 -1
- nominal/core/datasource.py +6 -1
- nominal/core/event.py +115 -3
- nominal/core/filetype.py +2 -1
- nominal/core/run.py +80 -2
- nominal/experimental/rust_streaming/rust_write_stream.py +1 -3
- {nominal-1.100.0.dist-info → nominal-1.101.0.dist-info}/METADATA +2 -2
- {nominal-1.100.0.dist-info → nominal-1.101.0.dist-info}/RECORD +15 -15
- {nominal-1.100.0.dist-info → nominal-1.101.0.dist-info}/WHEEL +0 -0
- {nominal-1.100.0.dist-info → nominal-1.101.0.dist-info}/entry_points.txt +0 -0
- {nominal-1.100.0.dist-info → nominal-1.101.0.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.101.0](https://github.com/nominal-io/nominal-client/compare/v1.100.0...v1.101.0) (2025-12-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add search_events to asset ([#553](https://github.com/nominal-io/nominal-client/issues/553)) ([3d291b7](https://github.com/nominal-io/nominal-client/commit/3d291b7e4b851676de882ac88d7695e49e9da0d3))
|
|
9
|
+
* added avi video file type ([#552](https://github.com/nominal-io/nominal-client/issues/552)) ([84bd35f](https://github.com/nominal-io/nominal-client/commit/84bd35ff83aa87cf8f7a718b5d71c1ca1445e9e9))
|
|
10
|
+
* allow adding data directly to runs, assets ([#543](https://github.com/nominal-io/nominal-client/issues/543)) ([6630717](https://github.com/nominal-io/nominal-client/commit/6630717827a35d50ee6008ede14b9c8e355f239c))
|
|
11
|
+
* allow creating events on runs, assets, _create_event helper method ([#540](https://github.com/nominal-io/nominal-client/issues/540)) ([dc84028](https://github.com/nominal-io/nominal-client/commit/dc84028d78df256f50ba58879416bb3b5f8752ed))
|
|
12
|
+
* allow creating events on runs, assets, use helper method ([dc84028](https://github.com/nominal-io/nominal-client/commit/dc84028d78df256f50ba58879416bb3b5f8752ed))
|
|
13
|
+
* reusable helper method for creating runs, create multi-asset runs in client ([#539](https://github.com/nominal-io/nominal-client/issues/539)) ([3118b43](https://github.com/nominal-io/nominal-client/commit/3118b43be4df552eb7418ffed08ed0afafbe88f4))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* make rust streaming optional unless supported architecture ([#556](https://github.com/nominal-io/nominal-client/issues/556)) ([24a2b98](https://github.com/nominal-io/nominal-client/commit/24a2b98218d025affb171411a72ad80b2dd2dd87))
|
|
19
|
+
|
|
3
20
|
## [1.100.0](https://github.com/nominal-io/nominal-client/compare/v1.99.0...v1.100.0) (2025-12-19)
|
|
4
21
|
|
|
5
22
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Mapping, Sequence
|
|
5
5
|
|
|
6
6
|
from nominal_api import (
|
|
7
7
|
api,
|
|
8
8
|
authentication_api,
|
|
9
|
-
event,
|
|
10
9
|
ingest_api,
|
|
11
10
|
scout_asset_api,
|
|
12
11
|
scout_catalog,
|
|
@@ -19,7 +18,6 @@ from nominal_api import (
|
|
|
19
18
|
secrets_api,
|
|
20
19
|
)
|
|
21
20
|
|
|
22
|
-
from nominal.core.event import EventType
|
|
23
21
|
from nominal.ts import IntegralNanosecondsUTC, _SecondsNanos
|
|
24
22
|
|
|
25
23
|
|
|
@@ -198,52 +196,6 @@ def create_search_datasets_query(
|
|
|
198
196
|
return scout_catalog.SearchDatasetsQuery(and_=queries)
|
|
199
197
|
|
|
200
198
|
|
|
201
|
-
def create_search_events_query( # noqa: PLR0912
|
|
202
|
-
search_text: str | None = None,
|
|
203
|
-
after: str | datetime | IntegralNanosecondsUTC | None = None,
|
|
204
|
-
before: str | datetime | IntegralNanosecondsUTC | None = None,
|
|
205
|
-
assets: Iterable[str] | None = None,
|
|
206
|
-
labels: Iterable[str] | None = None,
|
|
207
|
-
properties: Mapping[str, str] | None = None,
|
|
208
|
-
created_by: str | None = None,
|
|
209
|
-
workbook: str | None = None,
|
|
210
|
-
data_review: str | None = None,
|
|
211
|
-
assignee: str | None = None,
|
|
212
|
-
event_type: EventType | None = None,
|
|
213
|
-
workspace_rid: str | None = None,
|
|
214
|
-
) -> event.SearchQuery:
|
|
215
|
-
queries = []
|
|
216
|
-
if search_text is not None:
|
|
217
|
-
queries.append(event.SearchQuery(search_text=search_text))
|
|
218
|
-
if after is not None:
|
|
219
|
-
queries.append(event.SearchQuery(after=_SecondsNanos.from_flexible(after).to_api()))
|
|
220
|
-
if before is not None:
|
|
221
|
-
queries.append(event.SearchQuery(before=_SecondsNanos.from_flexible(before).to_api()))
|
|
222
|
-
if assets:
|
|
223
|
-
for asset in assets:
|
|
224
|
-
queries.append(event.SearchQuery(asset=asset))
|
|
225
|
-
if labels:
|
|
226
|
-
for label in labels:
|
|
227
|
-
queries.append(event.SearchQuery(label=label))
|
|
228
|
-
if properties:
|
|
229
|
-
for name, value in properties.items():
|
|
230
|
-
queries.append(event.SearchQuery(property=api.Property(name=name, value=value)))
|
|
231
|
-
if created_by:
|
|
232
|
-
queries.append(event.SearchQuery(created_by=created_by))
|
|
233
|
-
if workbook is not None:
|
|
234
|
-
queries.append(event.SearchQuery(workbook=workbook))
|
|
235
|
-
if data_review is not None:
|
|
236
|
-
queries.append(event.SearchQuery(data_review=data_review))
|
|
237
|
-
if assignee is not None:
|
|
238
|
-
queries.append(event.SearchQuery(assignee=assignee))
|
|
239
|
-
if event_type is not None:
|
|
240
|
-
queries.append(event.SearchQuery(event_type=event_type._to_api_event_type()))
|
|
241
|
-
if workspace_rid is not None:
|
|
242
|
-
queries.append(event.SearchQuery(workspace=workspace_rid))
|
|
243
|
-
|
|
244
|
-
return event.SearchQuery(and_=queries)
|
|
245
|
-
|
|
246
|
-
|
|
247
199
|
def create_search_runs_query(
|
|
248
200
|
start: str | datetime | IntegralNanosecondsUTC | None = None,
|
|
249
201
|
end: str | datetime | IntegralNanosecondsUTC | None = None,
|
nominal/core/asset.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import datetime
|
|
3
4
|
import logging
|
|
4
5
|
from dataclasses import dataclass, field
|
|
5
6
|
from types import MappingProxyType
|
|
6
7
|
from typing import Iterable, Literal, Mapping, Protocol, Sequence, TypeAlias, cast
|
|
7
8
|
|
|
8
9
|
from nominal_api import (
|
|
10
|
+
event,
|
|
9
11
|
scout,
|
|
10
12
|
scout_asset_api,
|
|
11
13
|
scout_assets,
|
|
@@ -18,18 +20,26 @@ from nominal.core._utils.api_tools import HasRid, Link, RefreshableMixin, create
|
|
|
18
20
|
from nominal.core._utils.pagination_tools import search_runs_by_asset_paginated
|
|
19
21
|
from nominal.core.attachment import Attachment, _iter_get_attachments
|
|
20
22
|
from nominal.core.connection import Connection, _get_connections
|
|
21
|
-
from nominal.core.dataset import Dataset, _create_dataset, _get_datasets
|
|
23
|
+
from nominal.core.dataset import Dataset, _create_dataset, _DatasetWrapper, _get_datasets
|
|
22
24
|
from nominal.core.datasource import DataSource
|
|
25
|
+
from nominal.core.event import Event, EventType, _create_event, _search_events
|
|
23
26
|
from nominal.core.video import Video, _create_video, _get_video
|
|
24
|
-
from nominal.ts import IntegralNanosecondsUTC, _SecondsNanos
|
|
27
|
+
from nominal.ts import IntegralNanosecondsDuration, IntegralNanosecondsUTC, _SecondsNanos
|
|
25
28
|
|
|
26
29
|
ScopeType: TypeAlias = Connection | Dataset | Video
|
|
30
|
+
ScopeTypeSpecifier: TypeAlias = Literal["connection", "dataset", "video"]
|
|
27
31
|
|
|
28
32
|
logger = logging.getLogger(__name__)
|
|
29
33
|
|
|
30
34
|
|
|
35
|
+
def _filter_scopes(
|
|
36
|
+
scopes: Sequence[scout_asset_api.DataScope], scope_type: ScopeTypeSpecifier
|
|
37
|
+
) -> Sequence[scout_asset_api.DataScope]:
|
|
38
|
+
return [scope for scope in scopes if scope.data_source.type.lower() == scope_type]
|
|
39
|
+
|
|
40
|
+
|
|
31
41
|
@dataclass(frozen=True)
|
|
32
|
-
class Asset(HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
42
|
+
class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
33
43
|
rid: str
|
|
34
44
|
name: str
|
|
35
45
|
description: str | None
|
|
@@ -43,6 +53,7 @@ class Asset(HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
43
53
|
DataSource._Clients,
|
|
44
54
|
Video._Clients,
|
|
45
55
|
Attachment._Clients,
|
|
56
|
+
Event._Clients,
|
|
46
57
|
HasScoutParams,
|
|
47
58
|
Protocol,
|
|
48
59
|
):
|
|
@@ -50,6 +61,8 @@ class Asset(HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
50
61
|
def assets(self) -> scout_assets.AssetService: ...
|
|
51
62
|
@property
|
|
52
63
|
def run(self) -> scout.RunService: ...
|
|
64
|
+
@property
|
|
65
|
+
def event(self) -> event.EventService: ...
|
|
53
66
|
|
|
54
67
|
@property
|
|
55
68
|
def nominal_url(self) -> str:
|
|
@@ -64,6 +77,17 @@ class Asset(HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
64
77
|
raise ValueError(f"multiple assets found with RID {self.rid!r}: {response!r}")
|
|
65
78
|
return response[self.rid]
|
|
66
79
|
|
|
80
|
+
def _list_dataset_scopes(self) -> Sequence[scout_asset_api.DataScope]:
|
|
81
|
+
return _filter_scopes(self._get_latest_api().data_scopes, "dataset")
|
|
82
|
+
|
|
83
|
+
def _scope_rid(self, stype: Literal["dataset", "video", "connection"]) -> dict[str, str]:
|
|
84
|
+
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
|
+
}
|
|
90
|
+
|
|
67
91
|
def update(
|
|
68
92
|
self,
|
|
69
93
|
*,
|
|
@@ -97,14 +121,6 @@ class Asset(HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
97
121
|
api_asset = self._clients.assets.update_asset(self._clients.auth_header, request, self.rid)
|
|
98
122
|
return self._refresh_from_api(api_asset)
|
|
99
123
|
|
|
100
|
-
def _scope_rid(self, stype: Literal["dataset", "video", "connection"]) -> dict[str, str]:
|
|
101
|
-
asset = self._get_latest_api()
|
|
102
|
-
return {
|
|
103
|
-
scope.data_scope_name: cast(str, getattr(scope.data_source, stype))
|
|
104
|
-
for scope in asset.data_scopes
|
|
105
|
-
if scope.data_source.type.lower() == stype
|
|
106
|
-
}
|
|
107
|
-
|
|
108
124
|
def promote(self) -> Self:
|
|
109
125
|
"""Promote this asset to be a standard, searchable, and displayable asset.
|
|
110
126
|
|
|
@@ -329,6 +345,43 @@ class Asset(HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
329
345
|
self.add_video(data_scope_name, video)
|
|
330
346
|
return video
|
|
331
347
|
|
|
348
|
+
def create_event(
|
|
349
|
+
self,
|
|
350
|
+
name: str,
|
|
351
|
+
type: EventType,
|
|
352
|
+
start: datetime.datetime | IntegralNanosecondsUTC,
|
|
353
|
+
duration: datetime.timedelta | IntegralNanosecondsDuration = 0,
|
|
354
|
+
*,
|
|
355
|
+
description: str | None = None,
|
|
356
|
+
properties: Mapping[str, str] | None = None,
|
|
357
|
+
labels: Sequence[str] | None = None,
|
|
358
|
+
) -> Event:
|
|
359
|
+
"""Create an event associated with this Asset at a given point in time.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
name: Name of the event
|
|
363
|
+
type: Verbosity level of the event.
|
|
364
|
+
start: Starting timestamp of the event
|
|
365
|
+
duration: Duration of the event, or 0 for an event without duration.
|
|
366
|
+
description: Optionally, a human readable description of the event to create
|
|
367
|
+
properties: Key-value pairs to use as properties on the created event
|
|
368
|
+
labels: Sequence of labels to use on the created event.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
The created event that is associated with the asset.
|
|
372
|
+
"""
|
|
373
|
+
return _create_event(
|
|
374
|
+
self._clients,
|
|
375
|
+
name=name,
|
|
376
|
+
type=type,
|
|
377
|
+
start=start,
|
|
378
|
+
duration=duration,
|
|
379
|
+
description=description,
|
|
380
|
+
assets=[self],
|
|
381
|
+
properties=properties,
|
|
382
|
+
labels=labels,
|
|
383
|
+
)
|
|
384
|
+
|
|
332
385
|
def get_dataset(self, data_scope_name: str) -> Dataset:
|
|
333
386
|
"""Retrieve a dataset by data scope name, or raise ValueError if one is not found."""
|
|
334
387
|
dataset = self.get_data_scope(data_scope_name)
|
|
@@ -411,6 +464,36 @@ class Asset(HasRid, RefreshableMixin[scout_asset_api.Asset]):
|
|
|
411
464
|
)
|
|
412
465
|
]
|
|
413
466
|
|
|
467
|
+
def search_events(
|
|
468
|
+
self,
|
|
469
|
+
*,
|
|
470
|
+
search_text: str | None = None,
|
|
471
|
+
after: str | datetime.datetime | IntegralNanosecondsUTC | None = None,
|
|
472
|
+
before: str | datetime.datetime | IntegralNanosecondsUTC | None = None,
|
|
473
|
+
labels: Iterable[str] | None = None,
|
|
474
|
+
properties: Mapping[str, str] | None = None,
|
|
475
|
+
created_by_rid: str | None = None,
|
|
476
|
+
workbook_rid: str | None = None,
|
|
477
|
+
data_review_rid: str | None = None,
|
|
478
|
+
assignee_rid: str | None = None,
|
|
479
|
+
event_type: EventType | None = None,
|
|
480
|
+
) -> Sequence[Event]:
|
|
481
|
+
"""Search for events associated with this Asset. See nominal.core.event._search_events for details."""
|
|
482
|
+
return _search_events(
|
|
483
|
+
self._clients,
|
|
484
|
+
search_text=search_text,
|
|
485
|
+
after=after,
|
|
486
|
+
before=before,
|
|
487
|
+
asset_rids=[self.rid],
|
|
488
|
+
labels=labels,
|
|
489
|
+
properties=properties,
|
|
490
|
+
created_by_rid=created_by_rid,
|
|
491
|
+
workbook_rid=workbook_rid,
|
|
492
|
+
data_review_rid=data_review_rid,
|
|
493
|
+
assignee_rid=assignee_rid,
|
|
494
|
+
event_type=event_type,
|
|
495
|
+
)
|
|
496
|
+
|
|
414
497
|
def remove_attachments(self, attachments: Iterable[Attachment] | Iterable[str]) -> None:
|
|
415
498
|
"""Remove attachments from this asset.
|
|
416
499
|
Does not remove the attachments from Nominal.
|
nominal/core/client.py
CHANGED
|
@@ -7,7 +7,7 @@ from dataclasses import dataclass, field
|
|
|
7
7
|
from datetime import datetime, timedelta
|
|
8
8
|
from io import TextIOBase
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import BinaryIO, Iterable, Mapping, Sequence
|
|
10
|
+
from typing import BinaryIO, Iterable, Mapping, Sequence, overload
|
|
11
11
|
|
|
12
12
|
import certifi
|
|
13
13
|
import conjure_python_client
|
|
@@ -16,7 +16,6 @@ from nominal_api import (
|
|
|
16
16
|
api,
|
|
17
17
|
attachments_api,
|
|
18
18
|
authentication_api,
|
|
19
|
-
event,
|
|
20
19
|
ingest_api,
|
|
21
20
|
scout_asset_api,
|
|
22
21
|
scout_catalog,
|
|
@@ -24,7 +23,6 @@ from nominal_api import (
|
|
|
24
23
|
scout_datasource_connection_api,
|
|
25
24
|
scout_layout_api,
|
|
26
25
|
scout_notebook_api,
|
|
27
|
-
scout_run_api,
|
|
28
26
|
scout_template_api,
|
|
29
27
|
scout_video_api,
|
|
30
28
|
scout_workbookcommon_api,
|
|
@@ -34,6 +32,7 @@ from nominal_api import (
|
|
|
34
32
|
from typing_extensions import Self, deprecated
|
|
35
33
|
|
|
36
34
|
from nominal import ts
|
|
35
|
+
from nominal._utils.deprecation_tools import warn_on_deprecated_argument
|
|
37
36
|
from nominal.config import NominalConfig, _config
|
|
38
37
|
from nominal.core._clientsbunch import ClientsBunch
|
|
39
38
|
from nominal.core._constants import DEFAULT_API_BASE_URL
|
|
@@ -41,7 +40,6 @@ from nominal.core._utils.api_tools import (
|
|
|
41
40
|
Link,
|
|
42
41
|
LinkDict,
|
|
43
42
|
construct_user_agent_string,
|
|
44
|
-
create_links,
|
|
45
43
|
rid_from_instance_or_string,
|
|
46
44
|
)
|
|
47
45
|
from nominal.core._utils.multipart import (
|
|
@@ -55,7 +53,6 @@ from nominal.core._utils.pagination_tools import (
|
|
|
55
53
|
search_checklists_paginated,
|
|
56
54
|
search_data_reviews_paginated,
|
|
57
55
|
search_datasets_paginated,
|
|
58
|
-
search_events_paginated,
|
|
59
56
|
search_runs_by_asset_paginated,
|
|
60
57
|
search_runs_paginated,
|
|
61
58
|
search_secrets_paginated,
|
|
@@ -69,7 +66,6 @@ from nominal.core._utils.query_tools import (
|
|
|
69
66
|
create_search_checklists_query,
|
|
70
67
|
create_search_containerized_extractors_query,
|
|
71
68
|
create_search_datasets_query,
|
|
72
|
-
create_search_events_query,
|
|
73
69
|
create_search_runs_query,
|
|
74
70
|
create_search_secrets_query,
|
|
75
71
|
create_search_users_query,
|
|
@@ -95,10 +91,10 @@ from nominal.core.dataset import (
|
|
|
95
91
|
_get_datasets,
|
|
96
92
|
)
|
|
97
93
|
from nominal.core.datasource import DataSource
|
|
98
|
-
from nominal.core.event import Event, EventType
|
|
94
|
+
from nominal.core.event import Event, EventType, _create_event, _search_events
|
|
99
95
|
from nominal.core.exceptions import NominalConfigError, NominalError, NominalIngestError, NominalMethodRemovedError
|
|
100
96
|
from nominal.core.filetype import FileType, FileTypes
|
|
101
|
-
from nominal.core.run import Run
|
|
97
|
+
from nominal.core.run import Run, _create_run
|
|
102
98
|
from nominal.core.secret import Secret
|
|
103
99
|
from nominal.core.unit import Unit, _available_units
|
|
104
100
|
from nominal.core.user import User
|
|
@@ -109,8 +105,6 @@ from nominal.core.workspace import Workspace
|
|
|
109
105
|
from nominal.ts import (
|
|
110
106
|
IntegralNanosecondsDuration,
|
|
111
107
|
IntegralNanosecondsUTC,
|
|
112
|
-
_SecondsNanos,
|
|
113
|
-
_to_api_duration,
|
|
114
108
|
_to_typed_timestamp_type,
|
|
115
109
|
)
|
|
116
110
|
|
|
@@ -492,6 +486,7 @@ class NominalClient:
|
|
|
492
486
|
)
|
|
493
487
|
return list(self._iter_search_videos(query))
|
|
494
488
|
|
|
489
|
+
@overload
|
|
495
490
|
def create_run(
|
|
496
491
|
self,
|
|
497
492
|
name: str,
|
|
@@ -503,24 +498,96 @@ class NominalClient:
|
|
|
503
498
|
labels: Sequence[str] = (),
|
|
504
499
|
links: Sequence[str | Link | LinkDict] = (),
|
|
505
500
|
attachments: Iterable[Attachment] | Iterable[str] = (),
|
|
501
|
+
) -> Run: ...
|
|
502
|
+
@overload
|
|
503
|
+
def create_run(
|
|
504
|
+
self,
|
|
505
|
+
name: str,
|
|
506
|
+
start: datetime | IntegralNanosecondsUTC,
|
|
507
|
+
end: datetime | IntegralNanosecondsUTC | None,
|
|
508
|
+
description: str | None = None,
|
|
509
|
+
*,
|
|
510
|
+
properties: Mapping[str, str] | None = None,
|
|
511
|
+
labels: Sequence[str] = (),
|
|
512
|
+
links: Sequence[str | Link | LinkDict] = (),
|
|
513
|
+
attachments: Iterable[Attachment] | Iterable[str] = (),
|
|
514
|
+
asset: Asset | str,
|
|
515
|
+
) -> Run: ...
|
|
516
|
+
@overload
|
|
517
|
+
def create_run(
|
|
518
|
+
self,
|
|
519
|
+
name: str,
|
|
520
|
+
start: datetime | IntegralNanosecondsUTC,
|
|
521
|
+
end: datetime | IntegralNanosecondsUTC | None,
|
|
522
|
+
description: str | None = None,
|
|
523
|
+
*,
|
|
524
|
+
properties: Mapping[str, str] | None = None,
|
|
525
|
+
labels: Sequence[str] = (),
|
|
526
|
+
links: Sequence[str | Link | LinkDict] = (),
|
|
527
|
+
attachments: Iterable[Attachment] | Iterable[str] = (),
|
|
528
|
+
assets: Sequence[Asset | str],
|
|
529
|
+
) -> Run: ...
|
|
530
|
+
@warn_on_deprecated_argument(
|
|
531
|
+
"asset", "The 'asset' parameter is deprecated and will be removed in a future release. Use 'assets' instead."
|
|
532
|
+
)
|
|
533
|
+
def create_run(
|
|
534
|
+
self,
|
|
535
|
+
name: str,
|
|
536
|
+
start: datetime | IntegralNanosecondsUTC,
|
|
537
|
+
end: datetime | IntegralNanosecondsUTC | None,
|
|
538
|
+
description: str | None = None,
|
|
539
|
+
*,
|
|
540
|
+
properties: Mapping[str, str] | None = None,
|
|
541
|
+
labels: Sequence[str] | None = None,
|
|
542
|
+
links: Sequence[str | Link | LinkDict] | None = None,
|
|
543
|
+
attachments: Iterable[Attachment] | Iterable[str] | None = None,
|
|
506
544
|
asset: Asset | str | None = None,
|
|
545
|
+
assets: Sequence[Asset | str] | None = None,
|
|
507
546
|
) -> Run:
|
|
508
|
-
"""Create a run.
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
properties
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
547
|
+
"""Create a run, which is is effectively a slice of time across a collection of assets and datasources.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
name: Name of the run to create
|
|
551
|
+
start: Starting timestamp of the run to create
|
|
552
|
+
end: Ending timestamp of the run to create, or None for an unbounded run.
|
|
553
|
+
description: Optional description of the run to create
|
|
554
|
+
properties: Optional key-value pairs to use as properties on the created run
|
|
555
|
+
labels: Optional sequence of labels for the created run
|
|
556
|
+
links: Link metadata to add to the created run
|
|
557
|
+
attachments: Attachments to associate with the created run
|
|
558
|
+
asset: Singular asset to associate with the run
|
|
559
|
+
NOTE: mutually exclusive with `assets`
|
|
560
|
+
NOTE: deprecated-- use `assets` instead.
|
|
561
|
+
assets: Sequence of assets to associate with the run
|
|
562
|
+
NOTE: mutually exclusive with `asset`
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
Reference to the created run object
|
|
566
|
+
|
|
567
|
+
Raises:
|
|
568
|
+
ValueError: both `asset` and `assets` provided
|
|
569
|
+
ConjureHTTPError: error making request
|
|
570
|
+
|
|
571
|
+
"""
|
|
572
|
+
if asset and assets:
|
|
573
|
+
raise ValueError("Only one of 'asset' and 'assets' may be provided")
|
|
574
|
+
elif asset:
|
|
575
|
+
assets = [asset]
|
|
576
|
+
elif assets is None:
|
|
577
|
+
assets = []
|
|
578
|
+
|
|
579
|
+
return _create_run(
|
|
580
|
+
self._clients,
|
|
581
|
+
name=name,
|
|
582
|
+
start=start,
|
|
583
|
+
end=end,
|
|
584
|
+
description=description,
|
|
585
|
+
properties=properties,
|
|
586
|
+
labels=labels,
|
|
587
|
+
links=links,
|
|
588
|
+
attachments=attachments,
|
|
589
|
+
asset_rids=[rid_from_instance_or_string(asset) for asset in assets],
|
|
521
590
|
)
|
|
522
|
-
response = self._clients.run.create_run(self._clients.auth_header, request)
|
|
523
|
-
return Run._from_conjure(self._clients, response)
|
|
524
591
|
|
|
525
592
|
def get_run(self, rid: str) -> Run:
|
|
526
593
|
"""Retrieve a run by its RID."""
|
|
@@ -1158,19 +1225,17 @@ class NominalClient:
|
|
|
1158
1225
|
properties: Mapping[str, str] | None = None,
|
|
1159
1226
|
labels: Iterable[str] = (),
|
|
1160
1227
|
) -> Event:
|
|
1161
|
-
|
|
1228
|
+
return _create_event(
|
|
1229
|
+
clients=self._clients,
|
|
1162
1230
|
name=name,
|
|
1231
|
+
type=type,
|
|
1232
|
+
start=start,
|
|
1233
|
+
duration=duration,
|
|
1163
1234
|
description=description,
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
origins=[],
|
|
1168
|
-
properties=dict(properties) if properties else {},
|
|
1169
|
-
labels=list(labels),
|
|
1170
|
-
type=type._to_api_event_type(),
|
|
1235
|
+
assets=assets,
|
|
1236
|
+
properties=properties,
|
|
1237
|
+
labels=labels,
|
|
1171
1238
|
)
|
|
1172
|
-
response = self._clients.event.create_event(self._clients.auth_header, request)
|
|
1173
|
-
return Event._from_conjure(self._clients, response)
|
|
1174
1239
|
|
|
1175
1240
|
def get_event(self, rid: str) -> Event:
|
|
1176
1241
|
events = self.get_events([rid])
|
|
@@ -1205,10 +1270,6 @@ class NominalClient:
|
|
|
1205
1270
|
# TODO (drake-nominal): Expose checklist_refs to users
|
|
1206
1271
|
return list(self._iter_search_data_reviews(assets, runs))
|
|
1207
1272
|
|
|
1208
|
-
def _iter_search_events(self, query: event.SearchQuery) -> Iterable[Event]:
|
|
1209
|
-
for e in search_events_paginated(self._clients.event, self._clients.auth_header, query):
|
|
1210
|
-
yield Event._from_conjure(self._clients, e)
|
|
1211
|
-
|
|
1212
1273
|
def search_events(
|
|
1213
1274
|
self,
|
|
1214
1275
|
*,
|
|
@@ -1251,21 +1312,21 @@ class NominalClient:
|
|
|
1251
1312
|
Returns:
|
|
1252
1313
|
All events which match all of the provided conditions
|
|
1253
1314
|
"""
|
|
1254
|
-
|
|
1315
|
+
return _search_events(
|
|
1316
|
+
clients=self._clients,
|
|
1255
1317
|
search_text=search_text,
|
|
1256
1318
|
after=after,
|
|
1257
1319
|
before=before,
|
|
1258
|
-
|
|
1320
|
+
asset_rids=[rid_from_instance_or_string(asset) for asset in assets] if assets else None,
|
|
1259
1321
|
labels=labels,
|
|
1260
1322
|
properties=properties,
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1323
|
+
created_by_rid=rid_from_instance_or_string(created_by) if created_by else None,
|
|
1324
|
+
workbook_rid=rid_from_instance_or_string(workbook) if workbook else None,
|
|
1325
|
+
data_review_rid=rid_from_instance_or_string(data_review) if data_review else None,
|
|
1326
|
+
assignee_rid=rid_from_instance_or_string(assignee) if assignee else None,
|
|
1265
1327
|
event_type=event_type,
|
|
1266
1328
|
workspace_rid=self._workspace_rid_for_search(workspace or WorkspaceSearchType.ALL),
|
|
1267
1329
|
)
|
|
1268
|
-
return list(self._iter_search_events(query))
|
|
1269
1330
|
|
|
1270
1331
|
def get_containerized_extractor(self, rid: str) -> ContainerizedExtractor:
|
|
1271
1332
|
return ContainerizedExtractor._from_conjure(
|
nominal/core/dataset.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import abc
|
|
3
4
|
import logging
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from datetime import timedelta
|
|
@@ -8,7 +9,7 @@ from pathlib import Path
|
|
|
8
9
|
from types import MappingProxyType
|
|
9
10
|
from typing import BinaryIO, Iterable, Mapping, Sequence, TypeAlias, overload
|
|
10
11
|
|
|
11
|
-
from nominal_api import api, ingest_api, scout_catalog
|
|
12
|
+
from nominal_api import api, ingest_api, scout_asset_api, scout_catalog
|
|
12
13
|
from typing_extensions import Self, deprecated
|
|
13
14
|
|
|
14
15
|
from nominal.core._stream.batch_processor import process_log_batch
|
|
@@ -646,6 +647,288 @@ class Dataset(DataSource, RefreshableMixin[scout_catalog.EnrichedDataset]):
|
|
|
646
647
|
)
|
|
647
648
|
|
|
648
649
|
|
|
650
|
+
def _unify_tags(datascope_tags: Mapping[str, str], provided_tags: Mapping[str, str] | None) -> Mapping[str, str]:
|
|
651
|
+
return {**datascope_tags, **(provided_tags or {})}
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
class _DatasetWrapper(abc.ABC):
|
|
655
|
+
"""A lightweight façade over `nominal.core.Dataset` that routes ingest calls through a *data scope*.
|
|
656
|
+
|
|
657
|
+
`_DatasetWrapper` resolves `data_scope_name` to a backing `nominal.core.Dataset` and then delegates to the
|
|
658
|
+
corresponding `Dataset` method.
|
|
659
|
+
|
|
660
|
+
How this differs from `Dataset`
|
|
661
|
+
-------------------------------
|
|
662
|
+
- All "add data" methods take an extra first argument, `data_scope_name`, which selects the target dataset.
|
|
663
|
+
- For methods that accept `tags`, this wrapper merges the scope's required tags into the provided tags.
|
|
664
|
+
User-provided tags take precedence on key collisions.
|
|
665
|
+
- Some formats cannot be safely tagged with scope tags; those wrapper methods raise `RuntimeError` when the selected
|
|
666
|
+
scope requires tags.
|
|
667
|
+
|
|
668
|
+
Subclasses must implement `_list_dataset_scopes`, which is used to resolve scopes.
|
|
669
|
+
"""
|
|
670
|
+
|
|
671
|
+
# static typing for required field
|
|
672
|
+
_clients: Dataset._Clients
|
|
673
|
+
|
|
674
|
+
@abc.abstractmethod
|
|
675
|
+
def _list_dataset_scopes(self) -> Sequence[scout_asset_api.DataScope]:
|
|
676
|
+
"""Return the data scopes available to this wrapper.
|
|
677
|
+
|
|
678
|
+
Subclasses provide the authoritative list of `scout_asset_api.DataScope` objects used to
|
|
679
|
+
resolve `data_scope_name` in wrapper methods.
|
|
680
|
+
"""
|
|
681
|
+
|
|
682
|
+
def _get_dataset_scope(self, data_scope_name: str) -> tuple[Dataset, Mapping[str, str]]:
|
|
683
|
+
"""Resolve a data scope name to its backing dataset and required series tags.
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
A tuple of the resolved `Dataset` and the scope's required `series_tags`.
|
|
687
|
+
|
|
688
|
+
Raises:
|
|
689
|
+
ValueError: If no scope exists with the given `data_scope_name`, or if the scope is not backed by a dataset.
|
|
690
|
+
"""
|
|
691
|
+
dataset_scopes = {scope.data_scope_name: scope for scope in self._list_dataset_scopes()}
|
|
692
|
+
data_scope = dataset_scopes.get(data_scope_name)
|
|
693
|
+
if data_scope is None:
|
|
694
|
+
raise ValueError(f"No such data scope found with data_scope_name {data_scope_name}")
|
|
695
|
+
elif data_scope.data_source.dataset is None:
|
|
696
|
+
raise ValueError(f"Datascope {data_scope_name} is not a dataset!")
|
|
697
|
+
|
|
698
|
+
dataset = Dataset._from_conjure(
|
|
699
|
+
self._clients,
|
|
700
|
+
_get_dataset(self._clients.auth_header, self._clients.catalog, data_scope.data_source.dataset),
|
|
701
|
+
)
|
|
702
|
+
return dataset, data_scope.series_tags
|
|
703
|
+
|
|
704
|
+
################
|
|
705
|
+
# Add Data API #
|
|
706
|
+
################
|
|
707
|
+
|
|
708
|
+
def add_tabular_data(
|
|
709
|
+
self,
|
|
710
|
+
data_scope_name: str,
|
|
711
|
+
path: Path | str,
|
|
712
|
+
*,
|
|
713
|
+
timestamp_column: str,
|
|
714
|
+
timestamp_type: _AnyTimestampType,
|
|
715
|
+
tag_columns: Mapping[str, str] | None = None,
|
|
716
|
+
tags: Mapping[str, str] | None = None,
|
|
717
|
+
) -> DatasetFile:
|
|
718
|
+
"""Append tabular data on-disk to the dataset selected by `data_scope_name`.
|
|
719
|
+
|
|
720
|
+
This method behaves like `nominal.core.Dataset.add_tabular_data`, except that the data scope's required
|
|
721
|
+
tags are merged into `tags` before ingest (with user-provided tags taking precedence on key collisions).
|
|
722
|
+
|
|
723
|
+
For supported file types, argument semantics, and return value details, see
|
|
724
|
+
`nominal.core.Dataset.add_tabular_data`.
|
|
725
|
+
"""
|
|
726
|
+
dataset, scope_tags = self._get_dataset_scope(data_scope_name)
|
|
727
|
+
return dataset.add_tabular_data(
|
|
728
|
+
path,
|
|
729
|
+
timestamp_column=timestamp_column,
|
|
730
|
+
timestamp_type=timestamp_type,
|
|
731
|
+
tag_columns=tag_columns,
|
|
732
|
+
tags=_unify_tags(scope_tags, tags),
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
def add_avro_stream(
|
|
736
|
+
self,
|
|
737
|
+
data_scope_name: str,
|
|
738
|
+
path: Path | str,
|
|
739
|
+
) -> DatasetFile:
|
|
740
|
+
"""Upload an avro stream file to the dataset selected by `data_scope_name`.
|
|
741
|
+
|
|
742
|
+
This method behaves like `nominal.core.Dataset.add_avro_stream`, with one important difference:
|
|
743
|
+
avro stream ingestion does not support applying scope tags. If the selected scope requires tags, this method
|
|
744
|
+
raises `RuntimeError` rather than ingesting (potentially) untagged data. This file may still be ingested
|
|
745
|
+
directly on the dataset itself if it is known to contain the correct set of tags.
|
|
746
|
+
|
|
747
|
+
For schema requirements and return value details, see
|
|
748
|
+
`nominal.core.Dataset.add_avro_stream`.
|
|
749
|
+
"""
|
|
750
|
+
dataset, scope_tags = self._get_dataset_scope(data_scope_name)
|
|
751
|
+
|
|
752
|
+
# TODO(drake): remove once avro stream supports ingest with tags
|
|
753
|
+
if scope_tags:
|
|
754
|
+
raise RuntimeError(
|
|
755
|
+
f"Cannot add avro files to datascope {data_scope_name}-- data would not get "
|
|
756
|
+
f"tagged with required tags: {scope_tags}"
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
return dataset.add_avro_stream(path)
|
|
760
|
+
|
|
761
|
+
def add_journal_json(
|
|
762
|
+
self,
|
|
763
|
+
data_scope_name: str,
|
|
764
|
+
path: Path | str,
|
|
765
|
+
) -> DatasetFile:
|
|
766
|
+
"""Add a journald json file to the dataset selected by `data_scope_name`.
|
|
767
|
+
|
|
768
|
+
This method behaves like `nominal.core.Dataset.add_journal_json`, with one important difference:
|
|
769
|
+
journal json ingestion does not support applying scope tags as args. If the selected scope requires tags,
|
|
770
|
+
this method raises `RuntimeError` rather than potentially ingesting untagged data. This file may still be
|
|
771
|
+
ingested directly on the dataset itself if it is known to contain the correct set of args.
|
|
772
|
+
|
|
773
|
+
For file expectations and return value details, see
|
|
774
|
+
`nominal.core.Dataset.add_journal_json`.
|
|
775
|
+
"""
|
|
776
|
+
dataset, scope_tags = self._get_dataset_scope(data_scope_name)
|
|
777
|
+
|
|
778
|
+
# TODO(drake): remove once journal json supports ingest with tags
|
|
779
|
+
if scope_tags:
|
|
780
|
+
raise RuntimeError(
|
|
781
|
+
f"Cannot add journal json files to datascope {data_scope_name}-- data would not get "
|
|
782
|
+
f"tagged with required arguments: {scope_tags}"
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
return dataset.add_journal_json(path)
|
|
786
|
+
|
|
787
|
+
def add_mcap(
|
|
788
|
+
self,
|
|
789
|
+
data_scope_name: str,
|
|
790
|
+
path: Path | str,
|
|
791
|
+
*,
|
|
792
|
+
include_topics: Iterable[str] | None = None,
|
|
793
|
+
exclude_topics: Iterable[str] | None = None,
|
|
794
|
+
) -> DatasetFile:
|
|
795
|
+
"""Add an MCAP file to the dataset selected by `data_scope_name`.
|
|
796
|
+
|
|
797
|
+
This method behaves like `nominal.core.Dataset.add_mcap`, with one important difference:
|
|
798
|
+
MCAP ingestion does not support applying scope tags. If the selected scope requires tags, this method raises
|
|
799
|
+
`RuntimeError` rather than ingesting untagged data.
|
|
800
|
+
|
|
801
|
+
For topic-filtering semantics and return value details, see
|
|
802
|
+
`nominal.core.Dataset.add_mcap`.
|
|
803
|
+
"""
|
|
804
|
+
dataset, scope_tags = self._get_dataset_scope(data_scope_name)
|
|
805
|
+
|
|
806
|
+
# TODO(drake): remove once MCAP supports ingest with tags
|
|
807
|
+
if scope_tags:
|
|
808
|
+
raise RuntimeError(
|
|
809
|
+
f"Cannot add mcap files to datascope {data_scope_name}-- data would not get "
|
|
810
|
+
f"tagged with required tags: {scope_tags}"
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
return dataset.add_mcap(path, include_topics=include_topics, exclude_topics=exclude_topics)
|
|
814
|
+
|
|
815
|
+
def add_ardupilot_dataflash(
|
|
816
|
+
self,
|
|
817
|
+
data_scope_name: str,
|
|
818
|
+
path: Path | str,
|
|
819
|
+
tags: Mapping[str, str] | None = None,
|
|
820
|
+
) -> DatasetFile:
|
|
821
|
+
"""Add a Dataflash file to the dataset selected by `data_scope_name`.
|
|
822
|
+
|
|
823
|
+
This method behaves like `nominal.core.Dataset.add_ardupilot_dataflash`, except that the data scope's
|
|
824
|
+
required tags are merged into `tags` before ingest (with user-provided tags taking precedence on key
|
|
825
|
+
collisions).
|
|
826
|
+
|
|
827
|
+
For file expectations and return value details, see
|
|
828
|
+
`nominal.core.Dataset.add_ardupilot_dataflash`.
|
|
829
|
+
"""
|
|
830
|
+
dataset, scope_tags = self._get_dataset_scope(data_scope_name)
|
|
831
|
+
return dataset.add_ardupilot_dataflash(path, tags=_unify_tags(scope_tags, tags))
|
|
832
|
+
|
|
833
|
+
@overload
|
|
834
|
+
def add_containerized(
|
|
835
|
+
self,
|
|
836
|
+
data_scope_name: str,
|
|
837
|
+
extractor: str | ContainerizedExtractor,
|
|
838
|
+
sources: Mapping[str, Path | str],
|
|
839
|
+
*,
|
|
840
|
+
tag: str | None = None,
|
|
841
|
+
tags: Mapping[str, str] | None = None,
|
|
842
|
+
) -> DatasetFile: ...
|
|
843
|
+
@overload
|
|
844
|
+
def add_containerized(
|
|
845
|
+
self,
|
|
846
|
+
data_scope_name: str,
|
|
847
|
+
extractor: str | ContainerizedExtractor,
|
|
848
|
+
sources: Mapping[str, Path | str],
|
|
849
|
+
*,
|
|
850
|
+
tag: str | None = None,
|
|
851
|
+
tags: Mapping[str, str] | None = None,
|
|
852
|
+
timestamp_column: str,
|
|
853
|
+
timestamp_type: _AnyTimestampType,
|
|
854
|
+
) -> DatasetFile: ...
|
|
855
|
+
def add_containerized(
|
|
856
|
+
self,
|
|
857
|
+
data_scope_name: str,
|
|
858
|
+
extractor: str | ContainerizedExtractor,
|
|
859
|
+
sources: Mapping[str, Path | str],
|
|
860
|
+
*,
|
|
861
|
+
tag: str | None = None,
|
|
862
|
+
tags: Mapping[str, str] | None = None,
|
|
863
|
+
timestamp_column: str | None = None,
|
|
864
|
+
timestamp_type: _AnyTimestampType | None = None,
|
|
865
|
+
) -> DatasetFile:
|
|
866
|
+
"""Add data from proprietary formats using a pre-registered custom extractor.
|
|
867
|
+
|
|
868
|
+
This method behaves like `nominal.core.Dataset.add_containerized`, except that the data scope's required
|
|
869
|
+
tags are merged into `tags` before ingest (with user-provided tags taking precedence on key collisions).
|
|
870
|
+
|
|
871
|
+
This wrapper also enforces that `timestamp_column` and `timestamp_type` are provided together (or omitted
|
|
872
|
+
together) before delegating.
|
|
873
|
+
|
|
874
|
+
For extractor inputs, tagging semantics, timestamp metadata behavior, and return value details, see
|
|
875
|
+
`nominal.core.Dataset.add_containerized`.
|
|
876
|
+
"""
|
|
877
|
+
dataset, scope_tags = self._get_dataset_scope(data_scope_name)
|
|
878
|
+
if timestamp_column is None and timestamp_type is None:
|
|
879
|
+
return dataset.add_containerized(
|
|
880
|
+
extractor,
|
|
881
|
+
sources,
|
|
882
|
+
tag=tag,
|
|
883
|
+
tags=_unify_tags(scope_tags, tags),
|
|
884
|
+
)
|
|
885
|
+
elif timestamp_column is not None and timestamp_type is not None:
|
|
886
|
+
return dataset.add_containerized(
|
|
887
|
+
extractor,
|
|
888
|
+
sources,
|
|
889
|
+
tag=tag,
|
|
890
|
+
tags=_unify_tags(scope_tags, tags),
|
|
891
|
+
timestamp_column=timestamp_column,
|
|
892
|
+
timestamp_type=timestamp_type,
|
|
893
|
+
)
|
|
894
|
+
else:
|
|
895
|
+
raise ValueError(
|
|
896
|
+
"Only one of `timestamp_column` and `timestamp_type` were provided to `add_containerized`, "
|
|
897
|
+
"either both must or neither must be provided."
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
def add_from_io(
|
|
901
|
+
self,
|
|
902
|
+
data_scope_name: str,
|
|
903
|
+
data_stream: BinaryIO,
|
|
904
|
+
file_type: tuple[str, str] | FileType,
|
|
905
|
+
*,
|
|
906
|
+
timestamp_column: str,
|
|
907
|
+
timestamp_type: _AnyTimestampType,
|
|
908
|
+
file_name: str | None = None,
|
|
909
|
+
tag_columns: Mapping[str, str] | None = None,
|
|
910
|
+
tags: Mapping[str, str] | None = None,
|
|
911
|
+
) -> DatasetFile:
|
|
912
|
+
"""Append to the dataset selected by `data_scope_name` from a file-like object.
|
|
913
|
+
|
|
914
|
+
This method behaves like `nominal.core.Dataset.add_from_io`, except that the data scope's required tags
|
|
915
|
+
are merged into `tags` before ingest (with user-provided tags taking precedence on key collisions).
|
|
916
|
+
|
|
917
|
+
For stream requirements, supported file types, argument semantics, and return value details, see
|
|
918
|
+
`nominal.core.Dataset.add_from_io`.
|
|
919
|
+
"""
|
|
920
|
+
dataset, scope_tags = self._get_dataset_scope(data_scope_name)
|
|
921
|
+
return dataset.add_from_io(
|
|
922
|
+
data_stream,
|
|
923
|
+
timestamp_column=timestamp_column,
|
|
924
|
+
timestamp_type=timestamp_type,
|
|
925
|
+
file_type=file_type,
|
|
926
|
+
file_name=file_name,
|
|
927
|
+
tag_columns=tag_columns,
|
|
928
|
+
tags=_unify_tags(scope_tags, tags),
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
|
|
649
932
|
@deprecated(
|
|
650
933
|
"poll_until_ingestion_completed() is deprecated and will be removed in a future release. "
|
|
651
934
|
"Instead, call poll_until_ingestion_completed() on individual DatasetFiles."
|
nominal/core/datasource.py
CHANGED
|
@@ -396,7 +396,12 @@ def _get_write_stream(
|
|
|
396
396
|
)
|
|
397
397
|
elif data_format == "rust_experimental":
|
|
398
398
|
# Delayed import intentionally in case of any issues with experimental and pre-compiled binaries
|
|
399
|
-
|
|
399
|
+
try:
|
|
400
|
+
from nominal.experimental.rust_streaming.rust_write_stream import RustWriteStream
|
|
401
|
+
except ImportError as ex:
|
|
402
|
+
raise ImportError(
|
|
403
|
+
"nominal-streaming is required to use get_write_stream with data_format='rust_experimental'"
|
|
404
|
+
) from ex
|
|
400
405
|
|
|
401
406
|
return RustWriteStream._from_datasource(
|
|
402
407
|
write_rid,
|
nominal/core/event.py
CHANGED
|
@@ -6,12 +6,13 @@ from datetime import datetime, timedelta
|
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from typing import Iterable, Mapping, Protocol, Sequence
|
|
8
8
|
|
|
9
|
-
from nominal_api import event
|
|
9
|
+
from nominal_api import api, event
|
|
10
10
|
from typing_extensions import Self
|
|
11
11
|
|
|
12
|
+
from nominal.core import asset as core_asset
|
|
12
13
|
from nominal.core._clientsbunch import HasScoutParams
|
|
13
14
|
from nominal.core._utils.api_tools import HasRid, RefreshableMixin, rid_from_instance_or_string
|
|
14
|
-
from nominal.core.
|
|
15
|
+
from nominal.core._utils.pagination_tools import search_events_paginated
|
|
15
16
|
from nominal.ts import IntegralNanosecondsDuration, IntegralNanosecondsUTC, _SecondsNanos, _to_api_duration
|
|
16
17
|
|
|
17
18
|
|
|
@@ -50,7 +51,7 @@ class Event(HasRid, RefreshableMixin[event.Event]):
|
|
|
50
51
|
*,
|
|
51
52
|
name: str | None = None,
|
|
52
53
|
description: str | None = None,
|
|
53
|
-
assets: Iterable[Asset | str] | None = None,
|
|
54
|
+
assets: Iterable[core_asset.Asset | str] | None = None,
|
|
54
55
|
start: datetime | IntegralNanosecondsUTC | None = None,
|
|
55
56
|
duration: timedelta | IntegralNanosecondsDuration | None = None,
|
|
56
57
|
properties: Mapping[str, str] | None = None,
|
|
@@ -154,3 +155,114 @@ class EventType(Enum):
|
|
|
154
155
|
return event.EventType.SUCCESS
|
|
155
156
|
else:
|
|
156
157
|
return event.EventType.UNKNOWN
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _create_event(
|
|
161
|
+
clients: Event._Clients,
|
|
162
|
+
*,
|
|
163
|
+
name: str,
|
|
164
|
+
type: EventType,
|
|
165
|
+
start: datetime | IntegralNanosecondsUTC,
|
|
166
|
+
duration: timedelta | IntegralNanosecondsDuration,
|
|
167
|
+
assets: Iterable[core_asset.Asset | str] | None,
|
|
168
|
+
description: str | None,
|
|
169
|
+
properties: Mapping[str, str] | None,
|
|
170
|
+
labels: Iterable[str] | None,
|
|
171
|
+
) -> Event:
|
|
172
|
+
request = event.CreateEvent(
|
|
173
|
+
name=name,
|
|
174
|
+
description=description,
|
|
175
|
+
asset_rids=[rid_from_instance_or_string(asset) for asset in (assets or [])],
|
|
176
|
+
timestamp=_SecondsNanos.from_flexible(start).to_api(),
|
|
177
|
+
duration=_to_api_duration(duration),
|
|
178
|
+
origins=[],
|
|
179
|
+
properties=dict(properties or {}),
|
|
180
|
+
labels=list(labels or []),
|
|
181
|
+
type=type._to_api_event_type(),
|
|
182
|
+
)
|
|
183
|
+
response = clients.event.create_event(clients.auth_header, request)
|
|
184
|
+
return Event._from_conjure(clients, response)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _iter_search_events(clients: Event._Clients, query: event.SearchQuery) -> Iterable[Event]:
|
|
188
|
+
for e in search_events_paginated(clients.event, clients.auth_header, query):
|
|
189
|
+
yield Event._from_conjure(clients, e)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _search_events(
|
|
193
|
+
clients: Event._Clients,
|
|
194
|
+
*,
|
|
195
|
+
search_text: str | None = None,
|
|
196
|
+
after: str | datetime | IntegralNanosecondsUTC | None = None,
|
|
197
|
+
before: str | datetime | IntegralNanosecondsUTC | None = None,
|
|
198
|
+
asset_rids: Iterable[str] | None = None,
|
|
199
|
+
labels: Iterable[str] | None = None,
|
|
200
|
+
properties: Mapping[str, str] | None = None,
|
|
201
|
+
created_by_rid: str | None = None,
|
|
202
|
+
workbook_rid: str | None = None,
|
|
203
|
+
data_review_rid: str | None = None,
|
|
204
|
+
assignee_rid: str | None = None,
|
|
205
|
+
event_type: EventType | None = None,
|
|
206
|
+
workspace_rid: str | None = None,
|
|
207
|
+
) -> Sequence[Event]:
|
|
208
|
+
query = _create_search_events_query(
|
|
209
|
+
asset_rids=asset_rids,
|
|
210
|
+
search_text=search_text,
|
|
211
|
+
after=after,
|
|
212
|
+
before=before,
|
|
213
|
+
labels=labels,
|
|
214
|
+
properties=properties,
|
|
215
|
+
created_by_rid=created_by_rid,
|
|
216
|
+
workbook_rid=workbook_rid,
|
|
217
|
+
data_review_rid=data_review_rid,
|
|
218
|
+
assignee_rid=assignee_rid,
|
|
219
|
+
event_type=event_type,
|
|
220
|
+
workspace_rid=workspace_rid,
|
|
221
|
+
)
|
|
222
|
+
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/filetype.py
CHANGED
|
@@ -111,6 +111,7 @@ class FileType(NamedTuple):
|
|
|
111
111
|
|
|
112
112
|
|
|
113
113
|
class FileTypes:
|
|
114
|
+
AVI: FileType = FileType(".avi", "video/x-msvideo")
|
|
114
115
|
AVRO_STREAM: FileType = FileType(".avro", "application/avro")
|
|
115
116
|
BINARY: FileType = FileType("", "application/octet-stream")
|
|
116
117
|
CSV: FileType = FileType(".csv", "text/csv")
|
|
@@ -134,4 +135,4 @@ class FileTypes:
|
|
|
134
135
|
_PARQUET_FILE_TYPES = (PARQUET_GZ, PARQUET)
|
|
135
136
|
_PARQUET_ARCHIVE_TYPES = (PARQUET_TAR_GZ, PARQUET_TAR, PARQUET_ZIP)
|
|
136
137
|
_JOURNAL_TYPES = (JOURNAL_JSONL, JOURNAL_JSONL_GZ)
|
|
137
|
-
_VIDEO_TYPES = (MKV, MP4, TS)
|
|
138
|
+
_VIDEO_TYPES = (AVI, MKV, MP4, TS)
|
nominal/core/run.py
CHANGED
|
@@ -6,6 +6,7 @@ from types import MappingProxyType
|
|
|
6
6
|
from typing import Iterable, Mapping, Protocol, Sequence, cast
|
|
7
7
|
|
|
8
8
|
from nominal_api import (
|
|
9
|
+
scout_asset_api,
|
|
9
10
|
scout_run_api,
|
|
10
11
|
)
|
|
11
12
|
from typing_extensions import Self
|
|
@@ -20,15 +21,17 @@ from nominal.core._utils.api_tools import (
|
|
|
20
21
|
create_links,
|
|
21
22
|
rid_from_instance_or_string,
|
|
22
23
|
)
|
|
24
|
+
from nominal.core.asset import _filter_scopes
|
|
23
25
|
from nominal.core.attachment import Attachment, _iter_get_attachments
|
|
24
26
|
from nominal.core.connection import Connection, _get_connections
|
|
25
|
-
from nominal.core.dataset import Dataset, _get_datasets
|
|
27
|
+
from nominal.core.dataset import Dataset, _DatasetWrapper, _get_datasets
|
|
28
|
+
from nominal.core.event import Event, EventType, _create_event
|
|
26
29
|
from nominal.core.video import Video, _get_video
|
|
27
30
|
from nominal.ts import IntegralNanosecondsDuration, IntegralNanosecondsUTC, _SecondsNanos, _to_api_duration
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
@dataclass(frozen=True)
|
|
31
|
-
class Run(HasRid, RefreshableMixin[scout_run_api.Run]):
|
|
34
|
+
class Run(HasRid, RefreshableMixin[scout_run_api.Run], _DatasetWrapper):
|
|
32
35
|
rid: str
|
|
33
36
|
name: str
|
|
34
37
|
description: str
|
|
@@ -96,6 +99,13 @@ class Run(HasRid, RefreshableMixin[scout_run_api.Run]):
|
|
|
96
99
|
updated_run = self._clients.run.update_run(self._clients.auth_header, request, self.rid)
|
|
97
100
|
return self._refresh_from_api(updated_run)
|
|
98
101
|
|
|
102
|
+
def _list_dataset_scopes(self) -> Sequence[scout_asset_api.DataScope]:
|
|
103
|
+
api_run = self._get_latest_api()
|
|
104
|
+
if len(api_run.assets) > 1:
|
|
105
|
+
raise RuntimeError("Can't retrieve dataset scopes on multi-asset runs")
|
|
106
|
+
|
|
107
|
+
return _filter_scopes(api_run.asset_data_scopes, "dataset")
|
|
108
|
+
|
|
99
109
|
def _list_datasource_rids(
|
|
100
110
|
self, datasource_type: str | None = None, property_name: str | None = None
|
|
101
111
|
) -> Mapping[str, str]:
|
|
@@ -148,6 +158,43 @@ class Run(HasRid, RefreshableMixin[scout_run_api.Run]):
|
|
|
148
158
|
)
|
|
149
159
|
self._refresh_from_api(updated_run)
|
|
150
160
|
|
|
161
|
+
def create_event(
|
|
162
|
+
self,
|
|
163
|
+
name: str,
|
|
164
|
+
type: EventType,
|
|
165
|
+
start: datetime | IntegralNanosecondsUTC,
|
|
166
|
+
duration: timedelta | IntegralNanosecondsDuration = 0,
|
|
167
|
+
*,
|
|
168
|
+
description: str | None = None,
|
|
169
|
+
properties: Mapping[str, str] | None = None,
|
|
170
|
+
labels: Iterable[str] = (),
|
|
171
|
+
) -> Event:
|
|
172
|
+
"""Create an event associated with all associated assets of this run at a given point in time.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
name: Name of the event
|
|
176
|
+
type: Verbosity level of the event.
|
|
177
|
+
start: Starting timestamp of the event
|
|
178
|
+
duration: Duration of the event, or 0 for an event without duration.
|
|
179
|
+
description: Optionally, a human readable description of the event to create
|
|
180
|
+
properties: Key-value pairs to use as properties on the created event
|
|
181
|
+
labels: Sequence of labels to use on the created event.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
The created event that is associated with all of the assets associated with this run..
|
|
185
|
+
"""
|
|
186
|
+
return _create_event(
|
|
187
|
+
self._clients,
|
|
188
|
+
name=name,
|
|
189
|
+
type=type,
|
|
190
|
+
start=start,
|
|
191
|
+
duration=duration,
|
|
192
|
+
description=description,
|
|
193
|
+
assets=self.assets,
|
|
194
|
+
properties=properties,
|
|
195
|
+
labels=labels,
|
|
196
|
+
)
|
|
197
|
+
|
|
151
198
|
def add_dataset(
|
|
152
199
|
self,
|
|
153
200
|
ref_name: str,
|
|
@@ -349,3 +396,34 @@ class Run(HasRid, RefreshableMixin[scout_run_api.Run]):
|
|
|
349
396
|
created_at=_SecondsNanos.from_flexible(run.created_at).to_nanoseconds(),
|
|
350
397
|
_clients=clients,
|
|
351
398
|
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _create_run(
|
|
402
|
+
clients: Run._Clients,
|
|
403
|
+
*,
|
|
404
|
+
name: str,
|
|
405
|
+
start: datetime | IntegralNanosecondsUTC,
|
|
406
|
+
end: datetime | IntegralNanosecondsUTC | None,
|
|
407
|
+
description: str | None,
|
|
408
|
+
properties: Mapping[str, str] | None,
|
|
409
|
+
labels: Sequence[str] | None,
|
|
410
|
+
links: Sequence[str | Link | LinkDict] | None,
|
|
411
|
+
attachments: Iterable[Attachment] | Iterable[str] | None,
|
|
412
|
+
asset_rids: Sequence[str] | None,
|
|
413
|
+
) -> Run:
|
|
414
|
+
"""Create a run."""
|
|
415
|
+
request = scout_run_api.CreateRunRequest(
|
|
416
|
+
attachments=[rid_from_instance_or_string(a) for a in attachments or ()],
|
|
417
|
+
data_sources={},
|
|
418
|
+
description=description or "",
|
|
419
|
+
labels=[] if labels is None else list(labels),
|
|
420
|
+
links=[] if links is None else create_links(links),
|
|
421
|
+
properties={} if properties is None else dict(properties),
|
|
422
|
+
start_time=_SecondsNanos.from_flexible(start).to_scout_run_api(),
|
|
423
|
+
title=name,
|
|
424
|
+
end_time=None if end is None else _SecondsNanos.from_flexible(end).to_scout_run_api(),
|
|
425
|
+
assets=[] if asset_rids is None else list(asset_rids),
|
|
426
|
+
workspace=clients.workspace_rid,
|
|
427
|
+
)
|
|
428
|
+
response = clients.run.create_run(clients.auth_header, request)
|
|
429
|
+
return Run._from_conjure(clients, response)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nominal
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.101.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
|
|
@@ -21,7 +21,7 @@ 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
23
|
Requires-Dist: nominal-api==0.1032.0
|
|
24
|
-
Requires-Dist: nominal-streaming==0.5.8
|
|
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
|
|
27
27
|
Requires-Dist: polars>=0.0.0
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
CHANGELOG.md,sha256=
|
|
1
|
+
CHANGELOG.md,sha256=95CbjI-NMgs9Jf26PjTGicKnbcm3SLZXRgXjPSLpn58,84741
|
|
2
2
|
LICENSE,sha256=zEGHG9mjDjaIS3I79O8mweQo-yiTbqx8jJvUPppVAwk,1067
|
|
3
3
|
README.md,sha256=KKe0dxh_pHXCtB7I9G4qWGQYvot_BZU8yW6MJyuyUHM,311
|
|
4
4
|
nominal/__init__.py,sha256=rbraORnXUrNn1hywLXM0XwSQCd9UmQt20PDYlsBalfE,2167
|
|
@@ -30,23 +30,23 @@ nominal/config/_config.py,sha256=yKq_H1iYJDoxRfLz2iXLbbVdoL0MTEY0FS4eVL12w0g,200
|
|
|
30
30
|
nominal/core/__init__.py,sha256=5eC2J0lzpV7JcuKDUimJCfgXuVL7HNgHrLhqxcy5NCc,2333
|
|
31
31
|
nominal/core/_clientsbunch.py,sha256=YwciugX7rQ9AOPHyvKuavG7b9SlX1PURRquP37nvLqE,8458
|
|
32
32
|
nominal/core/_constants.py,sha256=SrxgaSqAEB1MvTSrorgGam3eO29iCmRr6VIdajxX3gI,56
|
|
33
|
-
nominal/core/asset.py,sha256=
|
|
33
|
+
nominal/core/asset.py,sha256=48Znq-KaDZfTBcdXaBn3zlF22EOCHEEz8D3boB2MM6o,21544
|
|
34
34
|
nominal/core/attachment.py,sha256=iJaDyF6JXsKxxBLA03I0WMmQF8U0bA-wRwvXMEhfWLU,4284
|
|
35
35
|
nominal/core/bounds.py,sha256=742BWmGL3FBryRAjoiJRg2N6aVinjYkQLxN7kfnJ40Q,581
|
|
36
36
|
nominal/core/channel.py,sha256=dbe8wpfMiWqHu98x66w6GOmC9Ro33Wv9AhBVx2DvtVk,18970
|
|
37
37
|
nominal/core/checklist.py,sha256=rO1RPDYV3o2miPKF7DcCiYpj6bUN-sdtZNhJkXzkfYE,7110
|
|
38
|
-
nominal/core/client.py,sha256=
|
|
38
|
+
nominal/core/client.py,sha256=Csmu5dlQBmLFZcJDODPJytne4WQgM42a6W2UWSm34Go,69523
|
|
39
39
|
nominal/core/connection.py,sha256=ySbPN_a2takVa8wIU9mK4fB6vYLyZnN-qSmXVkLUxAY,5157
|
|
40
40
|
nominal/core/containerized_extractors.py,sha256=fUz3-NHoNWYKqOCD15gLwGXDKVfdsW-x_kpXnkOI3BE,10224
|
|
41
41
|
nominal/core/data_review.py,sha256=bEnRsd8LI4x9YOBPcF2H3h5-e12A7Gh8gQfsNUAZmPQ,7922
|
|
42
|
-
nominal/core/dataset.py,sha256=
|
|
42
|
+
nominal/core/dataset.py,sha256=Rt20H2ekUbF0_YyF-OkJhs3KaRTqQzNNxyneRjIEOJk,46627
|
|
43
43
|
nominal/core/dataset_file.py,sha256=oENANJ17A4K63cZ8Fr7lUm_kVPyA4fL2rUsZ3oXXk2U,16396
|
|
44
|
-
nominal/core/datasource.py,sha256=
|
|
45
|
-
nominal/core/event.py,sha256=
|
|
44
|
+
nominal/core/datasource.py,sha256=k13B6u6uw5pd49SuVM3gXtATgqO_BUnqGUMGiiW6Moc,16920
|
|
45
|
+
nominal/core/event.py,sha256=eQY_Csa5_6K0vrWfeOFBO4PVmFS8QhzhXfQ4wbb8Oy0,10215
|
|
46
46
|
nominal/core/exceptions.py,sha256=GUpwXRgdYamLl6684FE8ttCRHkBx6WEhOZ3NPE-ybD4,2671
|
|
47
|
-
nominal/core/filetype.py,sha256=
|
|
47
|
+
nominal/core/filetype.py,sha256=uzKe4iNHSv27mvz8-5EJEsvGOn3msEm_IhCj8OsCAPY,5526
|
|
48
48
|
nominal/core/log.py,sha256=z3hI3CIEyMwpUSWjwBsJ6a3JNGzBbsmrVusSU6uI7CY,3885
|
|
49
|
-
nominal/core/run.py,sha256=
|
|
49
|
+
nominal/core/run.py,sha256=uVBQevkD_Q3AWZ_pNt-jD5Y4hHfHGBexKEhobF9Se50,17857
|
|
50
50
|
nominal/core/secret.py,sha256=Ckq48m60i7rktxL9GY-nxHU5v8gHv9F1-JN7_MSf4bM,2863
|
|
51
51
|
nominal/core/unit.py,sha256=Wa-Bvu0hD-nzxVaQJSnn5YqAfnhUd2kWw2SswXnbMHY,3161
|
|
52
52
|
nominal/core/user.py,sha256=FV333TN4pQzcLh5b2CfxvBnnXyB1TrOP8Ppx1-XdaiE,481
|
|
@@ -67,7 +67,7 @@ nominal/core/_utils/multipart.py,sha256=0dA2XcTHuOQIyS0139O8WZiCjwePaD1sYDUmTgmW
|
|
|
67
67
|
nominal/core/_utils/multipart_downloader.py,sha256=16OJEPqxCwOnfjptYdrlwQVuSUQYoe9_iiW60ZSjWos,13859
|
|
68
68
|
nominal/core/_utils/networking.py,sha256=n9ZqYtnpwPCjz9C-4eixsTkrhFh-DW6lknBJlHckHhg,8200
|
|
69
69
|
nominal/core/_utils/pagination_tools.py,sha256=cEBY1WiA1d3cWJEM0myYF_pX8JdQ_e-5asngVXrUc_Y,12152
|
|
70
|
-
nominal/core/_utils/query_tools.py,sha256=
|
|
70
|
+
nominal/core/_utils/query_tools.py,sha256=wcEVvanF_kNCVGEEHI6WjFs1uDyOw9kCh0qHnlyKLBc,13860
|
|
71
71
|
nominal/core/_utils/queueing.py,sha256=3qljc7dFI1UahlKjCaRVybM4poMCV5SayjyRPyXcPxg,3654
|
|
72
72
|
nominal/exceptions/__init__.py,sha256=W2r_GWJkZQQ6t3HooFjGRdhIgJq3fBvRV7Yn6gseoO0,415
|
|
73
73
|
nominal/experimental/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -87,7 +87,7 @@ nominal/experimental/logging/rich_log_handler.py,sha256=8yz_VtxNgJg2oiesnXz2iXoB
|
|
|
87
87
|
nominal/experimental/migration/__init__.py,sha256=E2IgWJLwJ5bN6jbl8k5nHECKFx5aT11jKAzVYcyXn3o,460
|
|
88
88
|
nominal/experimental/migration/migration_utils.py,sha256=j4In_sU_cWW1kScneMP2G8B7LHDcnY2YDE0fwIv8BiY,22831
|
|
89
89
|
nominal/experimental/rust_streaming/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
90
|
-
nominal/experimental/rust_streaming/rust_write_stream.py,sha256=
|
|
90
|
+
nominal/experimental/rust_streaming/rust_write_stream.py,sha256=E-L5JtcwPWnCEm0o4_k-AVzw173sRSgElzKrgHoYwbs,1490
|
|
91
91
|
nominal/experimental/stream_v2/__init__.py,sha256=W39vK46pssx5sXvmsImMuJiEPs7iGtwrbYBI0bWnXCY,2313
|
|
92
92
|
nominal/experimental/stream_v2/_serializer.py,sha256=DcGimcY1LsXNeCzOWrel3SwuvoRV4XLdOFjqjM7MgPY,1035
|
|
93
93
|
nominal/experimental/stream_v2/_write_stream.py,sha256=-EncNPXUDYaL1YpFlJFEkuLgcxMdyKEXS5JJzP_2LlI,9981
|
|
@@ -104,8 +104,8 @@ nominal/thirdparty/polars/polars_export_handler.py,sha256=hGCSwXX9dC4MG01CmmjlTb
|
|
|
104
104
|
nominal/thirdparty/tdms/__init__.py,sha256=6n2ImFr2Wiil6JM1P5Q7Mpr0VzLcnDkmup_ftNpPq-s,142
|
|
105
105
|
nominal/thirdparty/tdms/_tdms.py,sha256=eiHFTUviyDPDClckNldjs_jTTSH_sdmboKDq0oIGChQ,8711
|
|
106
106
|
nominal/ts/__init__.py,sha256=hmd0ENvDhxRnzDKGLxIub6QG8LpcxCgcyAct029CaEs,21442
|
|
107
|
-
nominal-1.
|
|
108
|
-
nominal-1.
|
|
109
|
-
nominal-1.
|
|
110
|
-
nominal-1.
|
|
111
|
-
nominal-1.
|
|
107
|
+
nominal-1.101.0.dist-info/METADATA,sha256=OO0vRe9USFPos10KdaDORhPL1M1BO1h5aLnsX1dA09E,2277
|
|
108
|
+
nominal-1.101.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
109
|
+
nominal-1.101.0.dist-info/entry_points.txt,sha256=-mCLhxgg9R_lm5efT7vW9wuBH12izvY322R0a3TYxbE,66
|
|
110
|
+
nominal-1.101.0.dist-info/licenses/LICENSE,sha256=zEGHG9mjDjaIS3I79O8mweQo-yiTbqx8jJvUPppVAwk,1067
|
|
111
|
+
nominal-1.101.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|