nominal 1.166.0__tar.gz → 1.167.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 (177) hide show
  1. {nominal-1.166.0 → nominal-1.167.0}/CHANGELOG.md +13 -0
  2. {nominal-1.166.0 → nominal-1.167.0}/PKG-INFO +3 -3
  3. {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/container.py +4 -0
  4. {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/util/verify_connection.py +4 -1
  5. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/__init__.py +5 -0
  6. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_clientsbunch.py +5 -0
  7. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/grpc_tools.py +25 -17
  8. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/networking.py +45 -1
  9. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/pagination_tools.py +22 -0
  10. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/query_tools.py +13 -0
  11. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/client.py +105 -4
  12. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/containerized_extractor.py +5 -1
  13. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/dataset.py +9 -2
  14. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/dataset_file.py +94 -51
  15. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/datasource.py +5 -1
  16. nominal-1.167.0/nominal/core/elements.py +103 -0
  17. nominal-1.167.0/nominal/core/ingestion_job.py +337 -0
  18. nominal-1.167.0/nominal/core/marking.py +316 -0
  19. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/video.py +7 -2
  20. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute_as_code/_derived_datasets.py +7 -2
  21. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/dataset_utils/_dataset_utils.py +7 -2
  22. nominal-1.167.0/nominal/experimental/extractor/README.md +457 -0
  23. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/extractor/__init__.py +9 -5
  24. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/extractor/_runner.py +18 -8
  25. {nominal-1.166.0 → nominal-1.167.0}/pyproject.toml +3 -3
  26. nominal-1.166.0/nominal/core/ingestion_job.py +0 -192
  27. nominal-1.166.0/nominal/experimental/extractor/README.md +0 -293
  28. {nominal-1.166.0 → nominal-1.167.0}/.gitignore +0 -0
  29. {nominal-1.166.0 → nominal-1.167.0}/LICENSE +0 -0
  30. {nominal-1.166.0 → nominal-1.167.0}/NOTICE +0 -0
  31. {nominal-1.166.0 → nominal-1.167.0}/README.md +0 -0
  32. {nominal-1.166.0 → nominal-1.167.0}/nominal/_utils/README.md +0 -0
  33. {nominal-1.166.0 → nominal-1.167.0}/nominal/_utils/__init__.py +0 -0
  34. {nominal-1.166.0 → nominal-1.167.0}/nominal/_utils/dataclass_tools.py +0 -0
  35. {nominal-1.166.0 → nominal-1.167.0}/nominal/_utils/deprecation_tools.py +0 -0
  36. {nominal-1.166.0 → nominal-1.167.0}/nominal/_utils/iterator_tools.py +0 -0
  37. {nominal-1.166.0 → nominal-1.167.0}/nominal/_utils/streaming_tools.py +0 -0
  38. {nominal-1.166.0 → nominal-1.167.0}/nominal/_utils/timing_tools.py +0 -0
  39. {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/__init__.py +0 -0
  40. {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/__main__.py +0 -0
  41. {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/attachment.py +0 -0
  42. {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/config.py +0 -0
  43. {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/dataset.py +0 -0
  44. {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/download.py +0 -0
  45. {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/mis.py +0 -0
  46. {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/run.py +0 -0
  47. {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/util/__init__.py +0 -0
  48. {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/util/format.py +0 -0
  49. {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/util/global_decorators.py +0 -0
  50. {nominal-1.166.0 → nominal-1.167.0}/nominal/config/__init__.py +0 -0
  51. {nominal-1.166.0 → nominal-1.167.0}/nominal/config/_config.py +0 -0
  52. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_checklist_types.py +0 -0
  53. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_constants.py +0 -0
  54. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_event_types.py +0 -0
  55. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_stream/__init__.py +0 -0
  56. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_stream/batch_processor.py +0 -0
  57. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_stream/batch_processor_proto.py +0 -0
  58. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_stream/write_stream.py +0 -0
  59. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_stream/write_stream_base.py +0 -0
  60. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_types.py +0 -0
  61. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/README.md +0 -0
  62. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/__init__.py +0 -0
  63. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/api_tools.py +0 -0
  64. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/filenames.py +0 -0
  65. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/frontend_urls.py +0 -0
  66. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/multipart.py +0 -0
  67. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/multipart_downloader.py +0 -0
  68. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/queueing.py +0 -0
  69. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_video_types.py +0 -0
  70. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/asset.py +0 -0
  71. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/attachment.py +0 -0
  72. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/bounds.py +0 -0
  73. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/channel.py +0 -0
  74. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/checklist.py +0 -0
  75. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/comment.py +0 -0
  76. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/connection.py +0 -0
  77. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/container_image.py +0 -0
  78. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/data_review.py +0 -0
  79. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/event.py +0 -0
  80. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/exceptions.py +0 -0
  81. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/filetype.py +0 -0
  82. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/log.py +0 -0
  83. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/run.py +0 -0
  84. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/secret.py +0 -0
  85. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/streaming_checklist.py +0 -0
  86. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/unit.py +0 -0
  87. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/user.py +0 -0
  88. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/video_dataset_file.py +0 -0
  89. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/video_file.py +0 -0
  90. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/workbook.py +0 -0
  91. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/workbook_template.py +0 -0
  92. {nominal-1.166.0 → nominal-1.167.0}/nominal/core/workspace.py +0 -0
  93. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/__init__.py +0 -0
  94. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/checklist_utils/__init__.py +0 -0
  95. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/checklist_utils/checklist_utils.py +0 -0
  96. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/README.md +0 -0
  97. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/__init__.py +0 -0
  98. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/_buckets.py +0 -0
  99. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/dsl/__init__.py +0 -0
  100. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/dsl/_enum_expr_impls.py +0 -0
  101. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/dsl/_numeric_expr_impls.py +0 -0
  102. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/dsl/_range_expr_impls.py +0 -0
  103. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/dsl/exprs.py +0 -0
  104. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/dsl/params.py +0 -0
  105. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute_as_code/README.md +0 -0
  106. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute_as_code/__init__.py +0 -0
  107. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute_as_code/_series.py +0 -0
  108. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/dataset_utils/__init__.py +0 -0
  109. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/extractor/_context.py +0 -0
  110. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/extractor/_env.py +0 -0
  111. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/extractor/_manifest.py +0 -0
  112. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/id_utils/__init__.py +0 -0
  113. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/id_utils/id_utils.py +0 -0
  114. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/impersonation/__init__.py +0 -0
  115. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/ingest/README.md +0 -0
  116. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/ingest/__init__.py +0 -0
  117. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/ingest/_ingest_builder.py +0 -0
  118. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/ingest/_multipart_uploader.py +0 -0
  119. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/ingest/_upload_pacing.py +0 -0
  120. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/logging/__init__.py +0 -0
  121. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/logging/click_log_handler.py +0 -0
  122. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/logging/nominal_log_handler.py +0 -0
  123. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/logging/rich_log_handler.py +0 -0
  124. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/README.md +0 -0
  125. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/__init__.py +0 -0
  126. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/config/__init__.py +0 -0
  127. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/config/migration_data_config.py +0 -0
  128. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/config/migration_resources.py +0 -0
  129. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/dry_run.py +0 -0
  130. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migration_cli.py +0 -0
  131. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migration_decorators.py +0 -0
  132. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migration_runner.py +0 -0
  133. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migration_state.py +0 -0
  134. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migration_summary.py +0 -0
  135. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/__init__.py +0 -0
  136. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/asset_migrator.py +0 -0
  137. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/attachment_migrator.py +0 -0
  138. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/base.py +0 -0
  139. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/checklist_migrator.py +0 -0
  140. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/context.py +0 -0
  141. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/dataset_file_migrator.py +0 -0
  142. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/dataset_migrator.py +0 -0
  143. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/event_migrator.py +0 -0
  144. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/run_migrator.py +0 -0
  145. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/video_file_migrator.py +0 -0
  146. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/video_migrator.py +0 -0
  147. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/workbook_migrator.py +0 -0
  148. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/workbook_template_migrator.py +0 -0
  149. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/parallel_migration_executor.py +0 -0
  150. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/parallel_migration_runner.py +0 -0
  151. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/parallel_migration_state.py +0 -0
  152. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/preregister.py +0 -0
  153. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/resource_type.py +0 -0
  154. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/sample_migration_config.yml +0 -0
  155. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/utils/__init__.py +0 -0
  156. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/utils/conjure_clone_utils.py +0 -0
  157. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/utils/file_utils.py +0 -0
  158. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/utils/retry_utils.py +0 -0
  159. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/utils/video_file_utils.py +0 -0
  160. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/rust_streaming/__init__.py +0 -0
  161. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/rust_streaming/rust_write_stream.py +0 -0
  162. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/stream_v2/__init__.py +0 -0
  163. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/stream_v2/_serializer.py +0 -0
  164. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/stream_v2/_write_stream.py +0 -0
  165. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/video/__init__.py +0 -0
  166. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/video/_video_stream.py +0 -0
  167. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/video_processing/__init__.py +0 -0
  168. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/video_processing/resolution.py +0 -0
  169. {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/video_processing/video_conversion.py +0 -0
  170. {nominal-1.166.0 → nominal-1.167.0}/nominal/py.typed +0 -0
  171. {nominal-1.166.0 → nominal-1.167.0}/nominal/thirdparty/matlab/__init__.py +0 -0
  172. {nominal-1.166.0 → nominal-1.167.0}/nominal/thirdparty/matlab/_matlab.py +0 -0
  173. {nominal-1.166.0 → nominal-1.167.0}/nominal/thirdparty/pandas/__init__.py +0 -0
  174. {nominal-1.166.0 → nominal-1.167.0}/nominal/thirdparty/pandas/_pandas.py +0 -0
  175. {nominal-1.166.0 → nominal-1.167.0}/nominal/thirdparty/polars/__init__.py +0 -0
  176. {nominal-1.166.0 → nominal-1.167.0}/nominal/thirdparty/polars/polars_export_handler.py +0 -0
  177. {nominal-1.166.0 → nominal-1.167.0}/nominal/ts/__init__.py +0 -0
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.167.0](https://github.com/nominal-io/nominal-client/compare/v1.166.0...v1.167.0) (2026-09-10)
4
+
5
+
6
+ ### Features
7
+
8
+ * add markings support for datasets, connections, and videos ([#930](https://github.com/nominal-io/nominal-client/issues/930)) ([9e8503f](https://github.com/nominal-io/nominal-client/commit/9e8503f259a91141d54c8ff87067c0076bd78464))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * restrict plaintext grpc to literal loopback addresses ([#956](https://github.com/nominal-io/nominal-client/issues/956)) ([445f53e](https://github.com/nominal-io/nominal-client/commit/445f53ef66b75be053bbc9c3bde297262d46a88d))
14
+ * use LEGACY backing for read-only dataset fixture ([#965](https://github.com/nominal-io/nominal-client/issues/965)) ([ebf4376](https://github.com/nominal-io/nominal-client/commit/ebf43762935ce870eb0efe6ee77be171e99a29c8))
15
+
3
16
  ## [1.166.0](https://github.com/nominal-io/nominal-client/compare/v1.165.0...v1.166.0) (2026-09-08)
4
17
 
5
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: nominal
3
- Version: 1.166.0
3
+ Version: 1.167.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,8 +21,8 @@ Requires-Dist: click<9,>=8
21
21
  Requires-Dist: conjure-python-client<4,>=3.1.0
22
22
  Requires-Dist: exceptiongroup>=1.3.0
23
23
  Requires-Dist: ffmpeg-python>=0.2.0
24
- Requires-Dist: nominal-api-protos==0.1379.0
25
- Requires-Dist: nominal-api==0.1379.0
24
+ Requires-Dist: nominal-api-protos==0.1437.0
25
+ Requires-Dist: nominal-api==0.1437.0
26
26
  Requires-Dist: nominal-streaming==0.9.6; platform_python_implementation == 'CPython' 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' or platform_machine == 'aarch64' or platform_machine == 'arm64')))
27
27
  Requires-Dist: pandas>=2.0.0
28
28
  Requires-Dist: polars>=1.0.0
@@ -209,6 +209,10 @@ def register_image(
209
209
  nom container extractor set-active-image -r "$EXTRACTOR_RID" -i "$IMAGE_RID"
210
210
 
211
211
  The registered image starts PENDING and must be activated with `set-active-image` once READY.
212
+
213
+ Build the image for amd64 (`docker build --platform linux/amd64`). Nominal runs extractor
214
+ images on amd64 and registration does not check the tarball's architecture, so an arm64 image
215
+ registers and activates, then fails at ingest with an exec format error.
212
216
  """
213
217
  parsed = _parse_config(_load_config(config_file))
214
218
  tag = tag if tag is not None else parsed.tag
@@ -12,7 +12,10 @@ def validate_token_url(token: str, base_url: str, workspace_rid: str | None) ->
12
12
  docs_link = "https://docs.nominal.io/core/sdk/python-client/authentication"
13
13
  status_code = 200
14
14
  err_msg = ""
15
- client = NominalClient.create(base_url, token)
15
+ try:
16
+ client = NominalClient.create(base_url, token)
17
+ except NominalConfigError as err:
18
+ raise click.ClickException(f"Invalid client configuration: {err}") from err
16
19
 
17
20
  # first, validate that the api key is correct for the tenant / org by fetching the
18
21
  # current user using the api key
@@ -24,10 +24,12 @@ from nominal.core.data_review import CheckViolation, DataReview, DataReviewBuild
24
24
  from nominal.core.dataset import Dataset
25
25
  from nominal.core.dataset_file import DatasetFile, IngestWaitType, as_files_ingested, wait_for_files_to_ingest
26
26
  from nominal.core.datasource import DataSource
27
+ from nominal.core.elements import Symbol, SymbolKind
27
28
  from nominal.core.event import Event
28
29
  from nominal.core.filetype import FileType, FileTypes
29
30
  from nominal.core.ingestion_job import IngestionJob, IngestionJobStatus, IngestType
30
31
  from nominal.core.log import LogPoint
32
+ from nominal.core.marking import Marking
31
33
  from nominal.core.run import Run
32
34
  from nominal.core.secret import Secret
33
35
  from nominal.core.unit import Unit, UnitLike
@@ -75,11 +77,14 @@ __all__ = [
75
77
  "LinkDict",
76
78
  "LogPoint",
77
79
  "LogStream",
80
+ "Marking",
78
81
  "Comment",
79
82
  "NominalClient",
80
83
  "Run",
81
84
  "SearchEventOriginType",
82
85
  "Secret",
86
+ "Symbol",
87
+ "SymbolKind",
83
88
  "TimestampMetadata",
84
89
  "Unit",
85
90
  "UnitLike",
@@ -34,8 +34,10 @@ from nominal.core._utils.grpc_tools import GRPCStub, create_grpc_channel, transl
34
34
  from nominal.core._utils.networking import (
35
35
  HeaderProvider,
36
36
  create_conjure_client_factory,
37
+ validate_api_base_url,
37
38
  )
38
39
  from nominal.core.exceptions import NominalConfigError
40
+ from nominal.protos.authorization.markings.v1 import markings_pb2_grpc
39
41
  from nominal.protos.authorization.roles.v1 import roles_pb2_grpc
40
42
  from nominal.protos.comments.v1 import comments_pb2_grpc
41
43
  from nominal.protos.event.v2 import event_pb2_grpc
@@ -173,6 +175,7 @@ class ClientsBunch:
173
175
  containerized_extractor: containerized_extractor_pb2_grpc.ContainerizedExtractorServiceStub
174
176
  event: event_pb2_grpc.EventServiceStub
175
177
  ingest_v2: ingest_service_pb2_grpc.IngestServiceStub
178
+ markings: markings_pb2_grpc.MarkingServiceStub
176
179
  registry: registry_pb2_grpc.RegistryServiceStub
177
180
  roles: roles_pb2_grpc.RoleServiceStub
178
181
  sandbox_workspace: sandbox_workspace_pb2_grpc.SandboxWorkspaceServiceStub
@@ -277,6 +280,7 @@ class ClientsBunch:
277
280
  *,
278
281
  header_provider: HeaderProvider | None = None,
279
282
  ) -> Self:
283
+ validate_api_base_url(base_url)
280
284
  app_base_url = api_base_url_to_app_base_url(base_url)
281
285
 
282
286
  def client_factory(service_class: type[TService]) -> TService:
@@ -336,6 +340,7 @@ class ClientsBunch:
336
340
  containerized_extractor=grpc_factory(containerized_extractor_pb2_grpc.ContainerizedExtractorServiceStub),
337
341
  event=grpc_factory(event_pb2_grpc.EventServiceStub),
338
342
  ingest_v2=grpc_factory(ingest_service_pb2_grpc.IngestServiceStub),
343
+ markings=grpc_factory(markings_pb2_grpc.MarkingServiceStub),
339
344
  registry=grpc_factory(registry_pb2_grpc.RegistryServiceStub),
340
345
  roles=grpc_factory(roles_pb2_grpc.RoleServiceStub),
341
346
  sandbox_workspace=grpc_factory(sandbox_workspace_pb2_grpc.SandboxWorkspaceServiceStub),
@@ -6,7 +6,7 @@ generated stub to, so every stub shares that one channel.
6
6
 
7
7
  The channel is configured to track the conjure HTTP transport as closely as gRPC allows:
8
8
 
9
- - HTTP URLs select plaintext gRPC on any host; HTTPS URLs select TLS.
9
+ - HTTP URLs allow plaintext gRPC only for literal loopback IP addresses; HTTPS URLs select TLS.
10
10
  - For TLS, roots combine the configured trust store and the OS trust store (`_grpc_root_certificates`).
11
11
  - Retry mirrors conjure's `RetryWithJitter` (`_service_config_json`).
12
12
  - Per-call auth metadata and a default deadline are injected by client interceptors, so call sites never
@@ -29,7 +29,7 @@ from urllib.parse import urlparse
29
29
  import grpc
30
30
  from conjure_python_client import ServiceConfiguration
31
31
 
32
- from nominal.core._utils.networking import HeaderProvider, raise_header_conflict
32
+ from nominal.core._utils.networking import HeaderProvider, raise_header_conflict, validate_api_base_url
33
33
  from nominal.core.exceptions import (
34
34
  NominalAlreadyExistsError,
35
35
  NominalAuthenticationError,
@@ -254,12 +254,27 @@ def create_grpc_channel(
254
254
  auth_header: str,
255
255
  header_provider: HeaderProvider | None,
256
256
  ) -> grpc.Channel:
257
- """Build the shared gRPC channel, using plaintext for HTTP URLs and TLS otherwise.
257
+ """Build the shared gRPC channel, allowing plaintext only for loopback HTTP URLs.
258
258
 
259
259
  Configures gzip compression, native retry, lifted message-size limits, the SDK user-agent, and the
260
260
  auth-metadata + default-deadline interceptors. TLS channels use the union trust bundle; plaintext
261
261
  channels do not load TLS credentials. All of a client's gRPC stubs share this one channel.
262
+
263
+ Raises:
264
+ NominalConfigError: If the API URL is malformed, contains user information, or uses HTTP
265
+ without a literal loopback address.
262
266
  """
267
+ channel = _build_grpc_channel(api_base_url, service_config, user_agent)
268
+ # The two interceptors are order-independent: one rewrites metadata, the other the deadline.
269
+ return grpc.intercept_channel(
270
+ channel,
271
+ _AuthMetadataInterceptor(auth_header, header_provider),
272
+ _DefaultDeadlineInterceptor(service_config.read_timeout),
273
+ )
274
+
275
+
276
+ def _build_grpc_channel(api_base_url: str, service_config: ServiceConfiguration, user_agent: str) -> grpc.Channel:
277
+ parsed = validate_api_base_url(api_base_url)
263
278
  options = [
264
279
  ("grpc.primary_user_agent", user_agent),
265
280
  ("grpc.enable_retries", 1),
@@ -267,22 +282,15 @@ def create_grpc_channel(
267
282
  ("grpc.max_send_message_length", _MAX_MESSAGE_LENGTH),
268
283
  ("grpc.max_receive_message_length", _MAX_MESSAGE_LENGTH),
269
284
  ]
270
- target = api_base_url_to_grpc_target(api_base_url)
271
- if urlparse(api_base_url).scheme == "http":
272
- channel = grpc.insecure_channel(target, options=options, compression=grpc.Compression.Gzip)
273
- else:
274
- credentials = grpc.ssl_channel_credentials(
275
- root_certificates=_grpc_root_certificates(
276
- None if service_config.security is None else service_config.security.trust_store_path
277
- )
285
+ target = parsed.netloc
286
+ if parsed.scheme == "http":
287
+ return grpc.insecure_channel(target, options=options, compression=grpc.Compression.Gzip)
288
+ credentials = grpc.ssl_channel_credentials(
289
+ root_certificates=_grpc_root_certificates(
290
+ None if service_config.security is None else service_config.security.trust_store_path
278
291
  )
279
- channel = grpc.secure_channel(target, credentials, options=options, compression=grpc.Compression.Gzip)
280
- # The two interceptors are order-independent: one rewrites metadata, the other the deadline.
281
- return grpc.intercept_channel(
282
- channel,
283
- _AuthMetadataInterceptor(auth_header, header_provider),
284
- _DefaultDeadlineInterceptor(service_config.read_timeout),
285
292
  )
293
+ return grpc.secure_channel(target, credentials, options=options, compression=grpc.Compression.Gzip)
286
294
 
287
295
 
288
296
  _GRPC_STATUS_TO_EXCEPTION: dict[grpc.StatusCode, type[NominalError]] = {
@@ -1,12 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import gzip
4
+ import ipaddress
4
5
  import logging
5
6
  import ssl
6
7
  import threading
7
8
  from abc import ABC, abstractmethod
8
9
  from dataclasses import dataclass
9
10
  from typing import Any, Callable, Mapping, Type, TypeVar
11
+ from urllib.parse import ParseResult, urlparse
10
12
 
11
13
  import requests
12
14
  import truststore
@@ -17,7 +19,7 @@ from requests.adapters import DEFAULT_POOLSIZE, CaseInsensitiveDict, HTTPAdapter
17
19
  from urllib3.connection import HTTPConnection
18
20
  from urllib3.util.retry import Retry
19
21
 
20
- from nominal.core.exceptions import HeaderConflictError
22
+ from nominal.core.exceptions import HeaderConflictError, NominalConfigError
21
23
 
22
24
  logger = logging.getLogger(__name__)
23
25
 
@@ -25,6 +27,48 @@ T = TypeVar("T")
25
27
 
26
28
  GZIP_COMPRESSION_LEVEL = 1
27
29
 
30
+ _LOOPBACK_NETWORKS = (ipaddress.ip_network("127.0.0.0/8"), ipaddress.ip_network("::1/128"))
31
+
32
+
33
+ def _is_loopback_address(host: str | None) -> bool:
34
+ if host is None or "%" in host:
35
+ # Network membership alone does not exclude IPv6 scope identifiers.
36
+ return False
37
+ try:
38
+ address = ipaddress.ip_address(host)
39
+ except ValueError:
40
+ return False
41
+ # Explicit networks keep IPv4-mapped IPv6 excluded regardless of Python's is_loopback semantics.
42
+ return any(address in network for network in _LOOPBACK_NETWORKS)
43
+
44
+
45
+ def validate_api_base_url(api_base_url: str) -> ParseResult:
46
+ """Validate the client-wide URL policy without resolving hostnames.
47
+
48
+ Raises:
49
+ NominalConfigError: If the URL is malformed, contains user information, or uses HTTP
50
+ without a literal loopback address.
51
+ """
52
+ try:
53
+ parsed = urlparse(api_base_url)
54
+ host = parsed.hostname
55
+ parsed.port # Validate the port without including untrusted URL contents in errors.
56
+ except ValueError:
57
+ raise NominalConfigError("Invalid API base URL: check its hostname and port.") from None
58
+ if not parsed.netloc or host is None:
59
+ raise NominalConfigError("Invalid API base URL: a hostname is required.")
60
+ if parsed.username is not None or parsed.password is not None:
61
+ raise NominalConfigError(
62
+ f"API base URL for host {host!r} must not contain user information; pass the token separately."
63
+ )
64
+ if parsed.scheme == "http" and not _is_loopback_address(host):
65
+ raise NominalConfigError(
66
+ f"HTTP API base URL for host {host!r} requires a literal loopback IP address "
67
+ "(127.0.0.0/8 or [::1]). Hostnames are not resolved; use 127.0.0.1 instead of localhost. "
68
+ "Use an https:// API URL for remote deployments."
69
+ )
70
+ return parsed
71
+
28
72
 
29
73
  class HeaderProvider(ABC):
30
74
  @abstractmethod
@@ -21,6 +21,7 @@ from nominal_api import (
21
21
 
22
22
  from nominal.core._utils.grpc_tools import translate_grpc_errors
23
23
  from nominal.core._utils.query_tools import ArchiveStatusFilter
24
+ from nominal.protos.authorization.markings.v1 import markings_pb2, markings_pb2_grpc
24
25
  from nominal.protos.event.v2 import event_pb2, event_pb2_grpc
25
26
  from nominal.protos.ingest.v2 import containerized_extractor_pb2, containerized_extractor_pb2_grpc
26
27
  from nominal.protos.registry.v2 import registry_pb2, registry_pb2_grpc
@@ -243,6 +244,27 @@ def search_runs_by_asset_paginated(
243
244
  yield from response.results
244
245
 
245
246
 
247
+ def search_markings_paginated(
248
+ markings: markings_pb2_grpc.MarkingServiceStub,
249
+ query: markings_pb2.SearchMarkingsQuery,
250
+ ) -> Iterable[markings_pb2.MarkingMetadata]:
251
+ """Yield markings matching a query, oldest first.
252
+
253
+ Unlike its neighbours, the request type carries no sort options: the service always orders by
254
+ creation time ascending. Archived markings are excluded by the service and never appear here.
255
+ """
256
+
257
+ def factory(page_token: str | None) -> markings_pb2.SearchMarkingsRequest:
258
+ return markings_pb2.SearchMarkingsRequest(
259
+ page_size=DEFAULT_PAGE_SIZE,
260
+ query=query,
261
+ next_page_token=page_token,
262
+ )
263
+
264
+ for response in paginate_grpc(markings.SearchMarkings, request_factory=factory):
265
+ yield from response.marking_metadatas
266
+
267
+
246
268
  def search_secrets_paginated(
247
269
  secrets: secrets_pb2_grpc.SecretServiceStub,
248
270
  query: secrets_pb2.SearchSecretsQuery,
@@ -20,6 +20,7 @@ from nominal_api import (
20
20
 
21
21
  from nominal.core._event_types import EventType, SearchEventOriginType
22
22
  from nominal.core._utils.api_tools import rid_from_instance_or_string
23
+ from nominal.protos.authorization.markings.v1 import markings_pb2
23
24
  from nominal.protos.event.v2 import event_pb2
24
25
  from nominal.protos.registry.v2 import registry_pb2
25
26
  from nominal.protos.secrets.v1 import secrets_pb2
@@ -152,6 +153,18 @@ def _backfill_workbook_template_archive_query_clause(
152
153
  raise ValueError(f"Unexpected archive_status for workbook template search: {archive_status}")
153
154
 
154
155
 
156
+ def create_search_markings_query(id_substring: str | None = None) -> markings_pb2.SearchMarkingsQuery:
157
+ """Build a marking search query. With no arguments, matches every marking in the organization."""
158
+ queries = []
159
+ if id_substring is not None:
160
+ queries.append(markings_pb2.SearchMarkingsQuery(id_exact_substring_search=id_substring))
161
+ # `and` is a Python keyword, and generated typing stubs cannot expose it as a named argument.
162
+ # Unlike most search queries in this API, `and` holds a message wrapping the list, not a repeated field.
163
+ return markings_pb2.SearchMarkingsQuery(
164
+ **{"and": markings_pb2.SearchMarkingsQueryList(queries=queries)} # type: ignore[arg-type]
165
+ )
166
+
167
+
155
168
  def create_search_secrets_query(
156
169
  search_text: str | None = None,
157
170
  labels: Sequence[str] | None = None,
@@ -93,6 +93,7 @@ from nominal.core.dataset import (
93
93
  )
94
94
  from nominal.core.dataset_file import DatasetFile
95
95
  from nominal.core.datasource import DataSource
96
+ from nominal.core.elements import Symbol
96
97
  from nominal.core.event import Event, _create_event, _get_event, _get_events, _search_events
97
98
  from nominal.core.exceptions import (
98
99
  LegacyVideoDeprecationWarning,
@@ -103,6 +104,14 @@ from nominal.core.exceptions import (
103
104
  )
104
105
  from nominal.core.filetype import FileType, FileTypes
105
106
  from nominal.core.ingestion_job import IngestionJob, IngestionJobStatus
107
+ from nominal.core.marking import (
108
+ Marking,
109
+ _create_marking,
110
+ _get_marking,
111
+ _get_marking_by_id,
112
+ _marking_rids,
113
+ _search_markings,
114
+ )
106
115
  from nominal.core.run import Run, _create_run
107
116
  from nominal.core.secret import Secret
108
117
  from nominal.core.streaming_checklist import _iter_list_streaming_checklists
@@ -159,6 +168,10 @@ class NominalClient:
159
168
  your corporate CA PEM if you are behind a TLS-inspecting proxy.
160
169
  connect_timeout: Request connection timeout.
161
170
  extra_headers: Extra request headers, either as a mapping or HeaderProvider.
171
+
172
+ Raises:
173
+ NominalConfigError: If the base URL is malformed, contains user information, or uses HTTP
174
+ without a literal loopback IP. This prevents client construction even for HTTP-only usage.
162
175
  """
163
176
  config = NominalConfig.from_yaml()
164
177
  prof = config.get_profile(profile)
@@ -199,6 +212,10 @@ class NominalClient:
199
212
  your corporate CA PEM if you are behind a TLS-inspecting proxy.
200
213
  connect_timeout: Request connection timeout.
201
214
  extra_headers: Extra request headers, either as a mapping or HeaderProvider.
215
+
216
+ Raises:
217
+ NominalConfigError: If the base URL is malformed, contains user information, or uses HTTP
218
+ without a literal loopback IP. This prevents client construction even for HTTP-only usage.
202
219
  """
203
220
  trust_store_path = certifi.where() if trust_store_path is None else trust_store_path
204
221
  timeout_seconds = connect_timeout.total_seconds() if isinstance(connect_timeout, timedelta) else connect_timeout
@@ -244,6 +261,10 @@ class NominalClient:
244
261
  workspace_rid: Optional workspace RID to pin the client to for operations that require a single
245
262
  workspace. If not provided, those operations resolve a default workspace client-side when needed.
246
263
  extra_headers: Extra request headers, either as a mapping or HeaderProvider.
264
+
265
+ Raises:
266
+ NominalConfigError: If the base URL is malformed, contains user information, or uses HTTP
267
+ without a literal loopback IP. This prevents client construction even for HTTP-only usage.
247
268
  """
248
269
  if token is None:
249
270
  token = _config.get_token(base_url)
@@ -537,6 +558,80 @@ class NominalClient:
537
558
  )
538
559
  return list(self._iter_search_secrets(query, archive_status))
539
560
 
561
+ def create_marking(
562
+ self,
563
+ id: str,
564
+ *,
565
+ description: str | None = None,
566
+ authorized_groups: Sequence[str] = (),
567
+ symbol: Symbol | None = None,
568
+ color: str | None = None,
569
+ ) -> Marking:
570
+ """Create a marking in the current organization.
571
+
572
+ Args:
573
+ id: Human-readable identifier for the marking, unique within the organization. Must be
574
+ lowercase alphanumeric characters optionally separated by hyphens, starting with a
575
+ letter, e.g. `export-controlled`.
576
+ description: Human readable description of the marking.
577
+ authorized_groups: RIDs of the groups authorized to access data sources carrying
578
+ this marking.
579
+ symbol: Symbol identifying the marking in the Nominal app.
580
+ color: Six-digit hex color identifying the marking in the Nominal app, e.g. `#cc0000`.
581
+ Either case is accepted; the value is lowercased before being sent.
582
+
583
+ Returns:
584
+ Reference to the created marking.
585
+
586
+ Raises:
587
+ ValueError: If `id` is not a valid marking id, or `color` is not a valid hex color.
588
+ NominalError: If creation fails, including when the caller is not an organization admin
589
+ or a marking with this id already exists.
590
+ """
591
+ return _create_marking(
592
+ self._clients,
593
+ id=id,
594
+ description=description,
595
+ authorized_groups=authorized_groups,
596
+ symbol=symbol,
597
+ color=color,
598
+ )
599
+
600
+ def get_marking(self, rid: str) -> Marking:
601
+ """Retrieve a marking by RID.
602
+
603
+ Raises:
604
+ NominalNotFoundError: If no marking with the given RID exists.
605
+ NominalPermissionDeniedError: If the caller cannot read it.
606
+ """
607
+ return _get_marking(self._clients, rid)
608
+
609
+ def get_marking_by_id(self, id: str) -> Marking:
610
+ """Retrieve a marking by its human-readable id, unique within the organization.
611
+
612
+ Raises:
613
+ ValueError: If `id` is not a valid marking id.
614
+ NominalNotFoundError: If no marking with the given id exists.
615
+ NominalPermissionDeniedError: If the caller cannot read it.
616
+ """
617
+ return _get_marking_by_id(self._clients, id)
618
+
619
+ def search_markings(self, id_substring: str | None = None) -> Sequence[Marking]:
620
+ """Search markings in the current organization, oldest first.
621
+
622
+ Args:
623
+ id_substring: Substring that a marking's id must contain to be included.
624
+
625
+ Returns:
626
+ All markings matching the given conditions.
627
+
628
+ Note:
629
+ Markings are scoped to an organization rather than a workspace, so this search takes no
630
+ workspace filter. Archived markings are never returned; fetch them by RID with
631
+ `get_marking` instead.
632
+ """
633
+ return _search_markings(self._clients, id_substring=id_substring)
634
+
540
635
  def _iter_search_videos(
541
636
  self,
542
637
  query: scout_video_api.SearchVideosQuery,
@@ -768,7 +863,7 @@ class NominalClient:
768
863
  labels: Sequence[str] = (),
769
864
  properties: Mapping[str, str] | None = None,
770
865
  prefix_tree_delimiter: str | None = None,
771
- markings: Sequence[str] | None = None,
866
+ markings: Sequence[Marking | str] | None = None,
772
867
  ) -> Dataset:
773
868
  """Create an empty dataset.
774
869
 
@@ -778,7 +873,8 @@ class NominalClient:
778
873
  labels: Text labels to apply to the created dataset
779
874
  properties: Key-value properties to apply to the cleated dataset
780
875
  prefix_tree_delimiter: If present, the delimiter to represent tiers when viewing channels hierarchically.
781
- markings: If present, RIDs of markings to apply to the created dataset
876
+ markings: If present, markings (or marking RIDs) applied to the dataset. Sent as part of
877
+ the creation request rather than applied in a follow-up call.
782
878
 
783
879
  Returns:
784
880
  Reference to the created dataset in Nominal.
@@ -791,7 +887,7 @@ class NominalClient:
791
887
  labels=labels,
792
888
  properties=properties,
793
889
  workspace_rid=self._clients.resolve_default_workspace_rid(),
794
- marking_rids=markings,
890
+ marking_rids=_marking_rids(markings),
795
891
  )
796
892
  dataset = Dataset._from_conjure(self._clients, response)
797
893
 
@@ -812,6 +908,7 @@ class NominalClient:
812
908
  description: str | None = None,
813
909
  labels: Sequence[str] = (),
814
910
  properties: Mapping[str, str] | None = None,
911
+ markings: Sequence[Marking | str] | None = None,
815
912
  ) -> Video:
816
913
  """Create an empty video to append video files to.
817
914
 
@@ -820,6 +917,8 @@ class NominalClient:
820
917
  description: Description of the video to create in nominal
821
918
  labels: Labels to apply to the video in nominal
822
919
  properties: Properties to apply to the video in nominal
920
+ markings: If present, markings (or marking RIDs) applied to the video. Sent as part of
921
+ the creation request rather than applied in a follow-up call.
823
922
 
824
923
  Returns:
825
924
  Handle to the created video
@@ -832,6 +931,7 @@ class NominalClient:
832
931
  labels=labels,
833
932
  properties=properties,
834
933
  workspace_rid=self._clients.resolve_default_workspace_rid(),
934
+ marking_rids=_marking_rids(markings),
835
935
  )
836
936
  return Video._from_conjure(self._clients, response)
837
937
 
@@ -1077,6 +1177,7 @@ class NominalClient:
1077
1177
  datasource_description: str | None = None,
1078
1178
  *,
1079
1179
  required_tag_names: list[str] | None = None,
1180
+ markings: Sequence[Marking | str] | None = None,
1080
1181
  ) -> StreamingConnection:
1081
1182
  workspace_rid = self._clients.resolve_default_workspace_rid()
1082
1183
  datasource_response = self._clients.storage.create(
@@ -1109,7 +1210,7 @@ class NominalClient:
1109
1210
  available_tag_values={},
1110
1211
  should_scrape=True,
1111
1212
  workspace=workspace_rid,
1112
- marking_rids=[],
1213
+ marking_rids=_marking_rids(markings),
1113
1214
  ),
1114
1215
  )
1115
1216
  conn = Connection._from_conjure(self._clients, connection_response)
@@ -161,8 +161,12 @@ class ContainerizedExtractor(HasRid, RefreshableGrpcMixin[containerized_extracto
161
161
  This extractor instance is unmodified. Tags are immutable — registering an already-registered
162
162
  tag raises `NominalAlreadyExistsError`.
163
163
 
164
+ Nominal runs extractor images on amd64, so build with `--platform linux/amd64`. Registration
165
+ does not check the tarball's architecture: an arm64 image (the default on Apple Silicon)
166
+ registers and activates, then fails at ingest with an exec format error.
167
+
164
168
  Args:
165
- tarball: Path to a `docker save` tarball of the extractor image.
169
+ tarball: Path to a `docker save` tarball of the extractor image, built for linux/amd64.
166
170
  tag: Tag to register the image under.
167
171
  inputs: Input files the extractor consumes.
168
172
  default_timestamp_column: Name of the column containing timestamp data in the extractor's
@@ -947,8 +947,14 @@ class Dataset(DataSource, RefreshableConjureMixin[scout_catalog.EnrichedDataset]
947
947
  """Add data from proprietary data formats using a pre-registered custom extractor.
948
948
 
949
949
  A containerized extraction runs asynchronously and may emit multiple output files, so this returns
950
- the tracking `IngestionJob` immediately rather than a single file. Use `job.as_files_ingested()` or
951
- `job.dataset_files()` to pull the produced files, `job.status` to poll, and `job.cancel()` to cancel.
950
+ the tracking `IngestionJob` immediately rather than a single file, and `job.cancel()` cancels it.
951
+
952
+ Waiting takes two stages: the container finishes producing files, then those files finish
953
+ ingesting. `job.as_files_ingested()` covers both — it re-reads the file list while the job runs,
954
+ so it picks up outputs that do not exist yet at the time of the call. `job.dataset_files()`, by
955
+ contrast, returns only the files that exist when it is called, which is empty until a manifest
956
+ extractor's container has exited; and `job.status` is a snapshot, so polling it requires
957
+ `job.refresh()` to advance it.
952
958
 
953
959
  Args:
954
960
  extractor: ContainerizedExtractor instance (or rid of one) to use for extracting and ingesting data.
@@ -1565,6 +1571,7 @@ def _create_dataset(
1565
1571
  marking_rids: Sequence[str] | None = None,
1566
1572
  ) -> scout_catalog.EnrichedDataset:
1567
1573
  request = scout_catalog.CreateDataset(
1574
+ channel_search_split_tag_keys=[],
1568
1575
  name=name,
1569
1576
  description=description,
1570
1577
  labels=list(labels),