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.
- {nominal-1.166.0 → nominal-1.167.0}/CHANGELOG.md +13 -0
- {nominal-1.166.0 → nominal-1.167.0}/PKG-INFO +3 -3
- {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/container.py +4 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/util/verify_connection.py +4 -1
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/__init__.py +5 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_clientsbunch.py +5 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/grpc_tools.py +25 -17
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/networking.py +45 -1
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/pagination_tools.py +22 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/query_tools.py +13 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/client.py +105 -4
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/containerized_extractor.py +5 -1
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/dataset.py +9 -2
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/dataset_file.py +94 -51
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/datasource.py +5 -1
- nominal-1.167.0/nominal/core/elements.py +103 -0
- nominal-1.167.0/nominal/core/ingestion_job.py +337 -0
- nominal-1.167.0/nominal/core/marking.py +316 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/video.py +7 -2
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute_as_code/_derived_datasets.py +7 -2
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/dataset_utils/_dataset_utils.py +7 -2
- nominal-1.167.0/nominal/experimental/extractor/README.md +457 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/extractor/__init__.py +9 -5
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/extractor/_runner.py +18 -8
- {nominal-1.166.0 → nominal-1.167.0}/pyproject.toml +3 -3
- nominal-1.166.0/nominal/core/ingestion_job.py +0 -192
- nominal-1.166.0/nominal/experimental/extractor/README.md +0 -293
- {nominal-1.166.0 → nominal-1.167.0}/.gitignore +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/LICENSE +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/NOTICE +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/README.md +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/_utils/README.md +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/_utils/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/_utils/dataclass_tools.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/_utils/deprecation_tools.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/_utils/iterator_tools.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/_utils/streaming_tools.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/_utils/timing_tools.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/__main__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/attachment.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/config.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/dataset.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/download.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/mis.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/run.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/util/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/util/format.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/cli/util/global_decorators.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/config/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/config/_config.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_checklist_types.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_constants.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_event_types.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_stream/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_stream/batch_processor.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_stream/batch_processor_proto.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_stream/write_stream.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_stream/write_stream_base.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_types.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/README.md +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/api_tools.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/filenames.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/frontend_urls.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/multipart.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/multipart_downloader.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_utils/queueing.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/_video_types.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/asset.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/attachment.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/bounds.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/channel.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/checklist.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/comment.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/connection.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/container_image.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/data_review.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/event.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/exceptions.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/filetype.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/log.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/run.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/secret.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/streaming_checklist.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/unit.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/user.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/video_dataset_file.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/video_file.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/workbook.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/workbook_template.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/core/workspace.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/checklist_utils/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/checklist_utils/checklist_utils.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/README.md +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/_buckets.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/dsl/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/dsl/_enum_expr_impls.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/dsl/_numeric_expr_impls.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/dsl/_range_expr_impls.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/dsl/exprs.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute/dsl/params.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute_as_code/README.md +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute_as_code/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/compute_as_code/_series.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/dataset_utils/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/extractor/_context.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/extractor/_env.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/extractor/_manifest.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/id_utils/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/id_utils/id_utils.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/impersonation/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/ingest/README.md +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/ingest/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/ingest/_ingest_builder.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/ingest/_multipart_uploader.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/ingest/_upload_pacing.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/logging/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/logging/click_log_handler.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/logging/nominal_log_handler.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/logging/rich_log_handler.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/README.md +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/config/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/config/migration_data_config.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/config/migration_resources.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/dry_run.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migration_cli.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migration_decorators.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migration_runner.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migration_state.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migration_summary.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/asset_migrator.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/attachment_migrator.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/base.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/checklist_migrator.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/context.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/dataset_file_migrator.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/dataset_migrator.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/event_migrator.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/run_migrator.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/video_file_migrator.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/video_migrator.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/workbook_migrator.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/migrator/workbook_template_migrator.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/parallel_migration_executor.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/parallel_migration_runner.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/parallel_migration_state.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/preregister.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/resource_type.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/sample_migration_config.yml +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/utils/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/utils/conjure_clone_utils.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/utils/file_utils.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/utils/retry_utils.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/migration/utils/video_file_utils.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/rust_streaming/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/rust_streaming/rust_write_stream.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/stream_v2/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/stream_v2/_serializer.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/stream_v2/_write_stream.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/video/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/video/_video_stream.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/video_processing/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/video_processing/resolution.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/experimental/video_processing/video_conversion.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/py.typed +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/thirdparty/matlab/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/thirdparty/matlab/_matlab.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/thirdparty/pandas/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/thirdparty/pandas/_pandas.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/thirdparty/polars/__init__.py +0 -0
- {nominal-1.166.0 → nominal-1.167.0}/nominal/thirdparty/polars/polars_export_handler.py +0 -0
- {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.
|
|
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.
|
|
25
|
-
Requires-Dist: nominal-api==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
|
-
|
|
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
|
|
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,
|
|
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 =
|
|
271
|
-
if
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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,
|
|
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
|
|
951
|
-
|
|
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),
|