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 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 Iterable, Mapping, Sequence
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
- request = scout_run_api.CreateRunRequest(
510
- attachments=[rid_from_instance_or_string(a) for a in attachments],
511
- data_sources={},
512
- description=description or "",
513
- labels=list(labels),
514
- links=create_links(links),
515
- properties={} if properties is None else dict(properties),
516
- start_time=_SecondsNanos.from_flexible(start).to_scout_run_api(),
517
- title=name,
518
- end_time=None if end is None else _SecondsNanos.from_flexible(end).to_scout_run_api(),
519
- assets=[] if asset is None else [rid_from_instance_or_string(asset)],
520
- workspace=self._clients.workspace_rid,
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
- request = event.CreateEvent(
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
- asset_rids=[rid_from_instance_or_string(asset) for asset in assets],
1165
- timestamp=_SecondsNanos.from_flexible(start).to_api(),
1166
- duration=_to_api_duration(duration),
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
- query = create_search_events_query(
1315
+ return _search_events(
1316
+ clients=self._clients,
1255
1317
  search_text=search_text,
1256
1318
  after=after,
1257
1319
  before=before,
1258
- assets=None if assets is None else [rid_from_instance_or_string(asset) for asset in assets],
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
- created_by=rid_from_instance_or_string(created_by) if created_by else None,
1262
- workbook=rid_from_instance_or_string(workbook) if workbook else None,
1263
- data_review=rid_from_instance_or_string(data_review) if data_review else None,
1264
- assignee=rid_from_instance_or_string(assignee) if assignee else None,
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."
@@ -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
- from nominal.experimental.rust_streaming.rust_write_stream import RustWriteStream
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.asset import Asset
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)
@@ -5,9 +5,7 @@ import pathlib
5
5
 
6
6
  from nominal_streaming import NominalDatasetStream
7
7
 
8
- from nominal.core._stream.write_stream import (
9
- DataStream,
10
- )
8
+ from nominal.core._stream.write_stream import DataStream
11
9
  from nominal.core.datasource import DataSource
12
10
 
13
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nominal
3
- Version: 1.100.0
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=jvj73GmZ1DOpXVyEcRVZ7Wm2ejFmH2UhBPO66Yt8wu0,83100
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=vWi_5jNm1sBo4jCa4wTrL65IQ7b_lefZTLRsakoW7ro,18355
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=zTaayeJf8IFA7BlNoVCaVpDA6cXIBaZGP934Tg6OhDI,67568
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=SUsn6qsbuceVLRYF47IquY_sW6OBRp4aExL1F3Bsaec,34802
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=D9jHirAzUZ0pc3nW1XIURpw1UqQoA2E-nUUylZR1jbE,16707
45
- nominal/core/event.py,sha256=D8qIX_dTjfSHN7jFW8vV-9htbQTaqk9VvRfK7t-sbbw,5891
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=jAPe6F7pDT8ixsD2-Y8eJdHOxgimdEQte4RQybWwsos,5465
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=Rqy2o6sLE5RsAvvNnle7jRPJ-8UNfHmD-pdsRTOjA8Y,14792
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=rabmhqUYw0POybZtGDoMyAwwXh4VMuYM6mMf-iAfWdc,15860
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=UoJEujzRAPlsAq2c24fgrub3c0DO0HM10SGsIgZjPKk,1499
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.100.0.dist-info/METADATA,sha256=v6eSXRLbr-cRBxvxBm3hsi5cfAQavQ2Zhng00SrS3xc,1981
108
- nominal-1.100.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
109
- nominal-1.100.0.dist-info/entry_points.txt,sha256=-mCLhxgg9R_lm5efT7vW9wuBH12izvY322R0a3TYxbE,66
110
- nominal-1.100.0.dist-info/licenses/LICENSE,sha256=zEGHG9mjDjaIS3I79O8mweQo-yiTbqx8jJvUPppVAwk,1067
111
- nominal-1.100.0.dist-info/RECORD,,
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,,