nominal 1.101.0__tar.gz → 1.103.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. {nominal-1.101.0 → nominal-1.103.0}/.gitignore +7 -0
  2. {nominal-1.101.0 → nominal-1.103.0}/CHANGELOG.md +23 -0
  3. {nominal-1.101.0 → nominal-1.103.0}/PKG-INFO +1 -1
  4. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/__init__.py +3 -1
  5. nominal-1.103.0/nominal/core/_event_types.py +100 -0
  6. nominal-1.103.0/nominal/core/_types.py +6 -0
  7. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/_utils/query_tools.py +52 -1
  8. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/asset.py +87 -41
  9. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/attachment.py +3 -1
  10. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/client.py +24 -52
  11. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/connection.py +3 -3
  12. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/dataset.py +17 -16
  13. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/dataset_file.py +5 -2
  14. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/datasource.py +4 -4
  15. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/event.py +9 -82
  16. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/filetype.py +7 -5
  17. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/run.py +6 -3
  18. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/video.py +3 -2
  19. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/migration/migration_utils.py +189 -10
  20. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/rust_streaming/rust_write_stream.py +3 -2
  21. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/video_processing/video_conversion.py +5 -2
  22. {nominal-1.101.0 → nominal-1.103.0}/nominal/nominal.py +9 -8
  23. {nominal-1.101.0 → nominal-1.103.0}/nominal/thirdparty/tdms/_tdms.py +4 -3
  24. {nominal-1.101.0 → nominal-1.103.0}/pyproject.toml +1 -1
  25. {nominal-1.101.0 → nominal-1.103.0}/LICENSE +0 -0
  26. {nominal-1.101.0 → nominal-1.103.0}/README.md +0 -0
  27. {nominal-1.101.0 → nominal-1.103.0}/nominal/__init__.py +0 -0
  28. {nominal-1.101.0 → nominal-1.103.0}/nominal/__main__.py +0 -0
  29. {nominal-1.101.0 → nominal-1.103.0}/nominal/_utils/README.md +0 -0
  30. {nominal-1.101.0 → nominal-1.103.0}/nominal/_utils/__init__.py +0 -0
  31. {nominal-1.101.0 → nominal-1.103.0}/nominal/_utils/dataclass_tools.py +0 -0
  32. {nominal-1.101.0 → nominal-1.103.0}/nominal/_utils/deprecation_tools.py +0 -0
  33. {nominal-1.101.0 → nominal-1.103.0}/nominal/_utils/iterator_tools.py +0 -0
  34. {nominal-1.101.0 → nominal-1.103.0}/nominal/_utils/streaming_tools.py +0 -0
  35. {nominal-1.101.0 → nominal-1.103.0}/nominal/_utils/timing_tools.py +0 -0
  36. {nominal-1.101.0 → nominal-1.103.0}/nominal/cli/__init__.py +0 -0
  37. {nominal-1.101.0 → nominal-1.103.0}/nominal/cli/__main__.py +0 -0
  38. {nominal-1.101.0 → nominal-1.103.0}/nominal/cli/attachment.py +0 -0
  39. {nominal-1.101.0 → nominal-1.103.0}/nominal/cli/auth.py +0 -0
  40. {nominal-1.101.0 → nominal-1.103.0}/nominal/cli/config.py +0 -0
  41. {nominal-1.101.0 → nominal-1.103.0}/nominal/cli/dataset.py +0 -0
  42. {nominal-1.101.0 → nominal-1.103.0}/nominal/cli/download.py +0 -0
  43. {nominal-1.101.0 → nominal-1.103.0}/nominal/cli/mis.py +0 -0
  44. {nominal-1.101.0 → nominal-1.103.0}/nominal/cli/run.py +0 -0
  45. {nominal-1.101.0 → nominal-1.103.0}/nominal/cli/util/__init__.py +0 -0
  46. {nominal-1.101.0 → nominal-1.103.0}/nominal/cli/util/click_log_handler.py +0 -0
  47. {nominal-1.101.0 → nominal-1.103.0}/nominal/cli/util/global_decorators.py +0 -0
  48. {nominal-1.101.0 → nominal-1.103.0}/nominal/cli/util/verify_connection.py +0 -0
  49. {nominal-1.101.0 → nominal-1.103.0}/nominal/config/__init__.py +0 -0
  50. {nominal-1.101.0 → nominal-1.103.0}/nominal/config/_config.py +0 -0
  51. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/_clientsbunch.py +0 -0
  52. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/_constants.py +0 -0
  53. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/_stream/__init__.py +0 -0
  54. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/_stream/batch_processor.py +0 -0
  55. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/_stream/batch_processor_proto.py +0 -0
  56. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/_stream/write_stream.py +0 -0
  57. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/_stream/write_stream_base.py +0 -0
  58. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/_utils/README.md +0 -0
  59. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/_utils/__init__.py +0 -0
  60. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/_utils/api_tools.py +0 -0
  61. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/_utils/multipart.py +0 -0
  62. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/_utils/multipart_downloader.py +0 -0
  63. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/_utils/networking.py +0 -0
  64. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/_utils/pagination_tools.py +0 -0
  65. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/_utils/queueing.py +0 -0
  66. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/bounds.py +0 -0
  67. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/channel.py +0 -0
  68. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/checklist.py +0 -0
  69. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/containerized_extractors.py +0 -0
  70. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/data_review.py +0 -0
  71. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/exceptions.py +0 -0
  72. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/log.py +0 -0
  73. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/secret.py +0 -0
  74. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/unit.py +0 -0
  75. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/user.py +0 -0
  76. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/video_file.py +0 -0
  77. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/workbook.py +0 -0
  78. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/workbook_template.py +0 -0
  79. {nominal-1.101.0 → nominal-1.103.0}/nominal/core/workspace.py +0 -0
  80. {nominal-1.101.0 → nominal-1.103.0}/nominal/exceptions/__init__.py +0 -0
  81. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/__init__.py +0 -0
  82. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/compute/README.md +0 -0
  83. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/compute/__init__.py +0 -0
  84. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/compute/_buckets.py +0 -0
  85. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/compute/dsl/__init__.py +0 -0
  86. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/compute/dsl/_enum_expr_impls.py +0 -0
  87. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/compute/dsl/_numeric_expr_impls.py +0 -0
  88. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/compute/dsl/_range_expr_impls.py +0 -0
  89. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/compute/dsl/exprs.py +0 -0
  90. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/compute/dsl/params.py +0 -0
  91. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/logging/__init__.py +0 -0
  92. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/logging/click_log_handler.py +0 -0
  93. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/logging/nominal_log_handler.py +0 -0
  94. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/logging/rich_log_handler.py +0 -0
  95. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/migration/__init__.py +0 -0
  96. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/rust_streaming/__init__.py +0 -0
  97. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/stream_v2/__init__.py +0 -0
  98. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/stream_v2/_serializer.py +0 -0
  99. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/stream_v2/_write_stream.py +0 -0
  100. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/video_processing/__init__.py +0 -0
  101. {nominal-1.101.0 → nominal-1.103.0}/nominal/experimental/video_processing/resolution.py +0 -0
  102. {nominal-1.101.0 → nominal-1.103.0}/nominal/py.typed +0 -0
  103. {nominal-1.101.0 → nominal-1.103.0}/nominal/thirdparty/__init__.py +0 -0
  104. {nominal-1.101.0 → nominal-1.103.0}/nominal/thirdparty/matlab/__init__.py +0 -0
  105. {nominal-1.101.0 → nominal-1.103.0}/nominal/thirdparty/matlab/_matlab.py +0 -0
  106. {nominal-1.101.0 → nominal-1.103.0}/nominal/thirdparty/pandas/__init__.py +0 -0
  107. {nominal-1.101.0 → nominal-1.103.0}/nominal/thirdparty/pandas/_pandas.py +0 -0
  108. {nominal-1.101.0 → nominal-1.103.0}/nominal/thirdparty/polars/__init__.py +0 -0
  109. {nominal-1.101.0 → nominal-1.103.0}/nominal/thirdparty/polars/polars_export_handler.py +0 -0
  110. {nominal-1.101.0 → nominal-1.103.0}/nominal/thirdparty/tdms/__init__.py +0 -0
  111. {nominal-1.101.0 → nominal-1.103.0}/nominal/ts/__init__.py +0 -0
@@ -170,3 +170,10 @@ playground/
170
170
  # Backup files
171
171
  *~
172
172
  \#*
173
+
174
+ # Don't check in local uv.toml settings
175
+ uv.toml
176
+
177
+ # Don't check in data
178
+ *.csv
179
+ *.parquet
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.103.0](https://github.com/nominal-io/nominal-client/compare/v1.102.0...v1.103.0) (2026-01-06)
4
+
5
+
6
+ ### Features
7
+
8
+ * add clone/copy run ([#563](https://github.com/nominal-io/nominal-client/issues/563)) ([276c511](https://github.com/nominal-io/nominal-client/commit/276c511d10c73b9f80ea2efed32d4047fe33795b))
9
+ * universally support paths and strings, and add PathLike alias ([#566](https://github.com/nominal-io/nominal-client/issues/566)) ([860dd3d](https://github.com/nominal-io/nominal-client/commit/860dd3d67456d269356b66b5434b904647e5a2ff))
10
+
11
+ ## [1.102.0](https://github.com/nominal-io/nominal-client/compare/v1.101.0...v1.102.0) (2026-01-05)
12
+
13
+
14
+ ### Features
15
+
16
+ * add run creation on an asset ([#558](https://github.com/nominal-io/nominal-client/issues/558)) ([9c0718a](https://github.com/nominal-io/nominal-client/commit/9c0718ae2706d4192087fb787e99f76d702a18c9))
17
+ * add util to search events by origin type and add clone/copy events ([#555](https://github.com/nominal-io/nominal-client/issues/555)) ([bc427d6](https://github.com/nominal-io/nominal-client/commit/bc427d6b97a777e6802dc8139c8d389582558a77))
18
+ * allow unarchiving a run ([#560](https://github.com/nominal-io/nominal-client/issues/560)) ([0221a56](https://github.com/nominal-io/nominal-client/commit/0221a56c73d133581567cc5183fda237607d79b5))
19
+ * clean up and deprecate old video creation methods in NominalClient ([#561](https://github.com/nominal-io/nominal-client/issues/561)) ([e61919c](https://github.com/nominal-io/nominal-client/commit/e61919ca338a5e0de818471431d750246d87977c))
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * update how workspaces are selected in migration_utils ([#559](https://github.com/nominal-io/nominal-client/issues/559)) ([4379121](https://github.com/nominal-io/nominal-client/commit/4379121f44411eed19248ee16540672bbed743a0))
25
+
3
26
  ## [1.101.0](https://github.com/nominal-io/nominal-client/compare/v1.100.0...v1.101.0) (2025-12-23)
4
27
 
5
28
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nominal
3
- Version: 1.101.0
3
+ Version: 1.103.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
@@ -1,3 +1,4 @@
1
+ from nominal.core._event_types import EventType, SearchEventOriginType
1
2
  from nominal.core._stream.write_stream import WriteStream
2
3
  from nominal.core._utils.api_tools import LinkDict
3
4
  from nominal.core.asset import Asset
@@ -19,7 +20,7 @@ from nominal.core.data_review import CheckViolation, DataReview, DataReviewBuild
19
20
  from nominal.core.dataset import Dataset, poll_until_ingestion_completed
20
21
  from nominal.core.dataset_file import DatasetFile, IngestWaitType, as_files_ingested, wait_for_files_to_ingest
21
22
  from nominal.core.datasource import DataSource
22
- from nominal.core.event import Event, EventType
23
+ from nominal.core.event import Event
23
24
  from nominal.core.filetype import FileType, FileTypes
24
25
  from nominal.core.log import LogPoint
25
26
  from nominal.core.run import Run
@@ -60,6 +61,7 @@ __all__ = [
60
61
  "NominalClient",
61
62
  "poll_until_ingestion_completed",
62
63
  "Run",
64
+ "SearchEventOriginType",
63
65
  "Secret",
64
66
  "TagDetails",
65
67
  "TimestampMetadata",
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import Iterable, NamedTuple
5
+
6
+ from nominal_api import event
7
+
8
+
9
+ class EventType(Enum):
10
+ INFO = "INFO"
11
+ FLAG = "FLAG"
12
+ ERROR = "ERROR"
13
+ SUCCESS = "SUCCESS"
14
+ UNKNOWN = "UNKNOWN"
15
+
16
+ @classmethod
17
+ def from_api_event_type(cls, event: event.EventType) -> EventType:
18
+ if event.name == "INFO":
19
+ return cls.INFO
20
+ elif event.name == "FLAG":
21
+ return cls.FLAG
22
+ elif event.name == "ERROR":
23
+ return cls.ERROR
24
+ elif event.name == "SUCCESS":
25
+ return cls.SUCCESS
26
+ else:
27
+ return cls.UNKNOWN
28
+
29
+ def _to_api_event_type(self) -> event.EventType:
30
+ if self.name == "INFO":
31
+ return event.EventType.INFO
32
+ elif self.name == "FLAG":
33
+ return event.EventType.FLAG
34
+ elif self.name == "ERROR":
35
+ return event.EventType.ERROR
36
+ elif self.name == "SUCCESS":
37
+ return event.EventType.SUCCESS
38
+ else:
39
+ return event.EventType.UNKNOWN
40
+
41
+
42
+ class EventCreationType(Enum):
43
+ MANUAL = "MANUAL"
44
+ BY_EXTERNAL_RESOURCE = "BY_EXTERNAL_RESOURCE"
45
+
46
+
47
+ class SearchEventOriginType(NamedTuple):
48
+ name: str
49
+ creation_type: EventCreationType
50
+
51
+ @classmethod
52
+ def from_api_origin_type(cls, event: event.SearchEventOriginType) -> SearchEventOriginType:
53
+ if event.name == "WORKBOOK":
54
+ return SearchEventOriginTypes.WORKBOOK
55
+ elif event.name == "TEMPLATE":
56
+ return SearchEventOriginTypes.TEMPLATE
57
+ elif event.name == "API":
58
+ return SearchEventOriginTypes.API
59
+ elif event.name == "DATA_REVIEW":
60
+ return SearchEventOriginTypes.DATA_REVIEW
61
+ elif event.name == "PROCEDURE":
62
+ return SearchEventOriginTypes.PROCEDURE
63
+ elif event.name == "STREAMING_CHECKLIST":
64
+ return SearchEventOriginTypes.STREAMING_CHECKLIST
65
+ else:
66
+ raise ValueError(f"Unexpected Event Origin {event.name}")
67
+
68
+ def _to_api_search_event_origin_type(self) -> event.SearchEventOriginType:
69
+ if self.name == "WORKBOOK":
70
+ return event.SearchEventOriginType.WORKBOOK
71
+ elif self.name == "TEMPLATE":
72
+ return event.SearchEventOriginType.TEMPLATE
73
+ elif self.name == "API":
74
+ return event.SearchEventOriginType.API
75
+ elif self.name == "DATA_REVIEW":
76
+ return event.SearchEventOriginType.DATA_REVIEW
77
+ elif self.name == "PROCEDURE":
78
+ return event.SearchEventOriginType.PROCEDURE
79
+ elif self.name == "STREAMING_CHECKLIST":
80
+ return event.SearchEventOriginType.STREAMING_CHECKLIST
81
+ else:
82
+ raise ValueError(f"Unexpected Event Origin {self.name}")
83
+
84
+ @classmethod
85
+ def get_manual_origin_types(cls) -> Iterable[SearchEventOriginType]:
86
+ """Return all origin types that are manually created."""
87
+ return [
88
+ origin_type
89
+ for origin_type in SearchEventOriginTypes.__dict__.values()
90
+ if isinstance(origin_type, SearchEventOriginType) and origin_type.creation_type == EventCreationType.MANUAL
91
+ ]
92
+
93
+
94
+ class SearchEventOriginTypes:
95
+ WORKBOOK = SearchEventOriginType("WORKBOOK", EventCreationType.MANUAL)
96
+ TEMPLATE = SearchEventOriginType("TEMPLATE", EventCreationType.MANUAL)
97
+ API = SearchEventOriginType("API", EventCreationType.MANUAL)
98
+ DATA_REVIEW = SearchEventOriginType("DATA_REVIEW", EventCreationType.BY_EXTERNAL_RESOURCE)
99
+ PROCEDURE = SearchEventOriginType("PROCEDURE", EventCreationType.BY_EXTERNAL_RESOURCE)
100
+ STREAMING_CHECKLIST = SearchEventOriginType("STREAMING_CHECKLIST", EventCreationType.BY_EXTERNAL_RESOURCE)
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ import pathlib
4
+ from typing import TypeAlias
5
+
6
+ PathLike: TypeAlias = pathlib.Path | str
@@ -1,11 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import datetime
4
- from typing import Mapping, Sequence
4
+ from typing import Iterable, Mapping, Sequence
5
5
 
6
6
  from nominal_api import (
7
7
  api,
8
8
  authentication_api,
9
+ event,
9
10
  ingest_api,
10
11
  scout_asset_api,
11
12
  scout_catalog,
@@ -350,3 +351,53 @@ def create_search_workbook_templates_query(
350
351
  queries.append(scout_template_api.SearchTemplatesQuery(is_published=published))
351
352
 
352
353
  return scout_template_api.SearchTemplatesQuery(and_=queries)
354
+
355
+
356
+ def _create_search_events_query( # noqa: PLR0912
357
+ search_text: str | None = None,
358
+ after: str | datetime | IntegralNanosecondsUTC | None = None,
359
+ before: str | datetime | IntegralNanosecondsUTC | None = None,
360
+ asset_rids: Iterable[str] | None = None,
361
+ labels: Iterable[str] | None = None,
362
+ properties: Mapping[str, str] | None = None,
363
+ created_by_rid: str | None = None,
364
+ workbook_rid: str | None = None,
365
+ data_review_rid: str | None = None,
366
+ assignee_rid: str | None = None,
367
+ event_type: event.EventType | None = None,
368
+ origin_types: Iterable[event.SearchEventOriginType] | None = None,
369
+ workspace_rid: str | None = None,
370
+ ) -> event.SearchQuery:
371
+ queries = []
372
+ if search_text is not None:
373
+ queries.append(event.SearchQuery(search_text=search_text))
374
+ if after is not None:
375
+ queries.append(event.SearchQuery(after=_SecondsNanos.from_flexible(after).to_api()))
376
+ if before is not None:
377
+ queries.append(event.SearchQuery(before=_SecondsNanos.from_flexible(before).to_api()))
378
+ if asset_rids:
379
+ for asset in asset_rids:
380
+ queries.append(event.SearchQuery(asset=asset))
381
+ if labels:
382
+ for label in labels:
383
+ queries.append(event.SearchQuery(label=label))
384
+ if properties:
385
+ for name, value in properties.items():
386
+ queries.append(event.SearchQuery(property=api.Property(name=name, value=value)))
387
+ if created_by_rid:
388
+ queries.append(event.SearchQuery(created_by=created_by_rid))
389
+ if workbook_rid is not None:
390
+ queries.append(event.SearchQuery(workbook=workbook_rid))
391
+ if data_review_rid is not None:
392
+ queries.append(event.SearchQuery(data_review=data_review_rid))
393
+ if assignee_rid is not None:
394
+ queries.append(event.SearchQuery(assignee=assignee_rid))
395
+ if event_type is not None:
396
+ queries.append(event.SearchQuery(event_type=event_type))
397
+ if origin_types is not None:
398
+ origin_type_filter = event.OriginTypesFilter(api.SetOperator.OR, list(origin_types))
399
+ queries.append(event.SearchQuery(origin_types=origin_type_filter))
400
+ if workspace_rid is not None:
401
+ queries.append(event.SearchQuery(workspace=workspace_rid))
402
+
403
+ return event.SearchQuery(and_=queries)
@@ -4,7 +4,7 @@ import datetime
4
4
  import logging
5
5
  from dataclasses import dataclass, field
6
6
  from types import MappingProxyType
7
- from typing import Iterable, Literal, Mapping, Protocol, Sequence, TypeAlias, cast
7
+ from typing import Iterable, Literal, Mapping, Protocol, Sequence, TypeAlias
8
8
 
9
9
  from nominal_api import (
10
10
  event,
@@ -16,13 +16,21 @@ from nominal_api import (
16
16
  from typing_extensions import Self
17
17
 
18
18
  from nominal.core._clientsbunch import HasScoutParams
19
- from nominal.core._utils.api_tools import HasRid, Link, RefreshableMixin, create_links, rid_from_instance_or_string
19
+ from nominal.core._event_types import EventType, SearchEventOriginType
20
+ from nominal.core._utils.api_tools import (
21
+ HasRid,
22
+ Link,
23
+ LinkDict,
24
+ RefreshableMixin,
25
+ create_links,
26
+ rid_from_instance_or_string,
27
+ )
20
28
  from nominal.core._utils.pagination_tools import search_runs_by_asset_paginated
21
29
  from nominal.core.attachment import Attachment, _iter_get_attachments
22
30
  from nominal.core.connection import Connection, _get_connections
23
31
  from nominal.core.dataset import Dataset, _create_dataset, _DatasetWrapper, _get_datasets
24
32
  from nominal.core.datasource import DataSource
25
- from nominal.core.event import Event, EventType, _create_event, _search_events
33
+ from nominal.core.event import Event, _create_event, _search_events
26
34
  from nominal.core.video import Video, _create_video, _get_video
27
35
  from nominal.ts import IntegralNanosecondsDuration, IntegralNanosecondsUTC, _SecondsNanos
28
36
 
@@ -38,6 +46,14 @@ def _filter_scopes(
38
46
  return [scope for scope in scopes if scope.data_source.type.lower() == scope_type]
39
47
 
40
48
 
49
+ def _filter_scope_rids(
50
+ scopes: Sequence[scout_asset_api.DataScope], scope_type: ScopeTypeSpecifier
51
+ ) -> Mapping[str, str]:
52
+ return {
53
+ scope.data_scope_name: getattr(scope.data_source, scope_type) for scope in _filter_scopes(scopes, scope_type)
54
+ }
55
+
56
+
41
57
  @dataclass(frozen=True)
42
58
  class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
43
59
  rid: str
@@ -80,13 +96,9 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
80
96
  def _list_dataset_scopes(self) -> Sequence[scout_asset_api.DataScope]:
81
97
  return _filter_scopes(self._get_latest_api().data_scopes, "dataset")
82
98
 
83
- def _scope_rid(self, stype: Literal["dataset", "video", "connection"]) -> dict[str, str]:
99
+ def _scope_rids(self, scope_type: ScopeTypeSpecifier) -> Mapping[str, str]:
84
100
  asset = self._get_latest_api()
85
- return {
86
- scope.data_scope_name: cast(str, getattr(scope.data_source, stype))
87
- for scope in asset.data_scopes
88
- if scope.data_source.type.lower() == stype
89
- }
101
+ return _filter_scope_rids(asset.data_scopes, scope_type)
90
102
 
91
103
  def update(
92
104
  self,
@@ -153,23 +165,25 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
153
165
  """
154
166
  return (*self.list_datasets(), *self.list_connections(), *self.list_videos())
155
167
 
156
- def _remove_data_sources(
168
+ def remove_data_scopes(
157
169
  self,
158
170
  *,
159
- data_scope_names: Sequence[str] | None = None,
160
- data_sources: Sequence[ScopeType | str] | None = None,
171
+ names: Sequence[str] | None = None,
172
+ scopes: Sequence[ScopeType | str] | None = None,
161
173
  ) -> None:
162
- data_scope_names = data_scope_names or []
163
- data_sources = data_sources or []
164
-
165
- if isinstance(data_sources, str):
166
- raise RuntimeError("Expect `data_sources` to be a sequence, not a string")
174
+ """Remove data scopes from this asset.
167
175
 
168
- data_source_rids = {rid_from_instance_or_string(ds) for ds in data_sources}
176
+ Args:
177
+ names: Names of datascopes to remove
178
+ scopes: Rids or instances of scope types (dataset, video, connection) to remove.
179
+ """
180
+ scope_names_to_remove = names or []
181
+ data_scopes_to_remove = scopes or []
169
182
 
183
+ scope_rids_to_remove = {rid_from_instance_or_string(ds) for ds in data_scopes_to_remove}
170
184
  conjure_asset = self._get_latest_api()
171
185
 
172
- data_sources_to_keep = [
186
+ data_scopes_to_keep = [
173
187
  scout_asset_api.CreateAssetDataScope(
174
188
  data_scope_name=ds.data_scope_name,
175
189
  data_source=ds.data_source,
@@ -177,31 +191,21 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
177
191
  offset=ds.offset,
178
192
  )
179
193
  for ds in conjure_asset.data_scopes
180
- if ds.data_scope_name not in data_scope_names
181
- and (ds.data_source.dataset or ds.data_source.connection or ds.data_source.video) not in data_source_rids
194
+ if ds.data_scope_name not in scope_names_to_remove
195
+ and all(
196
+ rid not in scope_rids_to_remove
197
+ for rid in (ds.data_source.dataset, ds.data_source.connection, ds.data_source.video)
198
+ )
182
199
  ]
183
200
 
184
- api_asset = self._clients.assets.update_asset(
201
+ updated_asset = self._clients.assets.update_asset(
185
202
  self._clients.auth_header,
186
203
  scout_asset_api.UpdateAssetRequest(
187
- data_scopes=data_sources_to_keep,
204
+ data_scopes=data_scopes_to_keep,
188
205
  ),
189
206
  self.rid,
190
207
  )
191
- self._refresh_from_api(api_asset)
192
-
193
- def remove_data_scopes(
194
- self,
195
- *,
196
- names: Sequence[str] | None = None,
197
- scopes: Sequence[ScopeType | str] | None = None,
198
- ) -> None:
199
- """Remove data scopes from this asset.
200
-
201
- `names` are scope names.
202
- `scopes` are rids or scope objects.
203
- """
204
- self._remove_data_sources(data_scope_names=names, data_sources=scopes)
208
+ self._refresh_from_api(updated_asset)
205
209
 
206
210
  def add_dataset(
207
211
  self,
@@ -382,6 +386,46 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
382
386
  labels=labels,
383
387
  )
384
388
 
389
+ def create_run(
390
+ self,
391
+ name: str,
392
+ start: datetime.datetime | IntegralNanosecondsUTC,
393
+ end: datetime.datetime | IntegralNanosecondsUTC | None,
394
+ *,
395
+ description: str | None = None,
396
+ properties: Mapping[str, str] | None = None,
397
+ labels: Sequence[str] = (),
398
+ links: Sequence[str | Link | LinkDict] = (),
399
+ attachments: Iterable[Attachment] | Iterable[str] = (),
400
+ ) -> Run:
401
+ """Create a run associated with this Asset for a given span of time.
402
+
403
+ Args:
404
+ name: Name of the run.
405
+ start: Starting timestamp of the run.
406
+ end: Ending timestamp of the run, or None for an unbounded run.
407
+ description: Optionally, a human readable description of the run to create.
408
+ properties: Key-value pairs to use as properties on the created run.
409
+ labels: Sequence of labels to use on the created run.
410
+ links: Link metadata to add to the created run.
411
+ attachments: Attachments to associate with the created run.
412
+
413
+ Returns:
414
+ Returns the created run
415
+ """
416
+ return _create_run(
417
+ self._clients,
418
+ name=name,
419
+ start=start,
420
+ end=end,
421
+ description=description,
422
+ properties=properties,
423
+ labels=labels,
424
+ links=links,
425
+ attachments=attachments,
426
+ asset_rids=[self.rid],
427
+ )
428
+
385
429
  def get_dataset(self, data_scope_name: str) -> Dataset:
386
430
  """Retrieve a dataset by data scope name, or raise ValueError if one is not found."""
387
431
  dataset = self.get_data_scope(data_scope_name)
@@ -410,7 +454,7 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
410
454
  """List the datasets associated with this asset.
411
455
  Returns (data_scope_name, dataset) pairs for each dataset.
412
456
  """
413
- scope_rid = self._scope_rid(stype="dataset")
457
+ scope_rid = self._scope_rids(scope_type="dataset")
414
458
  if not scope_rid:
415
459
  return []
416
460
 
@@ -428,7 +472,7 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
428
472
  """List the connections associated with this asset.
429
473
  Returns (data_scope_name, connection) pairs for each connection.
430
474
  """
431
- scope_rid = self._scope_rid(stype="connection")
475
+ scope_rid = self._scope_rids(scope_type="connection")
432
476
  connections_meta = _get_connections(self._clients, list(scope_rid.values()))
433
477
  return [
434
478
  (scope, Connection._from_conjure(self._clients, connection))
@@ -439,7 +483,7 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
439
483
  """List the videos associated with this asset.
440
484
  Returns (data_scope_name, dataset) pairs for each video.
441
485
  """
442
- scope_rid = self._scope_rid(stype="video")
486
+ scope_rid = self._scope_rids(scope_type="video")
443
487
  return [
444
488
  (scope, Video._from_conjure(self._clients, _get_video(self._clients, rid)))
445
489
  for (scope, rid) in scope_rid.items()
@@ -477,6 +521,7 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
477
521
  data_review_rid: str | None = None,
478
522
  assignee_rid: str | None = None,
479
523
  event_type: EventType | None = None,
524
+ origin_types: Iterable[SearchEventOriginType] | None = None,
480
525
  ) -> Sequence[Event]:
481
526
  """Search for events associated with this Asset. See nominal.core.event._search_events for details."""
482
527
  return _search_events(
@@ -492,6 +537,7 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
492
537
  data_review_rid=data_review_rid,
493
538
  assignee_rid=assignee_rid,
494
539
  event_type=event_type,
540
+ origin_types=origin_types,
495
541
  )
496
542
 
497
543
  def remove_attachments(self, attachments: Iterable[Attachment] | Iterable[str]) -> None:
@@ -528,4 +574,4 @@ class Asset(_DatasetWrapper, HasRid, RefreshableMixin[scout_asset_api.Asset]):
528
574
 
529
575
 
530
576
  # Moving to bottom to deal with circular dependencies
531
- from nominal.core.run import Run # noqa: E402
577
+ from nominal.core.run import Run, _create_run # noqa: E402
@@ -10,6 +10,7 @@ from nominal_api import attachments_api
10
10
  from typing_extensions import Self
11
11
 
12
12
  from nominal.core._clientsbunch import HasScoutParams
13
+ from nominal.core._types import PathLike
13
14
  from nominal.core._utils.api_tools import HasRid, RefreshableMixin
14
15
  from nominal.ts import IntegralNanosecondsUTC, _SecondsNanos
15
16
 
@@ -69,11 +70,12 @@ class Attachment(HasRid, RefreshableMixin[attachments_api.Attachment]):
69
70
  # this acts like a file-like object in binary-mode.
70
71
  return cast(BinaryIO, response)
71
72
 
72
- def write(self, path: Path, mkdir: bool = True) -> None:
73
+ def write(self, path: PathLike, mkdir: bool = True) -> None:
73
74
  """Write an attachment to the filesystem.
74
75
 
75
76
  `path` should be the path you want to save to, i.e. a file, not a directory.
76
77
  """
78
+ path = Path(path)
77
79
  if mkdir:
78
80
  path.parent.mkdir(exist_ok=True, parents=True)
79
81
  with open(path, "wb") as wf:
@@ -36,6 +36,8 @@ from nominal._utils.deprecation_tools import warn_on_deprecated_argument
36
36
  from nominal.config import NominalConfig, _config
37
37
  from nominal.core._clientsbunch import ClientsBunch
38
38
  from nominal.core._constants import DEFAULT_API_BASE_URL
39
+ from nominal.core._event_types import EventType
40
+ from nominal.core._types import PathLike
39
41
  from nominal.core._utils.api_tools import (
40
42
  Link,
41
43
  LinkDict,
@@ -43,7 +45,6 @@ from nominal.core._utils.api_tools import (
43
45
  rid_from_instance_or_string,
44
46
  )
45
47
  from nominal.core._utils.multipart import (
46
- path_upload_name,
47
48
  upload_multipart_io,
48
49
  )
49
50
  from nominal.core._utils.pagination_tools import (
@@ -91,8 +92,8 @@ from nominal.core.dataset import (
91
92
  _get_datasets,
92
93
  )
93
94
  from nominal.core.datasource import DataSource
94
- from nominal.core.event import Event, EventType, _create_event, _search_events
95
- from nominal.core.exceptions import NominalConfigError, NominalError, NominalIngestError, NominalMethodRemovedError
95
+ from nominal.core.event import Event, _create_event, _search_events
96
+ from nominal.core.exceptions import NominalConfigError, NominalError, NominalMethodRemovedError
96
97
  from nominal.core.filetype import FileType, FileTypes
97
98
  from nominal.core.run import Run, _create_run
98
99
  from nominal.core.secret import Secret
@@ -732,7 +733,7 @@ class NominalClient:
732
733
 
733
734
  return dataset
734
735
 
735
- def create_empty_video(
736
+ def create_video(
736
737
  self,
737
738
  name: str,
738
739
  *,
@@ -762,6 +763,8 @@ class NominalClient:
762
763
  )
763
764
  return Video._from_conjure(self._clients, response)
764
765
 
766
+ create_empty_video = create_video
767
+
765
768
  def get_video(self, rid: str) -> Video:
766
769
  """Retrieve a video by its RID."""
767
770
  response = self._clients.video.get(self._clients.auth_header, rid)
@@ -851,7 +854,7 @@ class NominalClient:
851
854
 
852
855
  def create_attachment(
853
856
  self,
854
- attachment_file: Path | str,
857
+ attachment_file: PathLike,
855
858
  *,
856
859
  description: str | None = None,
857
860
  properties: Mapping[str, str] | None = None,
@@ -957,9 +960,13 @@ class NominalClient:
957
960
  response = self._clients.connection.get_connection(self._clients.auth_header, rid)
958
961
  return Connection._from_conjure(self._clients, response)
959
962
 
963
+ @deprecated(
964
+ "`create_video_from_mcap` is deprecated and will be removed in a future version. "
965
+ "Create a new video with `create_video` and then `add_mcap` to upload a file to the video."
966
+ )
960
967
  def create_video_from_mcap(
961
968
  self,
962
- path: Path | str,
969
+ path: PathLike,
963
970
  topic: str,
964
971
  name: str | None = None,
965
972
  description: str | None = None,
@@ -977,18 +984,14 @@ class NominalClient:
977
984
  if name is None:
978
985
  name = path.name
979
986
 
980
- with path.open("rb") as data_file:
981
- return self.create_video_from_mcap_io(
982
- data_file,
983
- name=name,
984
- topic=topic,
985
- file_type=FileTypes.MCAP,
986
- description=description,
987
- labels=labels,
988
- properties=properties,
989
- file_name=path_upload_name(path, FileTypes.MCAP),
990
- )
987
+ video = self.create_video(name, description=description, labels=labels, properties=properties)
988
+ video.add_mcap(path, topic, description)
989
+ return video
991
990
 
991
+ @deprecated(
992
+ "`create_video_from_mcap_io` is deprecated and will be removed in a future version. "
993
+ "Create a new video with `create_video` and then `add_mcap_from_io` to upload a file to the video."
994
+ )
992
995
  def create_video_from_mcap_io(
993
996
  self,
994
997
  mcap: BinaryIO,
@@ -1007,40 +1010,9 @@ class NominalClient:
1007
1010
 
1008
1011
  If name is None, the name of the file will be used.
1009
1012
  """
1010
- if isinstance(mcap, TextIOBase):
1011
- raise TypeError(f"dataset {mcap} must be open in binary mode, rather than text mode")
1012
-
1013
- if file_name is None:
1014
- file_name = name
1015
-
1016
- file_type = FileType(*file_type)
1017
- s3_path = upload_multipart_io(
1018
- self._clients.auth_header, self._clients.workspace_rid, mcap, file_name, file_type, self._clients.upload
1019
- )
1020
- request = ingest_api.IngestRequest(
1021
- options=ingest_api.IngestOptions(
1022
- video=ingest_api.VideoOpts(
1023
- source=ingest_api.IngestSource(s3=ingest_api.S3IngestSource(s3_path)),
1024
- target=ingest_api.VideoIngestTarget(
1025
- new=ingest_api.NewVideoIngestDestination(
1026
- title=name,
1027
- description=description,
1028
- properties={} if properties is None else dict(properties),
1029
- labels=list(labels),
1030
- workspace=self._clients.workspace_rid,
1031
- marking_rids=[],
1032
- )
1033
- ),
1034
- timestamp_manifest=scout_video_api.VideoFileTimestampManifest(
1035
- mcap=scout_video_api.McapTimestampManifest(api.McapChannelLocator(topic=topic))
1036
- ),
1037
- )
1038
- )
1039
- )
1040
- response = self._clients.ingest.ingest(self._clients.auth_header, request)
1041
- if response.details.video is None:
1042
- raise NominalIngestError("error ingesting mcap video: no video created")
1043
- return self.get_video(response.details.video.video_rid)
1013
+ video = self.create_video(name, description=description, labels=labels, properties=properties)
1014
+ video.add_mcap_from_io(mcap, file_name or name, topic, description, file_type)
1015
+ return video
1044
1016
 
1045
1017
  def create_streaming_connection(
1046
1018
  self,
@@ -1514,7 +1486,7 @@ class NominalClient:
1514
1486
  properties: A mapping of key-value pairs that must ALL be present on an workbook to be included.
1515
1487
  created_by: Searches for workbook templates with the given creator's rid
1516
1488
  archived: Searches for workbook templates that are archived if true
1517
- published: Searches f8or workbook templates that have been published if true
1489
+ published: Searches for workbook templates that have been published if true
1518
1490
 
1519
1491
  Returns:
1520
1492
  All workbook templates which match all of the provided conditions