diff --git a/.github/workflows/check_examples.yml b/.github/workflows/check_examples.yml index aef8f49..2ca7d1f 100644 --- a/.github/workflows/check_examples.yml +++ b/.github/workflows/check_examples.yml @@ -15,7 +15,7 @@ jobs: os: [windows-latest, ubuntu-latest, macos-latest] # Pin to 3.14.8 to workaround AB#4147280 python-version: [3.14.8] - example-name: ['overview', 'system'] + example-name: ['overview', 'overview-v2', 'system'] runs-on: ${{ matrix.os }} steps: - name: Check out repo @@ -29,6 +29,8 @@ jobs: python-version: ${{ matrix.python-version }} - name: Set up Poetry uses: ni/python-actions/setup-poetry@dee640bba235ae28fdc6b7337c643bd06358ae90 # v0.9.0 + with: + poetry-version: '2.5.1' - name: Analyze Python Project uses: ni/python-actions/analyze-project@dee640bba235ae28fdc6b7337c643bd06358ae90 # v0.9.0 with: diff --git a/.github/workflows/check_ni_datastore.yml b/.github/workflows/check_ni_datastore.yml index 6e75695..2a9f48d 100644 --- a/.github/workflows/check_ni_datastore.yml +++ b/.github/workflows/check_ni_datastore.yml @@ -27,6 +27,8 @@ jobs: python-version: ${{ matrix.python-version }} - name: Set up Poetry uses: ni/python-actions/setup-poetry@dee640bba235ae28fdc6b7337c643bd06358ae90 # v0.9.0 + with: + poetry-version: '2.5.1' - name: Analyze Python Project uses: ni/python-actions/analyze-project@dee640bba235ae28fdc6b7337c643bd06358ae90 # v0.9.0 - name: Bandit security checks diff --git a/.github/workflows/run_unit_tests.yml b/.github/workflows/run_unit_tests.yml index d50ae68..0b9c742 100644 --- a/.github/workflows/run_unit_tests.yml +++ b/.github/workflows/run_unit_tests.yml @@ -36,5 +36,7 @@ jobs: python-version: ${{ inputs.python-version }} - name: Set up Poetry uses: ni/python-actions/setup-poetry@dee640bba235ae28fdc6b7337c643bd06358ae90 # v0.9.0 + with: + poetry-version: '2.5.1' - name: Run and upload unit tests for ni.datastore uses: ./.github/actions/run_and_upload_unit_tests \ No newline at end of file diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 5f3f038..c780c41 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -8,5 +8,6 @@ This section provides comprehensive documentation for the NI Data Store system c :maxdepth: 3 ni-data-store + ni-data-store-v2 ni-metadata-store using-measurement-data-services \ No newline at end of file diff --git a/docs/reference/ni-data-store-v2.md b/docs/reference/ni-data-store-v2.md new file mode 100644 index 0000000..fdd04e7 --- /dev/null +++ b/docs/reference/ni-data-store-v2.md @@ -0,0 +1,185 @@ +# NI Data Store V2 Python API + +Use the separate V2 client when you need the V2 service model. Like the V1 +client, it uses Python types (`TestResult`, `Step`, `Measurement`, `Condition`, +`ErrorInformation`, `Outcome`, and `Status`) and constructs the gRPC request +messages internally. + +```python +from datetime import timezone + +from hightime import datetime +from ni.datastore.data.v2 import ( + Condition, + DataStoreClient, + Measurement, + Outcome, + Status, + Step, + TestResult, +) + +with DataStoreClient() as client: + test_result = client.create_test_result( + TestResult("Power test", start_date_time=datetime.now(tz=timezone.utc)) + ) + step = client.create_step( + Step("Measure voltage", test_result_id=test_result.id, status=Status.RUNNING) + ) + client.add_condition( + step.id, Condition("supply-voltage", 5.0, condition_type="Nominal", units="V") + ) + measurement = client.create_measurement( + "voltage", + 5.02, + step.id, + units="V", + outcome=Outcome.PASSED, + ) + voltage = client.read_measurement_value(measurement) +``` + +Each create method accepts either the object or its fields as arguments, so +`client.create_step("Measure voltage", test_result_id=test_result.id)` and +`client.create_measurement(Measurement(name="voltage", value=5.02, step_id=step.id))` +also work. `TestResultCreateFields` and `StepCreateFields` type the keyword fields. +Create methods return the created object as saved by the service, including its +generated ID. Nested resources, such as the steps of a test result, and expanded +metadata are not included. The client also provides `get_test_result`, `get_step`, +`query_test_results`, `query_steps`, `query_measurements`, and update methods for +test results, steps, and measurements. These return the Python types, with +timestamps as `hightime.datetime`, durations as `hightime.timedelta`, and +extensions as dictionaries. Queries accept an OData expression and optional +`read_fields`. Read fields are selected with `TestResultField`, `StepField`, or +`MeasurementField`, for example `read_fields=[StepField.CONDITIONS]`; field names +as strings are also accepted. + +Updates change only the fields named in the protobuf update mask, and the client +builds the mask for you. Pass a `Step`, or its ID, with keyword fields, an update +builder, or the whole object: + +```python +from ni.datastore.data.v2 import Outcome, StepUpdateBuilder + +# Keyword fields: only these fields change. None clears a field. +data_store_client.update_step(step, notes="Verified", outcome=Outcome.PASSED) + +# Update builder: each with_ call selects one field, like the .NET client builders. +update = StepUpdateBuilder(step).with_notes("Verified").with_outcome(Outcome.PASSED) +data_store_client.update_step(update) + +# Whole object: every updatable field is set from the object, so empty fields are cleared. +step.notes = "Verified" +data_store_client.update_step(step) +``` + +Keyword fields are typed by `TestResultUpdateFields`, `StepUpdateFields`, and +`MeasurementUpdateFields`, so type checkers report unknown fields and wrong value +types. `TestResultUpdateBuilder`, `StepUpdateBuilder`, and +`MeasurementUpdateBuilder` follow the .NET builders: setting a relationship by ID, +such as `with_operator_id`, replaces inline metadata set earlier, and inline +metadata replaces an ID. A whole-object update sends Stage 2 fields (`keywords`, +`duration`, and `uut_id`/`uut`) only when they have a value, and sends only the ID +when a relationship has both an ID and inline metadata. Empty updates and fields +that the service does not allow to change raise `ValueError` before an RPC is made. + +IDs can be passed as `str` or `uuid.UUID`, and reads return them as `str`. +Test result, step, and measurement IDs must be UUIDs. Metadata ID fields, such as +`operator_id` or `software_item_ids`, also accept registered aliases. + +`delete_test_result`, `delete_step`, and `delete_measurement` remove resources. +They and the batch delete methods accept the objects or their IDs. +Without `force=True`, deleting a test result with steps, or a step with +measurements, fails with `FAILED_PRECONDITION`; with `force=True`, the owned +children are deleted too. A step that has child steps cannot be deleted. + +The client wraps every method of the V2 low-level client: + +- `remove_condition` removes a condition from a step. +- `add_test_result_metadata`, `remove_test_result_metadata`, + `add_measurement_metadata`, and `remove_measurement_metadata` link or unlink + software items, hardware items, and test adapters after creation. A + measurement can only link items that are linked to its test result, and each + measurement keeps its own selection. Linking an item to a test result does not + link it to the test result's measurements. An item cannot be unlinked from a + test result while a measurement of that test result still references it; + unlink it from those measurements first. +- `create_measurement` accepts `upper_limit` and `lower_limit` of any supported + value type. The service stores the limits as given and does not compare them + with the value. `read_measurement_values` returns the value and both limits; + select fields with `MeasurementValueField`. `read_measurement_value` and + `read_measurement_values` accept a `Measurement` or its ID. +- `batch_create_*`, `batch_update_*`, and `batch_delete_*` handle many test + results, steps, or measurements in one atomic call. Batch updates take a + dictionary of fields keyed by ID, or a list of builders and objects. Batch + deletes accept one `force` flag or one flag per ID. +- `TestResult.steps`, `Step.children`, and `Step.measurements` create a whole + tree in one call. Metadata objects such as `Operator` or `SoftwareItem` can be + passed instead of IDs to find or create them. +- `create_measurement` accepts `SoftwareItem`, `HardwareItem`, and + `TestAdapter` objects for inline find-or-create, in addition to their IDs. +- `query_*` methods return `QueryResults`, a list with a `next_link` attribute + that is set when the service has more results. +- JSON values and conditions accept a plain dictionary, or `JsonValue` when the + value needs attributes. Reads return a `JsonValue` only when attributes are + present. + +## V1 Convenience Differences + +- V1 `publish_condition` and `publish_condition_batch` do not have direct V2 + equivalents. V2 conditions belong to a step; use `add_condition(step_id, ...)` + to add or replace one. Read conditions through the containing step returned + by `get_step` or `query_steps`; there are no standalone V2 condition get/query + methods in this high-level API. +- V1 `publish_measurement_batch` is shaped for parametric sweeps. In V2, use + `batch_create_measurements` with `Measurement` objects; each item can target a + different step, and the whole batch is atomic. +- V2 has its own `TestResult`, `Step`, `Measurement`, `Condition`, and + `ErrorInformation` types in `ni.datastore.data.v2`, because the V2 resource + fields differ from V1. They follow the V1 style: create methods take them, + and get/query/update methods return them instead of protobuf messages. +- V2 create and update methods accept `hightime.datetime` timestamps and + `hightime.timedelta` durations, converted with the NI precision converters. + Timestamps must use `datetime.timezone.utc`; other time zones are rejected. + Round trips are exact to about 1 ns; finer precision is not preserved. +- Test results and steps accept `keywords`, and test results accept `uut_id` + updates. Keywords, duration, and `uut_id` are stored once the service delivers + the Stage 2 New Resource Fields feature. +- Measurement values are converted to scalar, vector, supported NI + waveform-family, or JSON values; `read_measurement_value` converts them back to + Python or nitypes values. The `MeasurementValue` type lists the values that can + be written, and `MeasurementReadValue` lists the values that can be read. A + single value is always read back as a `Scalar`, + with or without units, and a list of values as a `Vector`. When a `Scalar` is + given and `units` is not, the measurement's `units` are taken from the + `Scalar`. Conditions accept scalar or JSON values. A scalar condition value + reads back as a plain Python value, or as a `Scalar` when it has extended + properties other than units. JSON values + require the service's Stage 3 Structured JSON Storage And Query Support. +- Software item, hardware item, and test adapter IDs on a measurement must first + be linked to its test result; pass them to `create_test_result`. +- Arguments that take several strings, such as `keywords`, item IDs, + `read_fields`, and the IDs passed to batch deletes, raise `TypeError` when + given a single string. Pass a list, for example `keywords=["smoke"]`, so a + string is not split into its characters. +- Discovery, channel configuration, and cleanup are delegated to the low-level + V2 client. Its gRPC transport exceptions pass through unchanged; invalid + wrapper values and update fields fail locally with `TypeError` or + `ValueError`. As in V1, calling a method after `close()` raises + `RuntimeError`. +- V1 and V2 hosts both register the metadata store interface. When both run in + the same discovery cluster, `MetadataStoreClient()` cannot choose between + them; pass a `grpc_channel` for the intended host. +- `status` and `outcome` are separate, but the service requires status `DONE` + (or no status) whenever a test result or step has an outcome other than + `UNSPECIFIED`. For example, `status=Status.RUNNING` with + `outcome=Outcome.FAILED` is rejected with `INVALID_ARGUMENT`. +- When you pass your own `grpc_channel`, gRPC limits received messages to 4 MB + by default, so reading a large value such as a 1-million-sample waveform fails + with `RESOURCE_EXHAUSTED`. Create the channel with + `options=[("grpc.max_receive_message_length", -1)]` to remove the limit. The + default discovery path already does this. +- Waveform values read back with the same data, but their timing start time is a + `nitypes.bintime.DateTime` rather than a `hightime.datetime`. As a result, `==` + between a published waveform and the one read back returns `False`; compare + the data, such as `raw_data`, instead. diff --git a/examples/notebooks/poetry.lock b/examples/notebooks/poetry.lock index 16bedb3..d43f875 100644 --- a/examples/notebooks/poetry.lock +++ b/examples/notebooks/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand. [[package]] name = "anyio" @@ -885,7 +885,7 @@ comm = ">=0.1.1" debugpy = ">=1.6.5" ipython = ">=7.23.1" jupyter-client = ">=8.9.0" -jupyter-core = ">=5.1,<6.0.dev0 || >=6.1.dev0" +jupyter-core = ">=5.1,<6.0 || >=6.1.dev0" matplotlib-inline = ">=0.1" nest-asyncio2 = ">=1.7.0" packaging = ">=22" @@ -920,7 +920,7 @@ comm = ">=0.1.1" debugpy = ">=1.6.5" ipython = ">=7.23.1" jupyter-client = ">=8.9.0" -jupyter-core = ">=5.1,<6.0.dev0 || >=6.1.dev0" +jupyter-core = ">=5.1,<6.0 || >=6.1.dev0" matplotlib-inline = ">=0.1" nest-asyncio2 = ">=1.7.0" packaging = ">=22" @@ -1243,7 +1243,7 @@ files = [ ipykernel = ">=6.14" ipython = "*" jupyter-client = ">=7.0.0" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-core = ">=4.12,<5.0 || >=5.1.dev0" prompt-toolkit = ">=3.0.30" pygments = "*" pyzmq = ">=17" @@ -1331,7 +1331,7 @@ anyio = ">=3.1.0" argon2-cffi = ">=21.1" jinja2 = ">=3.0.3" jupyter-client = ">=7.4.4" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-core = ">=4.12,<5.0 || >=5.1.dev0" jupyter-events = ">=0.11.0" jupyter-server-terminals = ">=0.4.4" nbconvert = ">=6.4.4" @@ -1339,7 +1339,7 @@ nbformat = ">=5.3.0" overrides = {version = ">=5.0", markers = "python_version < \"3.12\""} packaging = ">=22.0" prometheus-client = ">=0.9" -pywinpty = {version = ">=2.0.1,<3.0.4 || >3.0.4", markers = "os_name == \"nt\""} +pywinpty = {version = ">=2.0.1,!=3.0.4", markers = "os_name == \"nt\""} pyzmq = ">=24" send2trash = ">=1.8.2" terminado = ">=0.8.3" @@ -1386,7 +1386,7 @@ files = [ [package.dependencies] async-lru = ">=1.0.0" httpx = ">=0.25.0,<1" -ipykernel = ">=6.5.0,<6.30.0 || >6.30.0" +ipykernel = ">=6.5.0,!=6.30.0" jinja2 = ">=3.0.3" jupyter-builder = ">=1.0.2" jupyter-core = "*" @@ -1769,7 +1769,7 @@ files = [ [package.dependencies] fastjsonschema = ">=2.15" jsonschema = ">=2.6" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-core = ">=4.12,<5.0 || >=5.1.dev0" traitlets = ">=5.1" [package.extras] @@ -1805,7 +1805,7 @@ protobuf = ">=4.21" [[package]] name = "ni-datastore" -version = "2.2.0.dev0" +version = "3.0.0.dev0" description = "APIs for publishing and retrieving data from NI Measurement Data Services" optional = false python-versions = "^3.10" @@ -1816,6 +1816,8 @@ develop = true [package.dependencies] hightime = ">=1.0.0" ni-measurements-data-v1-client = ">=1.2.0.dev0" +ni-measurements-data-v2-client = {git = "https://github.com/ni/ni-apis-python.git", branch = "users/sudharshana/add-v2-proto-and-client-package", subdirectory = "packages/ni.measurements.data.v2.client"} +ni-measurements-data-v2-proto = {git = "https://github.com/ni/ni-apis-python.git", branch = "users/sudharshana/add-v2-proto-and-client-package", subdirectory = "packages/ni.measurements.data.v2.proto"} ni-measurements-metadata-v1-client = ">=1.0.0" ni-protobuf-types = ">=1.3.0.dev0" protobuf = ">=4.21" @@ -1907,6 +1909,53 @@ ni-measurements-metadata-v1-proto = ">=1.0.0" ni-protobuf-types = ">=1.3.0" protobuf = ">=4.21" +[[package]] +name = "ni-measurements-data-v2-client" +version = "1.0.0.dev0" +description = "gRPC Client for NI Data Store Service V2" +optional = false +python-versions = ">=3.10,<4.0" +groups = ["main"] +files = [] +develop = false + +[package.dependencies] +grpcio = ">=1.49.1,<2.0" +ni-grpc-extensions = ">=1.1.0" +ni-measurementlink-discovery-v1-client = ">=1.1.0" +ni-measurements-data-v2-proto = ">=1.0.0.dev0" + +[package.source] +type = "git" +url = "https://github.com/ni/ni-apis-python.git" +reference = "users/sudharshana/add-v2-proto-and-client-package" +resolved_reference = "28aee05fe1ce46d2d349311cd600bf0a47937d1d" +subdirectory = "packages/ni.measurements.data.v2.client" + +[[package]] +name = "ni-measurements-data-v2-proto" +version = "1.0.0.dev0" +description = "Protobuf data types and service stubs for NI data store V2 gRPC APIs" +optional = false +python-versions = ">=3.10,<4.0" +groups = ["main"] +files = [] +develop = false + +[package.dependencies] +grpcio = ">=1.49.1" +ni-datamonikers-v1-proto = ">=1.0.0" +ni-measurements-metadata-v1-proto = ">=1.0.0" +ni-protobuf-types = ">=1.3.0" +protobuf = ">=4.21" + +[package.source] +type = "git" +url = "https://github.com/ni/ni-apis-python.git" +reference = "users/sudharshana/add-v2-proto-and-client-package" +resolved_reference = "28aee05fe1ce46d2d349311cd600bf0a47937d1d" +subdirectory = "packages/ni.measurements.data.v2.proto" + [[package]] name = "ni-measurements-metadata-v1-client" version = "1.0.0" diff --git a/examples/notebooks/v2/README.md b/examples/notebooks/v2/README.md new file mode 100644 index 0000000..cc1010d --- /dev/null +++ b/examples/notebooks/v2/README.md @@ -0,0 +1,45 @@ +# V2 Example Notebooks + +The Jupyter Notebooks in the sub-directories of this directory demonstrate the +V2 Measurement Data Store Python API, `ni.datastore.data.v2`. They are the V2 +companions of the notebooks in the parent directory. + +V2 reuses the V1 Metadata API, so these notebooks create and read metadata with +`ni.datastore.metadata`, as the V1 notebooks do. + +## Setup + +These notebooks use the dedicated Python environment configured for the +notebooks. To create it, run the following commands from the repository root: + +```powershell +cd examples\notebooks +poetry install +``` + +This creates one environment containing the dependencies used by all notebooks, +including the local `ni.datastore` package and notebook utilities. Select the +environment it creates as the kernel when executing any of the provided +notebooks. + +The notebooks need NI Measurement Data Services running in V2 mode. By +default, the clients use discovery to find the service. To connect to a specific +service instance, set the `MDS_V2_GRPC_TARGET` environment variable to its gRPC +target, such as `[::1]:59606`, before starting Jupyter. + +## Notebooks + +- `overview/publish_measurement.ipynb`: Publish a waveform measurement with a + condition, then query and read it back. +- `query/publish_sample_data.ipynb`: Create sample data for the query notebook. + Run this first. +- `query/query_data.ipynb`: Query test results, steps, measurements, and + conditions with OData. +- `batch-create/batch_create.ipynb`: Create test results, steps, and measurements in + atomic batches, including nested steps and measurements. +- `measurement-values/measurement_values.ipynb`: Scalar and non-scalar + measurement values, and limits. +- `update-delete/update_delete.ipynb`: Update fields, change conditions, link + metadata, and delete data. +- `structured-extensions/structured_extensions.ipynb`: Store nested JSON data in + extensions. diff --git a/examples/notebooks/v2/batch-create/batch_create.ipynb b/examples/notebooks/v2/batch-create/batch_create.ipynb new file mode 100644 index 0000000..c035e2f --- /dev/null +++ b/examples/notebooks/v2/batch-create/batch_create.ipynb @@ -0,0 +1,302 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "706c1b5b", + "metadata": {}, + "source": [ + "# Batch Creating\n", + "\n", + "This notebook is the V2 companion of `../../publish/publish_batch.ipynb`. It shows how to create many test results, steps, and measurements with one call.\n", + "\n", + "V2 does not have `publish_condition_batch` or `publish_measurement_batch`. Instead, it has:\n", + "- `batch_create_test_results`: creates test results, and any steps, conditions, and measurements nested inside them.\n", + "- `batch_create_steps`: creates steps, and any child steps, conditions, and measurements nested inside them.\n", + "- `batch_create_measurements`: creates measurements. Each measurement may target a different step.\n", + "\n", + "Each batch call is **atomic**: either every item is created, or nothing is created." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95b3abe1", + "metadata": {}, + "outputs": [], + "source": [ + "# Perform example-specific setup. This is not needed when writing production code.\n", + "# Set MDS_V2_GRPC_TARGET (for example, \"[::1]:59606\") to connect to a specific V2 service.\n", + "# When it is not set, the clients use discovery to find the service.\n", + "import os\n", + "\n", + "import grpc\n", + "\n", + "grpc_target = os.environ.get(\"MDS_V2_GRPC_TARGET\", \"\")\n", + "# Remove gRPC's 4 MB receive limit so that large values, such as long waveforms, can be read.\n", + "grpc_options = [(\"grpc.max_receive_message_length\", -1)]\n", + "grpc_channel = grpc.insecure_channel(grpc_target, options=grpc_options) if grpc_target else None\n", + "\n", + "from ni.datastore.data.v2 import DataStoreClient\n", + "\n", + "data_store_client = DataStoreClient(grpc_channel=grpc_channel)" + ] + }, + { + "cell_type": "markdown", + "id": "6e3908b8", + "metadata": {}, + "source": [ + "## Create a Test Result with Nested Steps, Measurements, and Metadata\n", + "\n", + "A test result can include its steps, and a step can include its conditions, measurements, and child steps. One `batch_create_test_results` call creates the whole tree.\n", + "\n", + "Metadata, such as the operator, test station, software items, and the test a step runs, can be written inline as objects. The service finds a matching record or creates a new one in the same call, so there is no need to create metadata first. Create metadata separately only when you want to share the same records across many test results by ID." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e31d9cdf", + "metadata": {}, + "outputs": [], + "source": [ + "from ni.datastore.data.v2 import Condition, Measurement, Step, TestResult\n", + "from ni.datastore.metadata import HardwareItem, Operator, SoftwareItem, Test, TestStation\n", + "\n", + "test_result = TestResult(\n", + " name=\"Batch Create Example\",\n", + " operator=Operator(name=\"Example Operator\", role=\"Test Engineer\"),\n", + " test_station=TestStation(name=\"Example Station\"),\n", + " software_items=[SoftwareItem(product=\"Example Driver\", version=\"1.0.0\")],\n", + " hardware_items=[HardwareItem(manufacturer=\"NI\", model=\"PXIe-4081\", serial_number=\"EX-001\")],\n", + " steps=[\n", + " Step(\n", + " name=\"Example Parent Step\",\n", + " test=Test(name=\"Example Voltage Test\", description=\"Checks the supply voltage.\"),\n", + " conditions=[Condition(\"Ambient Temperature\", 25.0, condition_type=\"Environment\", units=\"degC\")],\n", + " measurements=[Measurement(name=\"Example Parent Measurement\", value=1.0, units=\"V\")],\n", + " children=[\n", + " Step(\n", + " name=\"Example Child Step\",\n", + " measurements=[Measurement(name=\"Example Child Measurement\", value=2.0, units=\"V\")],\n", + " ),\n", + " ],\n", + " ),\n", + " ],\n", + ")\n", + "\n", + "[created_test_result] = data_store_client.batch_create_test_results([test_result])\n", + "test_result_id = created_test_result.id\n", + "print(f\"Created Test Result: {test_result_id}\")\n", + "print(f\"Linked operator: {created_test_result.operator_id}\")\n", + "print(f\"Linked test station: {created_test_result.test_station_id}\")\n", + "print(f\"Linked software items: {list(created_test_result.software_item_ids)}\")\n", + "print(f\"Linked hardware items: {list(created_test_result.hardware_item_ids)}\")\n", + "\n", + "created_steps = data_store_client.query_steps(f\"$filter=TestResultId eq {test_result_id}\")\n", + "created_measurements = data_store_client.query_measurements(f\"$filter=TestResultId eq {test_result_id}\")\n", + "for step in created_steps:\n", + " kind = \"child\" if step.parent_step_id else \"parent\"\n", + " test_text = f\", test: {step.test_id}\" if step.test_id else \"\"\n", + " print(f\"Created {kind} step: {step.name}{test_text}\")\n", + "for measurement in created_measurements:\n", + " print(f\"Created measurement: {measurement.name} = {measurement.value}\")\n", + "\n", + "assert created_test_result.operator_id\n", + "assert len(created_test_result.software_item_ids) == 1\n", + "assert len(created_steps) == 2\n", + "assert len(created_measurements) == 2" + ] + }, + { + "cell_type": "markdown", + "id": "2d4458a2", + "metadata": {}, + "source": [ + "## Batch Creating Steps\n", + "\n", + "`batch_create_steps` creates multiple steps at once. Each step names the test result that owns it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6486e64a", + "metadata": {}, + "outputs": [], + "source": [ + "new_steps = data_store_client.batch_create_steps(\n", + " [\n", + " Step(\n", + " name=\"Example Step 1\",\n", + " test_result_id=test_result_id,\n", + " conditions=[Condition(\"Supply Voltage\", 5.0, condition_type=\"Setup\", units=\"V\")],\n", + " ),\n", + " Step(\n", + " name=\"Example Step 2\",\n", + " test_result_id=test_result_id,\n", + " conditions=[Condition(\"Supply Voltage\", 12.0, condition_type=\"Setup\", units=\"V\")],\n", + " ),\n", + " ]\n", + ")\n", + "step_ids = [step.id for step in new_steps]\n", + "print(f\"Created Steps: {step_ids}\")" + ] + }, + { + "cell_type": "markdown", + "id": "bd4458eb", + "metadata": {}, + "source": [ + "## Batch Creating Measurements\n", + "\n", + "`batch_create_measurements` creates multiple measurements at once. The measurements can have different names, different value types, and different steps." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de7c6787", + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import timezone\n", + "\n", + "import hightime as ht\n", + "import numpy as np\n", + "from nitypes.scalar import Scalar\n", + "from nitypes.vector import Vector\n", + "from nitypes.waveform import AnalogWaveform, Timing\n", + "\n", + "waveform = AnalogWaveform(\n", + " sample_count=4,\n", + " raw_data=np.array([0.0, 0.25, 0.5, 0.75], dtype=np.float64),\n", + " timing=Timing.create_with_regular_interval(\n", + " ht.timedelta(seconds=1e-3),\n", + " ht.datetime.now(timezone.utc),\n", + " ),\n", + ")\n", + "\n", + "new_measurements = data_store_client.batch_create_measurements(\n", + " [\n", + " Measurement(name=\"Example Float Measurement\", value=Scalar(4.98, \"V\"), step_id=step_ids[0]),\n", + " Measurement(name=\"Example String Measurement\", value=\"nominal\", step_id=step_ids[0]),\n", + " Measurement(name=\"Example Vector Measurement\", value=Vector([1.0, 1.25, 1.5], units=\"V\"), step_id=step_ids[1]),\n", + " Measurement(name=\"Example AnalogWaveform Measurement\", value=waveform, step_id=step_ids[1]),\n", + " ]\n", + ")\n", + "measurement_ids = [measurement.id for measurement in new_measurements]\n", + "print(f\"Created Measurements: {measurement_ids}\")" + ] + }, + { + "cell_type": "markdown", + "id": "14e7544a", + "metadata": {}, + "source": [ + "The returned IDs are in the same order as the request. Read the values back with `read_measurement_value`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67a450e7", + "metadata": {}, + "outputs": [], + "source": [ + "for measurement_id in measurement_ids:\n", + " measurement = data_store_client.get_measurement(measurement_id)\n", + " value = data_store_client.read_measurement_value(measurement)\n", + " print(f\"{measurement.name}: {value}\")\n", + "\n", + "read_back_waveform = data_store_client.read_measurement_value(\n", + " data_store_client.get_measurement(measurement_ids[3]), expected_type=AnalogWaveform\n", + ")\n", + "assert read_back_waveform.raw_data.tolist() == [0.0, 0.25, 0.5, 0.75]" + ] + }, + { + "cell_type": "markdown", + "id": "677d93be", + "metadata": {}, + "source": [ + "## Batches Are Atomic\n", + "\n", + "If any item in a batch fails, nothing in the batch is created. Below, the second measurement names a step that does not exist, so the first measurement is not created either." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6f7a5de", + "metadata": {}, + "outputs": [], + "source": [ + "import grpc\n", + "\n", + "try:\n", + " data_store_client.batch_create_measurements(\n", + " [\n", + " Measurement(name=\"Example Rollback Measurement\", value=1.0, step_id=step_ids[0]),\n", + " Measurement(\n", + " name=\"Example Rollback Measurement\",\n", + " value=2.0,\n", + " step_id=\"00000000-0000-0000-0000-000000000000\",\n", + " ),\n", + " ]\n", + " )\n", + "except grpc.RpcError as e:\n", + " print(f\"Batch failed: {e.details()}\")\n", + "\n", + "rolled_back = data_store_client.query_measurements(\"$filter=Name eq 'Example Rollback Measurement'\")\n", + "print(f\"Measurements created by the failed batch: {len(rolled_back)}\")\n", + "assert len(rolled_back) == 0" + ] + }, + { + "cell_type": "markdown", + "id": "a74cc0d1", + "metadata": {}, + "source": [ + "## Clean-Up\n", + "\n", + "Close the `DataStoreClient` and tear down the example-specific setup when done." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62162947", + "metadata": {}, + "outputs": [], + "source": [ + "data_store_client.close()\n", + "\n", + "# Perform example-specific cleanup. This is not needed when writing production code.\n", + "if grpc_channel is not None:\n", + " grpc_channel.close()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/v2/measurement-values/measurement_values.ipynb b/examples/notebooks/v2/measurement-values/measurement_values.ipynb new file mode 100644 index 0000000..6633517 --- /dev/null +++ b/examples/notebooks/v2/measurement-values/measurement_values.ipynb @@ -0,0 +1,296 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "98e8cdcf", + "metadata": {}, + "source": [ + "# Measurement Values (V2)\n", + "\n", + "This notebook shows the kinds of values that a V2 measurement can hold, how to add limits, and how to read the values back.\n", + "\n", + "- **Scalar values**: `float`, `int`, `str`, `bool`, and `Scalar` (a value with units).\n", + "- **Non-scalar values**: `Vector`, a list of numbers, `AnalogWaveform`, `ComplexWaveform`, `DigitalWaveform`, `Spectrum`, and `XYData`.\n", + "- **Limits**: an upper and lower limit stored with the measurement.\n", + "\n", + "`get_measurement` and `query_measurements` include the value only for scalar values. For any value, use `read_measurement_value`." + ] + }, + { + "cell_type": "markdown", + "id": "f29e3749", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "- NI Measurement Data Services running in V2 mode\n", + "- Python environment with the `ni.datastore` package (see `../README.md`)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e42feae1", + "metadata": {}, + "outputs": [], + "source": [ + "# Perform example-specific setup. This is not needed when writing production code.\n", + "# Set MDS_V2_GRPC_TARGET (for example, \"[::1]:59606\") to connect to a specific V2 service.\n", + "# When it is not set, the clients use discovery to find the service.\n", + "import os\n", + "\n", + "import grpc\n", + "\n", + "grpc_target = os.environ.get(\"MDS_V2_GRPC_TARGET\", \"\")\n", + "# Remove gRPC's 4 MB receive limit so that large values, such as long waveforms, can be read.\n", + "grpc_options = [(\"grpc.max_receive_message_length\", -1)]\n", + "grpc_channel = grpc.insecure_channel(grpc_target, options=grpc_options) if grpc_target else None\n", + "\n", + "from ni.datastore.data.v2 import DataStoreClient\n", + "\n", + "data_store_client = DataStoreClient(grpc_channel=grpc_channel)\n", + "\n", + "from ni.datastore.data.v2 import Step, TestResult\n", + "\n", + "test_result_id = data_store_client.create_test_result(TestResult(name=\"Measurement Values Example\")).id\n", + "step_id = data_store_client.create_step(Step(name=\"Example Step\", test_result_id=test_result_id)).id\n", + "print(f\"Created Test Result: {test_result_id}\")\n", + "print(f\"Created Step: {step_id}\")" + ] + }, + { + "cell_type": "markdown", + "id": "4bfcc9dc", + "metadata": {}, + "source": [ + "## Scalar Values\n", + "\n", + "A `Scalar` carries its own units, which are also saved as the measurement's `units`. For a plain value, pass `units` to `create_measurement`.\n", + "\n", + "A single value is always read back as a `Scalar`, so `.value` and `.units` work for every single value." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cae596d1", + "metadata": {}, + "outputs": [], + "source": [ + "from nitypes.scalar import Scalar\n", + "\n", + "scalar_values = {\n", + " \"Example Float Measurement\": 0.125,\n", + " \"Example Integer Measurement\": 42,\n", + " \"Example String Measurement\": \"nominal\",\n", + " \"Example Bool Measurement\": True,\n", + " \"Example Scalar Measurement\": Scalar(4.98, \"V\"),\n", + "}\n", + "\n", + "scalar_ids = {\n", + " name: data_store_client.create_measurement(name, value, step_id).id\n", + " for name, value in scalar_values.items()\n", + "}\n", + "\n", + "for name, measurement_id in scalar_ids.items():\n", + " measurement = data_store_client.get_measurement(measurement_id)\n", + " print(f\"{name}: value={measurement.value.value!r}, units='{measurement.units}'\")\n", + " expected = scalar_values[name]\n", + " assert measurement.value == (expected if isinstance(expected, Scalar) else Scalar(expected))\n", + "\n", + "assert data_store_client.get_measurement(scalar_ids[\"Example Scalar Measurement\"]).units == \"V\"" + ] + }, + { + "cell_type": "markdown", + "id": "e2411323", + "metadata": {}, + "source": [ + "## Limits\n", + "\n", + "Limits are stored with the measurement. Use `read_measurement_values` to read the value and both limits in one call. A limit that was not set is `None`.\n", + "\n", + "The service stores the outcome that you supply; it does not compute it from the limits." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bfc0c49a", + "metadata": {}, + "outputs": [], + "source": [ + "from ni.datastore.data.v2 import MeasurementValueField, Outcome\n", + "\n", + "limited_measurement_id = data_store_client.create_measurement(\n", + " \"Example Limited Measurement\",\n", + " Scalar(5.01, \"V\"),\n", + " step_id,\n", + " upper_limit=5.1,\n", + " lower_limit=4.9,\n", + " outcome=Outcome.PASSED,\n", + ").id\n", + "\n", + "values = data_store_client.read_measurement_values(\n", + " data_store_client.get_measurement(limited_measurement_id)\n", + ")\n", + "print(f\"value: {values.value}\")\n", + "print(f\"upper limit: {values.upper_limit}\")\n", + "print(f\"lower limit: {values.lower_limit}\")\n", + "assert values.upper_limit == Scalar(5.1) and values.lower_limit == Scalar(4.9)\n", + "\n", + "value_only = data_store_client.read_measurement_values(\n", + " data_store_client.get_measurement(limited_measurement_id), read_fields=[MeasurementValueField.VALUE]\n", + ")\n", + "print(f\"value only: {value_only.value}, upper limit: {value_only.upper_limit}\")" + ] + }, + { + "cell_type": "markdown", + "id": "2a3a752c", + "metadata": {}, + "source": [ + "## Non-Scalar Values\n", + "\n", + "Create non-scalar values the same way. Read them back with `read_measurement_value`, optionally passing `expected_type` to check the type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a3e4bef", + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import timezone\n", + "\n", + "import hightime as ht\n", + "import numpy as np\n", + "from nitypes.vector import Vector\n", + "from nitypes.waveform import AnalogWaveform, ComplexWaveform, DigitalWaveform, Spectrum, Timing\n", + "from nitypes.xy_data import XYData\n", + "\n", + "non_scalar_values = {\n", + " \"Example Vector Measurement\": Vector([1.0, 1.25, 1.5], units=\"V\"),\n", + " \"Example List Measurement\": [1.0, 2.0, 3.0],\n", + " \"Example AnalogWaveform Measurement\": AnalogWaveform(\n", + " sample_count=3,\n", + " raw_data=np.array([1.0, 2.0, 3.0]),\n", + " timing=Timing.create_with_regular_interval(\n", + " ht.timedelta(seconds=1e-3), ht.datetime.now(timezone.utc)\n", + " ),\n", + " ),\n", + " \"Example ComplexWaveform Measurement\": ComplexWaveform.from_array_1d(\n", + " np.array([1 + 2j, 3 + 4j]), dtype=np.complex128\n", + " ),\n", + " \"Example DigitalWaveform Measurement\": DigitalWaveform.from_lines([[0, 1], [1, 0]], signal_count=2),\n", + " \"Example Spectrum Measurement\": Spectrum.from_array_1d(\n", + " np.array([1.0, 2.0]), dtype=np.float64, start_frequency=10.0, frequency_increment=5.0\n", + " ),\n", + " \"Example XYData Measurement\": XYData.from_arrays_1d(\n", + " [1.0, 2.0], [3.0, 4.0], np.float64, x_units=\"s\", y_units=\"V\"\n", + " ),\n", + "}\n", + "\n", + "non_scalar_ids = {\n", + " name: data_store_client.create_measurement(name, value, step_id).id\n", + " for name, value in non_scalar_values.items()\n", + "}\n", + "\n", + "for name, measurement_id in non_scalar_ids.items():\n", + " measurement = data_store_client.get_measurement(measurement_id)\n", + " read_back = data_store_client.read_measurement_value(measurement)\n", + " print(f\"{name}: {type(read_back).__name__}\")\n", + " # get_measurement does not include non-scalar values.\n", + " assert measurement.value is None\n", + "\n", + "read_back_waveform = data_store_client.read_measurement_value(\n", + " data_store_client.get_measurement(non_scalar_ids[\"Example AnalogWaveform Measurement\"]),\n", + " expected_type=AnalogWaveform,\n", + ")\n", + "print(f\"waveform data: {read_back_waveform.raw_data}\")\n", + "assert read_back_waveform.raw_data.tolist() == [1.0, 2.0, 3.0]\n", + "\n", + "# A list of numbers is read back as a Vector.\n", + "read_back_list = data_store_client.read_measurement_value(\n", + " data_store_client.get_measurement(non_scalar_ids[\"Example List Measurement\"]),\n", + " expected_type=Vector,\n", + ")\n", + "print(f\"list read back as: {read_back_list}\")" + ] + }, + { + "cell_type": "markdown", + "id": "03e41bf9", + "metadata": {}, + "source": [ + "Limits can also be non-scalar, such as a `Vector` for per-point limits:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b52a28e", + "metadata": {}, + "outputs": [], + "source": [ + "vector_limit_id = data_store_client.create_measurement(\n", + " \"Example Vector Limit Measurement\",\n", + " Vector([1.0, 2.0], units=\"V\"),\n", + " step_id,\n", + " upper_limit=Vector([3.0, 3.0], units=\"V\"),\n", + " lower_limit=Vector([0.0, 0.0], units=\"V\"),\n", + ").id\n", + "vector_values = data_store_client.read_measurement_values(\n", + " data_store_client.get_measurement(vector_limit_id)\n", + ")\n", + "print(f\"value: {vector_values.value}\")\n", + "print(f\"upper limit: {vector_values.upper_limit}\")\n", + "print(f\"lower limit: {vector_values.lower_limit}\")" + ] + }, + { + "cell_type": "markdown", + "id": "ed51ece1", + "metadata": {}, + "source": [ + "## Clean up" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "125c1596", + "metadata": {}, + "outputs": [], + "source": [ + "data_store_client.close()\n", + "\n", + "# Perform example-specific cleanup. This is not needed when writing production code.\n", + "if grpc_channel is not None:\n", + " grpc_channel.close()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/v2/overview/publish_measurement.ipynb b/examples/notebooks/v2/overview/publish_measurement.ipynb new file mode 100644 index 0000000..5d7e061 --- /dev/null +++ b/examples/notebooks/v2/overview/publish_measurement.ipynb @@ -0,0 +1,311 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2110fdd9", + "metadata": {}, + "source": [ + "# Publish a Measurement (V2)\n", + "\n", + "This notebook is the V2 companion of `../../overview/publish_measurement.ipynb`. It publishes a waveform measurement and reads it back when NI Measurement Data Services runs in V2 mode.\n", + "\n", + "The main V2 differences are:\n", + "- Data uses `ni.datastore.data.v2`. Metadata still uses `ni.datastore.metadata`, because V2 reuses the V1 Metadata API.\n", + "- Conditions belong to a step. You add them to the step instead of publishing them separately.\n", + "- Measurements are created with `create_measurement`.\n", + "- Test results and steps have a `status` (how far the work has progressed) that is separate from the `outcome` (whether it passed)." + ] + }, + { + "cell_type": "markdown", + "id": "bb5d191d", + "metadata": {}, + "source": [ + "## Create a client\n", + "\n", + "To get started creating metadata and publishing measurements, the client code must first instantiate a `MetadataStoreClient` and a V2 `DataStoreClient`. The `MetadataStoreClient` is used for creating and querying metadata, and the `DataStoreClient` is used for creating and querying test results, steps, and measurements." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bca88a15", + "metadata": {}, + "outputs": [], + "source": [ + "# Perform example-specific setup. This is not needed when writing production code.\n", + "# Set MDS_V2_GRPC_TARGET (for example, \"[::1]:59606\") to connect to a specific V2 service.\n", + "# When it is not set, the clients use discovery to find the service.\n", + "import os\n", + "\n", + "import grpc\n", + "\n", + "grpc_target = os.environ.get(\"MDS_V2_GRPC_TARGET\", \"\")\n", + "# Remove gRPC's 4 MB receive limit so that large values, such as long waveforms, can be read.\n", + "grpc_options = [(\"grpc.max_receive_message_length\", -1)]\n", + "grpc_channel = grpc.insecure_channel(grpc_target, options=grpc_options) if grpc_target else None\n", + "\n", + "from ni.datastore.data.v2 import DataStoreClient\n", + "from ni.datastore.metadata import MetadataStoreClient\n", + "\n", + "metadata_store_client = MetadataStoreClient(grpc_channel=grpc_channel)\n", + "data_store_client = DataStoreClient(grpc_channel=grpc_channel)" + ] + }, + { + "cell_type": "markdown", + "id": "984c7a43", + "metadata": {}, + "source": [ + "__Recommendation:__ A client can be used within the context of a `with` statement in order to have its lifetime managed for it. Upon exiting the `with` statement, any gRPC channels owned by the client will be closed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ea43e3c", + "metadata": {}, + "outputs": [], + "source": [ + "with DataStoreClient(grpc_channel=grpc_channel) as data_store_client_one:\n", + " query_results = data_store_client_one.query_test_results()\n", + " # ... Perform any further operations with data_store_client_one ...\n", + "\n", + "# data_store_client_one will automatically close upon exiting the 'with' block" + ] + }, + { + "cell_type": "markdown", + "id": "16c2dfe2", + "metadata": {}, + "source": [ + "Alternatively, the `close()` method of the client may be called to perform the same cleanup that occurs when exiting the `with` block above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "91de5983", + "metadata": {}, + "outputs": [], + "source": [ + "data_store_client_two = DataStoreClient(grpc_channel=grpc_channel)\n", + "\n", + "query_results = data_store_client_two.query_test_results()\n", + "# ... Perform any further operations with data_store_client_two ...\n", + "\n", + "# Manually close the client when done using it\n", + "data_store_client_two.close()" + ] + }, + { + "cell_type": "markdown", + "id": "55dc458a", + "metadata": {}, + "source": [ + "Because notebooks such as the current example do not support splitting a single `with` statement across multiple code snippets, this example opts to call `close()` manually in the last code snippet." + ] + }, + { + "cell_type": "markdown", + "id": "c304f516", + "metadata": {}, + "source": [ + "## Create the test result metadata\n", + "\n", + "Create the metadata that describes who ran the test, where it ran, and which software was used. Then describe the test result.\n", + "\n", + "The V1 notebook also registers a metadata schema here. V2 does not accept a `schema_id` on test results, steps, or measurements yet, so this notebook stores its custom data in `extension` without a schema." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63bd3dd0", + "metadata": {}, + "outputs": [], + "source": [ + "from ni.datastore.data.v2 import Status, TestResult\n", + "from ni.datastore.metadata import Operator, SoftwareItem, TestStation\n", + "\n", + "operator_id = metadata_store_client.create_operator(\n", + " Operator(name=\"James Bowery\", role=\"Test Operator\")\n", + ")\n", + "test_station_id = metadata_store_client.create_test_station(TestStation(name=\"TestStation_12\"))\n", + "software_item_id = metadata_store_client.create_software_item(\n", + " SoftwareItem(product=\"Windows\", version=\"10.0.19044\")\n", + ")\n", + "\n", + "# The test is starting, so its status is RUNNING. The outcome is set when it finishes.\n", + "test_result = TestResult(\n", + " name=\"sample publish test result\",\n", + " operator_id=operator_id,\n", + " test_station_id=test_station_id,\n", + " software_item_ids=[software_item_id],\n", + " status=Status.RUNNING,\n", + " extension={\"session_file_path\": \"C:\\\\my_test_description.xlsx\"},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "74dc1422", + "metadata": {}, + "source": [ + "## Create the test result object\n", + "\n", + "Now that we have the test result metadata, we can create the test result object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad313793", + "metadata": {}, + "outputs": [], + "source": [ + "test_result_id = data_store_client.create_test_result(test_result).id\n", + "print(f\"created test result id: {test_result_id}\")" + ] + }, + { + "cell_type": "markdown", + "id": "bd91ffe2", + "metadata": {}, + "source": [ + "## Publish the data\n", + "\n", + "In V2, a step owns its conditions. Add them when you create the step, or later with `add_condition`. Then create the measurement on the step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da0e69ef", + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import timezone\n", + "\n", + "import hightime as ht\n", + "import numpy as np\n", + "from nitypes.waveform import AnalogWaveform, Timing\n", + "\n", + "from ni.datastore.data.v2 import Condition, Outcome, Step\n", + "\n", + "name = \"data publish sample\"\n", + "waveform = AnalogWaveform(\n", + " sample_count=3,\n", + " raw_data=np.array([1.0, 2.0, 3.0]),\n", + " timing=Timing.create_with_regular_interval(\n", + " ht.timedelta(seconds=1e-3),\n", + " ht.datetime.now(timezone.utc)\n", + " )\n", + ")\n", + "step = Step(\n", + " name=\"Initial step\",\n", + " test_result_id=test_result_id,\n", + " conditions=[\n", + " Condition(\"Ambient Temperature\", 23.0, condition_type=\"Environment\", units=\"degC\"),\n", + " ],\n", + ")\n", + "step_id = data_store_client.create_step(step).id\n", + "published_measurement_id = data_store_client.create_measurement(\n", + " name=name,\n", + " value=waveform,\n", + " step_id=step_id,\n", + " units=\"V\",\n", + " outcome=Outcome.PASSED,\n", + ").id\n", + "print(f\"The published measurement id is {published_measurement_id}\")\n", + "\n", + "# The test is finished, so mark it as done and record the outcome.\n", + "data_store_client.update_test_result(test_result_id, status=Status.DONE, outcome=Outcome.PASSED)" + ] + }, + { + "cell_type": "markdown", + "id": "2f89ce0b", + "metadata": {}, + "source": [ + "## Query the data\n", + "\n", + "Now that we've got data in the database, we can query for it using our OData query API.\n", + "\n", + "For non-scalar values, such as waveforms, `get_measurement` and `query_measurements` return the measurement details without the value. Use `read_measurement_value` to read the value." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17e06ee2", + "metadata": {}, + "outputs": [], + "source": [ + "published_measurements = data_store_client.query_measurements(\n", + " odata_query=f\"$filter=Id eq {published_measurement_id}\"\n", + ")\n", + "found_measurement = next(iter(published_measurements), None)\n", + "assert found_measurement is not None\n", + "\n", + "test_result = data_store_client.get_test_result(found_measurement.test_result_id)\n", + "operator = metadata_store_client.get_operator(test_result.operator_id)\n", + "print(f\"test result '{test_result.name}' is {test_result.status.name}: {test_result.outcome.name}\")\n", + "print(f\"operator: {operator.name}\")\n", + "print(f\"session file: {test_result.extension['session_file_path']}\")\n", + "\n", + "step = data_store_client.get_step(found_measurement.step_id)\n", + "for condition in step.conditions:\n", + " print(f\"condition: {condition.name} = {condition.value} {condition.units}\")\n", + "\n", + "waveform = data_store_client.read_measurement_value(found_measurement, expected_type=AnalogWaveform)\n", + "print(f\"published data is: {waveform.raw_data}\")\n", + "assert waveform.raw_data.tolist() == [1.0, 2.0, 3.0]" + ] + }, + { + "cell_type": "markdown", + "id": "4e76a5b5", + "metadata": {}, + "source": [ + "## Clean up" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c8b351f", + "metadata": {}, + "outputs": [], + "source": [ + "metadata_store_client.close()\n", + "data_store_client.close()\n", + "\n", + "# Perform example-specific cleanup. This is not needed when writing production code.\n", + "if grpc_channel is not None:\n", + " grpc_channel.close()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/v2/query/publish_sample_data.ipynb b/examples/notebooks/v2/query/publish_sample_data.ipynb new file mode 100644 index 0000000..4f70cc5 --- /dev/null +++ b/examples/notebooks/v2/query/publish_sample_data.ipynb @@ -0,0 +1,455 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "28b7ae47", + "metadata": {}, + "source": [ + "# Publish Sample Data for V2 OData Query Examples\n", + "\n", + "This notebook is the V2 companion of `../../query/publish_sample_data.ipynb`. It creates structured sample data in NI Measurement Data Services (V2 mode) that the V2 query notebook uses.\n", + "\n", + "**Run this notebook first** before using `query_data.ipynb`.\n", + "\n", + "## What This Creates\n", + "\n", + "This notebook sets up a realistic test scenario with:\n", + "- **3 TestResults**: 2 Power Supply tests and 1 Audio Amplifier test\n", + "- **12 Test Steps**: Various measurement procedures across all tests\n", + "- **38 Measurements**: Scalar measurements (voltages, currents, power, THD, etc.)\n", + "- **Conditions on every step**: Environmental and setup parameters. In V2, conditions belong to the step, and text values are supported.\n", + "- **Metadata**: 3 operators, 3 test stations, 2 hardware items, 2 UUTs, 3 UUT instances, 3 software items\n", + "- **Aliases**: Human-readable references for all metadata entities" + ] + }, + { + "cell_type": "markdown", + "id": "e8c184e8", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "- NI Measurement Data Services running in V2 mode\n", + "- Python environment with the `ni.datastore` package (see `../README.md`)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4644f4c2", + "metadata": {}, + "outputs": [], + "source": [ + "# Perform example-specific setup. This is not needed when writing production code.\n", + "# Set MDS_V2_GRPC_TARGET (for example, \"[::1]:59606\") to connect to a specific V2 service.\n", + "# When it is not set, the clients use discovery to find the service.\n", + "import os\n", + "\n", + "import grpc\n", + "\n", + "grpc_target = os.environ.get(\"MDS_V2_GRPC_TARGET\", \"\")\n", + "# Remove gRPC's 4 MB receive limit so that large values, such as long waveforms, can be read.\n", + "grpc_options = [(\"grpc.max_receive_message_length\", -1)]\n", + "grpc_channel = grpc.insecure_channel(grpc_target, options=grpc_options) if grpc_target else None\n", + "\n", + "from ni.datastore.metadata import (\n", + " MetadataStoreClient,\n", + " HardwareItem,\n", + " Operator,\n", + " SoftwareItem,\n", + " TestStation,\n", + " Uut,\n", + " UutInstance,\n", + ")\n", + "from ni.datastore.data.v2 import DataStoreClient\n", + "\n", + "# Create clients\n", + "metadata_store_client = MetadataStoreClient(grpc_channel=grpc_channel)\n", + "data_store_client = DataStoreClient(grpc_channel=grpc_channel)" + ] + }, + { + "cell_type": "markdown", + "id": "a23853a9", + "metadata": {}, + "source": [ + "## Create Metadata Entities\n", + "\n", + "Setting up the metadata for operators, test stations, hardware, etc. This is the same as in V1, because V2 reuses the V1 Metadata API. This is typically done once and establishes 'aliases' which are human-readable strings that can be used to refer to metadata later." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73eb10ac", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"šŸš€ Starting sample data creation...\")\n", + "\n", + "# Create sample operators\n", + "print(\"\\nāœ… Creating operators...\")\n", + "operator1 = Operator(name=\"Alex Smith\", role=\"Test Engineer\")\n", + "metadata_store_client.create_operator(operator1)\n", + "ALIAS_OPERATOR_ALEX = \"V2_Operator_Alex_Smith\"\n", + "metadata_store_client.create_alias(ALIAS_OPERATOR_ALEX, operator1)\n", + "\n", + "operator2 = Operator(name=\"Jordan Chen\", role=\"Senior Test Engineer\")\n", + "metadata_store_client.create_operator(operator2)\n", + "ALIAS_OPERATOR_JORDAN = \"V2_Operator_Jordan_Chen\"\n", + "metadata_store_client.create_alias(ALIAS_OPERATOR_JORDAN, operator2)\n", + "\n", + "operator3 = Operator(name=\"Taylor Johnson\", role=\"Lab Technician\")\n", + "metadata_store_client.create_operator(operator3)\n", + "ALIAS_OPERATOR_TAYLOR = \"V2_Operator_Taylor_Johnson\"\n", + "metadata_store_client.create_alias(ALIAS_OPERATOR_TAYLOR, operator3)\n", + "\n", + "# Create sample test stations\n", + "print(\"\\nāœ… Creating test stations...\")\n", + "station1 = TestStation(name=\"TestStation_A1\")\n", + "metadata_store_client.create_test_station(station1)\n", + "ALIAS_STATION_A1 = \"V2_Station_A1\"\n", + "metadata_store_client.create_alias(ALIAS_STATION_A1, station1)\n", + "\n", + "station2 = TestStation(name=\"TestStation_B2\")\n", + "metadata_store_client.create_test_station(station2)\n", + "ALIAS_STATION_B2 = \"V2_Station_B2\"\n", + "metadata_store_client.create_alias(ALIAS_STATION_B2, station2)\n", + "\n", + "station3 = TestStation(name=\"TestStation_C3\")\n", + "metadata_store_client.create_test_station(station3)\n", + "ALIAS_STATION_C3 = \"V2_Station_C3\"\n", + "metadata_store_client.create_alias(ALIAS_STATION_C3, station3)\n", + "\n", + "# Create sample hardware items (test equipment)\n", + "print(\"\\nāœ… Creating hardware items...\")\n", + "dmm = HardwareItem(manufacturer=\"NI\", model=\"PXIe-4081\", serial_number=\"DMM001\")\n", + "metadata_store_client.create_hardware_item(dmm)\n", + "ALIAS_DMM = \"V2_DMM_PXIe4081\"\n", + "metadata_store_client.create_alias(ALIAS_DMM, dmm)\n", + "\n", + "scope = HardwareItem(manufacturer=\"NI\", model=\"PXIe-5171\", serial_number=\"SCOPE001\")\n", + "metadata_store_client.create_hardware_item(scope)\n", + "ALIAS_SCOPE = \"V2_Scope_PXIe5171\"\n", + "metadata_store_client.create_alias(ALIAS_SCOPE, scope)\n", + "\n", + "# Create sample UUTs (Units Under Test - product definitions)\n", + "print(\"\\nāœ… Creating UUTs...\")\n", + "power_supply_uut = Uut(model_name=\"PowerSupply v2.1\", family=\"Power\")\n", + "metadata_store_client.create_uut(power_supply_uut)\n", + "ALIAS_UUT_POWER_SUPPLY = \"V2_UUT_PowerSupply_v2_1\"\n", + "metadata_store_client.create_alias(ALIAS_UUT_POWER_SUPPLY, power_supply_uut)\n", + "\n", + "amplifier_uut = Uut(model_name=\"Audio Amplifier v1.3\", family=\"Audio\")\n", + "metadata_store_client.create_uut(amplifier_uut)\n", + "ALIAS_UUT_AMPLIFIER = \"V2_UUT_AudioAmplifier_v1_3\"\n", + "metadata_store_client.create_alias(ALIAS_UUT_AMPLIFIER, amplifier_uut)\n", + "\n", + "# Create sample software items\n", + "print(\"\\nāœ… Creating software items...\")\n", + "python_env = SoftwareItem(product=\"Python\", version=\"3.11.5\")\n", + "metadata_store_client.create_software_item(python_env)\n", + "ALIAS_SOFTWARE_PYTHON = \"V2_Software_Python_3_11_5\"\n", + "metadata_store_client.create_alias(ALIAS_SOFTWARE_PYTHON, python_env)\n", + "\n", + "pytest_env = SoftwareItem(product=\"pytest\", version=\"7.4.0\")\n", + "metadata_store_client.create_software_item(pytest_env)\n", + "ALIAS_SOFTWARE_PYTEST = \"V2_Software_pytest_7_4_0\"\n", + "metadata_store_client.create_alias(ALIAS_SOFTWARE_PYTEST, pytest_env)\n", + "\n", + "nidaq_env = SoftwareItem(product=\"NI-DAQmx\", version=\"23.3.0\")\n", + "metadata_store_client.create_software_item(nidaq_env)\n", + "ALIAS_SOFTWARE_NIDAQ = \"V2_Software_NI_DAQmx_23_3_0\"\n", + "metadata_store_client.create_alias(ALIAS_SOFTWARE_NIDAQ, nidaq_env)" + ] + }, + { + "cell_type": "markdown", + "id": "fea2d252", + "metadata": {}, + "source": [ + "## Create UUT Instances\n", + "\n", + "This setup is done for each test run. Each time we're going to run a test, we create instances of the specific UUT we are testing." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d02ceda", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\nāœ… Creating UUT instances...\")\n", + "ps_instance1 = UutInstance(uut_id=ALIAS_UUT_POWER_SUPPLY, serial_number=\"PS-2024-001\")\n", + "metadata_store_client.create_uut_instance(ps_instance1)\n", + "ALIAS_UUT_INSTANCE_PS1 = \"V2_UUT_Instance_PS_001\"\n", + "metadata_store_client.create_alias(ALIAS_UUT_INSTANCE_PS1, ps_instance1)\n", + "\n", + "ps_instance2 = UutInstance(uut_id=ALIAS_UUT_POWER_SUPPLY, serial_number=\"PS-2024-002\")\n", + "metadata_store_client.create_uut_instance(ps_instance2)\n", + "ALIAS_UUT_INSTANCE_PS2 = \"V2_UUT_Instance_PS_002\"\n", + "metadata_store_client.create_alias(ALIAS_UUT_INSTANCE_PS2, ps_instance2)\n", + "\n", + "amp_instance1 = UutInstance(uut_id=ALIAS_UUT_AMPLIFIER, serial_number=\"AMP-2024-001\")\n", + "metadata_store_client.create_uut_instance(amp_instance1)\n", + "ALIAS_UUT_INSTANCE_AMP1 = \"V2_UUT_Instance_AMP_001\"\n", + "metadata_store_client.create_alias(ALIAS_UUT_INSTANCE_AMP1, amp_instance1)" + ] + }, + { + "cell_type": "markdown", + "id": "8461bd00", + "metadata": {}, + "source": [ + "## Create Test Results, Steps, and Measurements\n", + "\n", + "The helper below creates one test result. For each step, it creates the step with its conditions, then creates a scalar measurement for each measurement name.\n", + "\n", + "In V2, a test result starts as `RUNNING`. When all of its measurements are created, the helper marks it `DONE` and sets its outcome to `FAILED` if any measurement failed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "76b4e351", + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "\n", + "from nitypes.scalar import Scalar\n", + "\n", + "from ni.datastore.data.v2 import Condition, Outcome, Status, Step, TestResult\n", + "\n", + "\n", + "def sample_value(measurement_name: str) -> tuple[float, str]:\n", + " \"\"\"Return a realistic value and units for a measurement name.\"\"\"\n", + " if \"-12V\" in measurement_name:\n", + " return random.uniform(-12.2, -11.8), \"V\"\n", + " if \"5V\" in measurement_name:\n", + " return random.uniform(4.95, 5.05), \"V\"\n", + " if \"12V\" in measurement_name:\n", + " return random.uniform(11.8, 12.2), \"V\"\n", + " if \"Input Voltage\" in measurement_name:\n", + " return random.uniform(115, 125), \"V\"\n", + " if \"Supply Voltage\" in measurement_name:\n", + " return random.uniform(14.5, 15.5), \"V\"\n", + " if \"Standby Current\" in measurement_name:\n", + " return random.uniform(0.01, 0.05), \"A\"\n", + " if \"Current\" in measurement_name:\n", + " return random.uniform(0.5, 1.5), \"A\"\n", + " if \"DC Offset\" in measurement_name:\n", + " return random.uniform(-5, 5), \"mV\"\n", + " if \"Response\" in measurement_name:\n", + " return random.uniform(-1, 1), \"dB\"\n", + " if \"THD\" in measurement_name and \"Power\" not in measurement_name:\n", + " return random.uniform(0.001, 0.01), \"%\"\n", + " if \"Power\" in measurement_name:\n", + " return random.uniform(40, 150), \"W\"\n", + " if \"Ripple\" in measurement_name:\n", + " return random.uniform(10, 50), \"mV\"\n", + " if \"SNR\" in measurement_name:\n", + " return random.uniform(95, 105), \"dB\"\n", + " return random.uniform(0, 100), \"\"\n", + "\n", + "\n", + "def create_sample_test(\n", + " test_result: TestResult,\n", + " steps: list[tuple[str, list[str]]],\n", + " conditions: list[tuple[str, object, str, str]],\n", + ") -> tuple[str, int, int]:\n", + " \"\"\"Create a test result with steps, conditions, and measurements.\n", + "\n", + " Returns the test result ID, the number of measurements, and the number of conditions.\n", + " \"\"\"\n", + " test_result_id = data_store_client.create_test_result(test_result).id\n", + " measurement_count = 0\n", + " condition_count = 0\n", + " any_failed = False\n", + " for step_name, measurement_names in steps:\n", + " step = Step(\n", + " name=step_name,\n", + " test_result_id=test_result_id,\n", + " conditions=[\n", + " Condition(name, value, condition_type=condition_type, units=units)\n", + " for name, value, units, condition_type in conditions\n", + " ],\n", + " )\n", + " step_id = data_store_client.create_step(step).id\n", + " condition_count += len(conditions)\n", + " for measurement_name in measurement_names:\n", + " value, units = sample_value(measurement_name)\n", + " # Insert some random failures\n", + " outcome = Outcome.PASSED if random.random() > 0.4 else Outcome.FAILED\n", + " any_failed = any_failed or outcome == Outcome.FAILED\n", + " data_store_client.create_measurement(\n", + " measurement_name, Scalar(value, units), step_id, outcome=outcome\n", + " )\n", + " measurement_count += 1\n", + "\n", + " data_store_client.update_test_result(\n", + " test_result_id,\n", + " status=Status.DONE,\n", + " outcome=Outcome.FAILED if any_failed else Outcome.PASSED,\n", + " )\n", + " return test_result_id, measurement_count, condition_count" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "973ce332", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\nšŸ”¬ Creating sample test results with measurements...\")\n", + "\n", + "# Scenario 1: Power Supply Test. Use aliases instead of IDs for better readability.\n", + "print(\"šŸ“‹ Creating Power Supply test...\")\n", + "ps_test_result_id, ps_measurements, ps_conditions = create_sample_test(\n", + " TestResult(\n", + " name=\"Power Supply Test v2.1\",\n", + " uut_instance_id=ALIAS_UUT_INSTANCE_PS1,\n", + " operator_id=ALIAS_OPERATOR_ALEX,\n", + " test_station_id=ALIAS_STATION_A1,\n", + " software_item_ids=[ALIAS_SOFTWARE_PYTHON],\n", + " hardware_item_ids=[ALIAS_DMM, ALIAS_SCOPE],\n", + " status=Status.RUNNING,\n", + " ),\n", + " steps=[\n", + " (\"Initialize\", [\"Configure DMM\", \"Set Load Conditions\", \"Warm-up Period\"]),\n", + " (\"Input Voltage Test\", [\"Measure Input Voltage\", \"Measure Input Current\", \"Calculate Input Power\"]),\n", + " (\"Output Voltage Test\", [\"Measure 5V Rail\", \"Measure 12V Rail\", \"Measure -12V Rail\"]),\n", + " (\"Load Regulation\", [\"No Load Test\", \"25% Load Test\", \"50% Load Test\", \"75% Load Test\", \"Full Load Test\"]),\n", + " (\"Ripple Measurement\", [\"5V Ripple\", \"12V Ripple\", \"-12V Ripple\"]),\n", + " ],\n", + " conditions=[\n", + " (\"Ambient Temperature\", random.uniform(21, 26), \"°C\", \"Environment\"),\n", + " (\"Barometric Pressure\", random.uniform(995, 1015), \"hPa\", \"Environment\"),\n", + " (\"Line Frequency\", random.uniform(59.9, 60.1), \"Hz\", \"Environment\"),\n", + " # V2 conditions also support text values.\n", + " (\"Calibration Date\", \"2024-03-15\", \"\", \"Setup\"),\n", + " (\"Test Fixture\", \"PS-TEST-001\", \"\", \"Setup\"),\n", + " ],\n", + ")\n", + "print(f\" šŸ“Š Created {ps_measurements} measurements and {ps_conditions} conditions\")\n", + "\n", + "# Scenario 2: Audio Amplifier Test\n", + "print(\"\\nšŸ“‹ Creating Audio Amplifier test...\")\n", + "amp_test_result_id, amp_measurements, amp_conditions = create_sample_test(\n", + " TestResult(\n", + " name=\"Audio Amplifier Test v1.3\",\n", + " uut_instance_id=ALIAS_UUT_INSTANCE_AMP1,\n", + " operator_id=ALIAS_OPERATOR_JORDAN,\n", + " test_station_id=ALIAS_STATION_B2,\n", + " software_item_ids=[ALIAS_SOFTWARE_PYTHON],\n", + " hardware_item_ids=[ALIAS_DMM, ALIAS_SCOPE],\n", + " status=Status.RUNNING,\n", + " ),\n", + " steps=[\n", + " (\"Power-On Test\", [\"Supply Voltage\", \"Standby Current\", \"Enable Signal\"]),\n", + " (\"DC Offset\", [\"Left Channel DC Offset\", \"Right Channel DC Offset\"]),\n", + " (\"Frequency Response\", [\"20Hz Response\", \"1kHz Response\", \"10kHz Response\", \"20kHz Response\"]),\n", + " (\"THD+N Test\", [\"1kHz THD+N Left\", \"1kHz THD+N Right\", \"10kHz THD+N Left\", \"10kHz THD+N Right\"]),\n", + " (\"Power Output\", [\"Max Power Left\", \"Max Power Right\", \"Power at 1% THD\"]),\n", + " (\"Signal-to-Noise\", [\"SNR Left Channel\", \"SNR Right Channel\"]),\n", + " ],\n", + " conditions=[\n", + " (\"Ambient Temperature\", random.uniform(22, 25), \"°C\", \"Environment\"),\n", + " (\"Relative Humidity\", random.uniform(45, 55), \"%\", \"Environment\"),\n", + " (\"Test Load\", random.uniform(7.8, 8.2), \"Ī©\", \"Setup\"),\n", + " ],\n", + ")\n", + "print(f\" šŸ“Š Created {amp_measurements} measurements and {amp_conditions} conditions\")\n", + "\n", + "# Scenario 3: Additional Power Supply Test (second unit, quick verification)\n", + "print(\"\\nšŸ“‹ Creating second Power Supply test...\")\n", + "ps2_test_result_id, ps2_measurements, ps2_conditions = create_sample_test(\n", + " TestResult(\n", + " name=\"Power Supply Test v2.1\",\n", + " uut_instance_id=ALIAS_UUT_INSTANCE_PS2,\n", + " operator_id=ALIAS_OPERATOR_TAYLOR,\n", + " test_station_id=ALIAS_STATION_C3,\n", + " software_item_ids=[ALIAS_SOFTWARE_PYTHON],\n", + " hardware_item_ids=[ALIAS_DMM, ALIAS_SCOPE],\n", + " status=Status.RUNNING,\n", + " ),\n", + " steps=[(\"Quick Verification\", [\"Output 5V\", \"Output 12V\", \"Load Current\"])],\n", + " conditions=[(\"Room Temperature\", random.uniform(20, 24), \"°C\", \"Environment\")],\n", + ")\n", + "print(f\" šŸ“Š Created {ps2_measurements} measurements and {ps2_conditions} conditions\")\n", + "\n", + "total_measurements = ps_measurements + amp_measurements + ps2_measurements\n", + "total_conditions = ps_conditions + amp_conditions + ps2_conditions\n", + "assert total_measurements == 38\n", + "\n", + "print(\"\\nšŸŽÆ Sample data creation complete!\")\n", + "print(\"šŸ“Š Summary:\")\n", + "print(\" - Test Results: 3\")\n", + "print(\" - Steps: 12\")\n", + "print(f\" - Measurements: {total_measurements}\")\n", + "print(f\" - Conditions: {total_conditions}\")\n", + "print(\"\\nāœ… Ready to run query_data.ipynb!\")" + ] + }, + { + "cell_type": "markdown", + "id": "8e6ea1a5", + "metadata": {}, + "source": [ + "## Clean up" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6e5f10d", + "metadata": {}, + "outputs": [], + "source": [ + "data_store_client.close()\n", + "metadata_store_client.close()\n", + "\n", + "# Perform example-specific cleanup. This is not needed when writing production code.\n", + "if grpc_channel is not None:\n", + " grpc_channel.close()" + ] + }, + { + "cell_type": "markdown", + "id": "9ea63443", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "āœ… **Sample data has been successfully created!**\n", + "\n", + "You can now run **`query_data.ipynb`** to learn how to query test results, steps, measurements, and conditions with the V2 API.\n", + "\n", + "To query metadata, such as operators and hardware, use `../../query/query_metadata.ipynb`. The Metadata API is the same in V1 and V2." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/v2/query/query_data.ipynb b/examples/notebooks/v2/query/query_data.ipynb new file mode 100644 index 0000000..fd698e3 --- /dev/null +++ b/examples/notebooks/v2/query/query_data.ipynb @@ -0,0 +1,304 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4e8596e4", + "metadata": {}, + "source": [ + "# Querying Data with OData (V2)\n", + "\n", + "This notebook is the V2 companion of `../../query/query_measurements.ipynb`. It shows how to use OData queries to find test results, steps, measurements, and conditions in NI Measurement Data Services (V2 mode).\n", + "\n", + "## Prerequisites\n", + "\n", + "**āš ļø Important:** Run the `publish_sample_data.ipynb` notebook first to create the sample test data that this notebook queries.\n", + "\n", + "- NI Measurement Data Services running in V2 mode\n", + "- Python environment with the `ni.datastore` package (see `../README.md`)\n", + "- Sample test data created by running `publish_sample_data.ipynb`" + ] + }, + { + "cell_type": "markdown", + "id": "c09f60e1", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "Import the required libraries and create both metadata and data store clients." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "801c9619", + "metadata": {}, + "outputs": [], + "source": [ + "# Perform example-specific setup. This is not needed when writing production code.\n", + "# Set MDS_V2_GRPC_TARGET (for example, \"[::1]:59606\") to connect to a specific V2 service.\n", + "# When it is not set, the clients use discovery to find the service.\n", + "import os\n", + "\n", + "import grpc\n", + "\n", + "grpc_target = os.environ.get(\"MDS_V2_GRPC_TARGET\", \"\")\n", + "# Remove gRPC's 4 MB receive limit so that large values, such as long waveforms, can be read.\n", + "grpc_options = [(\"grpc.max_receive_message_length\", -1)]\n", + "grpc_channel = grpc.insecure_channel(grpc_target, options=grpc_options) if grpc_target else None\n", + "\n", + "from ni.datastore.data.v2 import DataStoreClient\n", + "from ni.datastore.metadata import MetadataStoreClient\n", + "\n", + "metadata_store_client = MetadataStoreClient(grpc_channel=grpc_channel)\n", + "data_store_client = DataStoreClient(grpc_channel=grpc_channel)" + ] + }, + { + "cell_type": "markdown", + "id": "4431330c", + "metadata": {}, + "source": [ + "## Query Test Results\n", + "\n", + "V2 can query test results directly. Test results also have a `status` that you can filter on.\n", + "\n", + "**Additional test result query examples:**\n", + "\n", + "```python\n", + "# Filter by status\n", + "done_test_results = data_store_client.query_test_results(\"$filter=Status eq 'Done'\")\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d9b7ed5d", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"=== Power Supply Test Results ===\")\n", + "power_test_results = data_store_client.query_test_results(\"$filter=contains(Name,'Power Supply')\")\n", + "for test_result in power_test_results:\n", + " print(f\" šŸ“‹ {test_result.name} - {test_result.status.name}, {test_result.outcome.name}\")\n", + "assert len(power_test_results) >= 2\n", + "\n", + "print(\"\\n=== Failed Test Results ===\")\n", + "failed_test_results = data_store_client.query_test_results(\"$filter=Outcome eq 'Failed'\")\n", + "for test_result in failed_test_results:\n", + " print(f\" āŒ {test_result.name}\")\n", + "if not failed_test_results:\n", + " print(\" āœ… No failed test results found!\")" + ] + }, + { + "cell_type": "markdown", + "id": "d2a45bf8", + "metadata": {}, + "source": [ + "## Query Measurements\n", + "\n", + "Measurements are the individual data points collected during test steps. In V2, you can filter on the outcome in the query itself.\n", + "\n", + "**Additional measurement query examples:**\n", + "\n", + "```python\n", + "# Filter by measurement name\n", + "voltage_measurements = data_store_client.query_measurements(\"$filter=contains(Name,'Voltage')\")\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f0a9279", + "metadata": {}, + "outputs": [], + "source": [ + "from ni.datastore.data.v2 import Outcome\n", + "\n", + "def print_measurement_with_outcome(measurement) -> str:\n", + " name = measurement.name or \"Unnamed Measurement\"\n", + " passed = measurement.outcome == Outcome.PASSED\n", + " failed = measurement.outcome == Outcome.FAILED\n", + " return f\"{name} - {'āœ… (Passed)' if passed else 'āŒ (Failed)' if failed else 'ā“ (Unknown)'}\"\n", + "\n", + "print(\"=== Failed Measurements ===\")\n", + "failed_measurements = data_store_client.query_measurements(\"$filter=Outcome eq 'Failed'\")\n", + "for measurement in failed_measurements[:15]:\n", + " print(f\" āŒ {measurement.name} = {measurement.value}\")\n", + "if not failed_measurements:\n", + " print(\" āœ… No failed measurements found!\")\n", + "if len(failed_measurements) > 15:\n", + " print(f\" ... and {len(failed_measurements) - 15} more measurements\")\n", + "assert all(measurement.outcome == Outcome.FAILED for measurement in failed_measurements)\n", + "\n", + "print(\"\\n=== Voltage Measurements ===\")\n", + "voltage_measurements = data_store_client.query_measurements(\"$filter=contains(Name,'Voltage')\")\n", + "for measurement in voltage_measurements:\n", + " print(f\" ⚔ {print_measurement_with_outcome(measurement)}\")\n", + "\n", + "print(\"\\n=== Current Measurements ===\")\n", + "current_measurements = data_store_client.query_measurements(\"$filter=contains(Name,'Current')\")\n", + "for measurement in current_measurements:\n", + " print(f\" šŸ”Œ {print_measurement_with_outcome(measurement)}\")\n", + "\n", + "print(\"\\n=== Failed Measurements by Operator Alex Smith ===\")\n", + "test_results_by_id = {}\n", + "for measurement in failed_measurements:\n", + " if measurement.test_result_id not in test_results_by_id:\n", + " test_results_by_id[measurement.test_result_id] = data_store_client.get_test_result(\n", + " measurement.test_result_id\n", + " )\n", + " test_result = test_results_by_id[measurement.test_result_id]\n", + " if test_result.operator_id:\n", + " operator = metadata_store_client.get_operator(test_result.operator_id)\n", + " if operator.name == \"Alex Smith\":\n", + " print(f\" šŸ“‹ {print_measurement_with_outcome(measurement)} - Operator: {operator.name}\")" + ] + }, + { + "cell_type": "markdown", + "id": "7cfeedef", + "metadata": {}, + "source": [ + "## Query Steps\n", + "\n", + "Steps are the individual test phases within a test result. Each step can contain multiple measurements.\n", + "\n", + "**Additional step query examples:**\n", + "\n", + "```python\n", + "# Filter by step name\n", + "initialization_steps = data_store_client.query_steps(\"$filter=contains(Name,'Initialize')\")\n", + "measurement_steps = data_store_client.query_steps(\"$filter=contains(Name,'Measure')\")\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b50d0dcf", + "metadata": {}, + "outputs": [], + "source": [ + "all_steps = data_store_client.query_steps(\"\")\n", + "\n", + "print(f\"Found {len(all_steps)} steps total\")\n", + "print(\"\\nStep summary:\")\n", + "for step in all_steps[:10]: # Show first 10 steps\n", + " print(f\" šŸ”§ {step.name}\")\n", + "\n", + "if len(all_steps) > 10:\n", + " print(f\" ... and {len(all_steps) - 10} more steps\")\n", + "\n", + "print(\"\\n=== Steps containing 'Voltage' ===\")\n", + "voltage_steps = data_store_client.query_steps(\"$filter=contains(Name,'Voltage')\")\n", + "for step in voltage_steps:\n", + " print(f\" šŸ”§ {step.name}\")" + ] + }, + { + "cell_type": "markdown", + "id": "7c92759e", + "metadata": {}, + "source": [ + "## Query Conditions\n", + "\n", + "In V2, conditions belong to steps. To find conditions, query the steps that have them with `Conditions/any(...)`, then read each step's `conditions`.\n", + "\n", + "`read_fields` limits which fields the service returns. When you use it, list every field that you need, such as `name` and `conditions`.\n", + "\n", + "**Additional condition query examples:**\n", + "\n", + "```python\n", + "# Find the measurements whose step has a humidity condition\n", + "humidity_measurements = data_store_client.query_measurements(\n", + " \"$filter=Conditions/any(c: c/Name eq 'Relative Humidity')\"\n", + ")\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b1635f2", + "metadata": {}, + "outputs": [], + "source": [ + "temp_and_pressure_steps = data_store_client.query_steps(\n", + " \"$filter=Conditions/any(c: contains(c/Name,'Temperature') or contains(c/Name,'Pressure'))\",\n", + " read_fields=[\"name\", \"conditions\"],\n", + ")\n", + "\n", + "unique_condition_names = sorted(\n", + " {\n", + " condition.name\n", + " for step in temp_and_pressure_steps\n", + " for condition in step.conditions\n", + " if \"Temperature\" in condition.name or \"Pressure\" in condition.name\n", + " }\n", + ")\n", + "\n", + "if unique_condition_names:\n", + " print(f\"Found {len(temp_and_pressure_steps)} steps with temperature or pressure conditions\")\n", + " print(\"\\nConditions related to temp and pressure:\")\n", + " for condition_name in unique_condition_names:\n", + " print(f\" šŸŒ”ļø {condition_name}\")\n", + "else:\n", + " print(\"No conditions found in the sample data\")\n", + "\n", + "print(\"\\n=== Measurements whose step has a humidity condition ===\")\n", + "humidity_measurements = data_store_client.query_measurements(\n", + " \"$filter=Conditions/any(c: c/Name eq 'Relative Humidity')\"\n", + ")\n", + "print(f\" šŸ’§ {len(humidity_measurements)} measurements\")" + ] + }, + { + "cell_type": "markdown", + "id": "06cd0667", + "metadata": {}, + "source": [ + "## Clean up" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "91f398e4", + "metadata": {}, + "outputs": [], + "source": [ + "metadata_store_client.close()\n", + "data_store_client.close()\n", + "\n", + "# Perform example-specific cleanup. This is not needed when writing production code.\n", + "if grpc_channel is not None:\n", + " grpc_channel.close()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/v2/structured-extensions/structured_extensions.ipynb b/examples/notebooks/v2/structured-extensions/structured_extensions.ipynb new file mode 100644 index 0000000..d885392 --- /dev/null +++ b/examples/notebooks/v2/structured-extensions/structured_extensions.ipynb @@ -0,0 +1,197 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "de3c93e3", + "metadata": {}, + "source": [ + "# Structured Extensions (V2)\n", + "\n", + "This notebook is the V2 companion of `../../extension-attributes/extension_attributes.ipynb`. It shows how to store your own data in the `extension` of test results, steps, and measurements.\n", + "\n", + "In V2, an extension is a JSON object. It can hold nested objects and lists, not just text.\n", + "\n", + "**Note:** V2 does not accept a `schema_id` on test results, steps, or measurements yet, so the service does not check extensions against a schema. Numbers are stored as JSON numbers, so whole numbers are read back as `float` values (for example, `3` is read back as `3.0`)." + ] + }, + { + "cell_type": "markdown", + "id": "b67c1e9c", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "- NI Measurement Data Services running in V2 mode\n", + "- Python environment with the `ni.datastore` package (see `../README.md`)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f216c9a8", + "metadata": {}, + "outputs": [], + "source": [ + "# Perform example-specific setup. This is not needed when writing production code.\n", + "# Set MDS_V2_GRPC_TARGET (for example, \"[::1]:59606\") to connect to a specific V2 service.\n", + "# When it is not set, the clients use discovery to find the service.\n", + "import os\n", + "\n", + "import grpc\n", + "\n", + "grpc_target = os.environ.get(\"MDS_V2_GRPC_TARGET\", \"\")\n", + "# Remove gRPC's 4 MB receive limit so that large values, such as long waveforms, can be read.\n", + "grpc_options = [(\"grpc.max_receive_message_length\", -1)]\n", + "grpc_channel = grpc.insecure_channel(grpc_target, options=grpc_options) if grpc_target else None\n", + "\n", + "from ni.datastore.data.v2 import DataStoreClient\n", + "\n", + "data_store_client = DataStoreClient(grpc_channel=grpc_channel)" + ] + }, + { + "cell_type": "markdown", + "id": "3ea98e28", + "metadata": {}, + "source": [ + "## Create Data with Extensions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31230c46", + "metadata": {}, + "outputs": [], + "source": [ + "from ni.datastore.data.v2 import Step, TestResult\n", + "\n", + "test_result_id = data_store_client.create_test_result(\n", + " TestResult(\n", + " name=\"Structured Extensions Example\",\n", + " extension={\n", + " \"fixture\": {\"id\": \"FIX-12\", \"slots\": [1, 2, 3]},\n", + " \"retest\": False,\n", + " \"operator_notes\": \"Second run after recalibration.\",\n", + " },\n", + " )\n", + ").id\n", + "step_id = data_store_client.create_step(\n", + " Step(\n", + " name=\"Warm-up\",\n", + " test_result_id=test_result_id,\n", + " extension={\"phase\": {\"name\": \"warmup\", \"seconds\": 30}},\n", + " )\n", + ").id\n", + "measurement_id = data_store_client.create_measurement(\n", + " \"Output Voltage\",\n", + " 5.01,\n", + " step_id,\n", + " units=\"V\",\n", + " extension={\"probe\": {\"channel\": 3, \"attenuation\": \"10x\"}},\n", + ").id\n", + "print(f\"Created Test Result: {test_result_id}\")" + ] + }, + { + "cell_type": "markdown", + "id": "0b0b891d", + "metadata": {}, + "source": [ + "## Read Extensions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35887f6d", + "metadata": {}, + "outputs": [], + "source": [ + "test_result = data_store_client.get_test_result(test_result_id)\n", + "step = data_store_client.get_step(step_id)\n", + "measurement = data_store_client.get_measurement(measurement_id)\n", + "\n", + "print(f\"test result fixture: {test_result.extension['fixture']}\")\n", + "print(f\"test result retest: {test_result.extension['retest']}\")\n", + "print(f\"step phase: {step.extension['phase']}\")\n", + "print(f\"measurement probe: {measurement.extension['probe']}\")\n", + "\n", + "assert test_result.extension[\"fixture\"][\"id\"] == \"FIX-12\"\n", + "assert test_result.extension[\"fixture\"][\"slots\"] == [1, 2, 3]\n", + "assert measurement.extension[\"probe\"][\"channel\"] == 3" + ] + }, + { + "cell_type": "markdown", + "id": "cabcb73d", + "metadata": {}, + "source": [ + "## Update Extensions\n", + "\n", + "Updating `extension` replaces the whole extension, so include every key that you want to keep." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad08c394", + "metadata": {}, + "outputs": [], + "source": [ + "new_extension = dict(test_result.extension)\n", + "new_extension[\"retest\"] = True\n", + "new_extension[\"fixture\"] = {\"id\": \"FIX-13\", \"slots\": [1, 2]}\n", + "\n", + "updated_test_result = data_store_client.update_test_result(test_result_id, extension=new_extension)\n", + "print(f\"updated extension: {updated_test_result.extension}\")\n", + "assert updated_test_result.extension[\"retest\"] is True\n", + "assert updated_test_result.extension[\"operator_notes\"] == \"Second run after recalibration.\"" + ] + }, + { + "cell_type": "markdown", + "id": "dfced6a4", + "metadata": {}, + "source": [ + "## Clean up" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "009034a8", + "metadata": {}, + "outputs": [], + "source": [ + "data_store_client.delete_test_result(test_result_id, force=True)\n", + "data_store_client.close()\n", + "\n", + "# Perform example-specific cleanup. This is not needed when writing production code.\n", + "if grpc_channel is not None:\n", + " grpc_channel.close()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/v2/update-delete/update_delete.ipynb b/examples/notebooks/v2/update-delete/update_delete.ipynb new file mode 100644 index 0000000..8842e79 --- /dev/null +++ b/examples/notebooks/v2/update-delete/update_delete.ipynb @@ -0,0 +1,388 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7d407736", + "metadata": {}, + "source": [ + "# Update and Delete (V2)\n", + "\n", + "This notebook shows how to change and remove data that was already created in V2:\n", + "- Update fields of test results, steps, and measurements, one at a time or in a batch.\n", + "- Add, replace, and remove the conditions of a step.\n", + "- Link and unlink software items, hardware items, and test adapters.\n", + "- Delete measurements, steps, and test results, with and without `force`.\n", + "\n", + "Not every field can be changed. For example, a measurement's name and value cannot be changed after it is created." + ] + }, + { + "cell_type": "markdown", + "id": "df014e04", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "- NI Measurement Data Services running in V2 mode\n", + "- Python environment with the `ni.datastore` package (see `../README.md`)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c839bb4a", + "metadata": {}, + "outputs": [], + "source": [ + "# Perform example-specific setup. This is not needed when writing production code.\n", + "# Set MDS_V2_GRPC_TARGET (for example, \"[::1]:59606\") to connect to a specific V2 service.\n", + "# When it is not set, the clients use discovery to find the service.\n", + "import os\n", + "\n", + "import grpc\n", + "\n", + "grpc_target = os.environ.get(\"MDS_V2_GRPC_TARGET\", \"\")\n", + "# Remove gRPC's 4 MB receive limit so that large values, such as long waveforms, can be read.\n", + "grpc_options = [(\"grpc.max_receive_message_length\", -1)]\n", + "grpc_channel = grpc.insecure_channel(grpc_target, options=grpc_options) if grpc_target else None\n", + "\n", + "from ni.datastore.data.v2 import DataStoreClient\n", + "from ni.datastore.metadata import MetadataStoreClient\n", + "\n", + "metadata_store_client = MetadataStoreClient(grpc_channel=grpc_channel)\n", + "data_store_client = DataStoreClient(grpc_channel=grpc_channel)" + ] + }, + { + "cell_type": "markdown", + "id": "18cc3ab7", + "metadata": {}, + "source": [ + "## Create the Example Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1465905c", + "metadata": {}, + "outputs": [], + "source": [ + "from ni.datastore.data.v2 import Condition, Outcome, Status, Step, TestResult\n", + "from ni.datastore.metadata import SoftwareItem\n", + "\n", + "software_item_id = metadata_store_client.create_software_item(\n", + " SoftwareItem(product=\"Example Driver\", version=\"1.0.0\")\n", + ")\n", + "\n", + "# Create methods return the created objects. Keep them to update and delete them later.\n", + "test_result = data_store_client.create_test_result(\n", + " TestResult(name=\"Update and Delete Example\", status=Status.RUNNING)\n", + ")\n", + "step = data_store_client.create_step(\n", + " Step(\n", + " name=\"Example Step\",\n", + " test_result_id=test_result.id,\n", + " status=Status.RUNNING,\n", + " conditions=[\n", + " Condition(\"Ambient Temperature\", 23.0, condition_type=\"Environment\", units=\"degC\"),\n", + " Condition(\"Test Fixture\", \"FIX-001\", condition_type=\"Setup\"),\n", + " ],\n", + " )\n", + ")\n", + "measurements = [\n", + " data_store_client.create_measurement(f\"Example Measurement {index}\", float(index), step.id)\n", + " for index in range(3)\n", + "]\n", + "print(f\"Created Test Result: {test_result.id}\")\n", + "print(f\"Created Step: {step.id}\")\n", + "print(f\"Created Measurements: {[measurement.id for measurement in measurements]}\")" + ] + }, + { + "cell_type": "markdown", + "id": "2077b8ef", + "metadata": {}, + "source": [ + "## Update Fields\n", + "\n", + "There are three ways to update an object. Each returns the changed object.\n", + "- Pass the object, or its ID, with the fields to change as keyword arguments. Only those fields change.\n", + "- Pass an update builder, such as `TestResultUpdateBuilder`. Each `with_` call selects one field to change.\n", + "- Change the object and pass it on its own. Every field that can be updated is sent, so a field that is empty is cleared.\n", + "\n", + "`status` describes how far the work has progressed, and `outcome` describes the result. They are separate, so a test can be `DONE` and `FAILED`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "244dfe7a", + "metadata": {}, + "outputs": [], + "source": [ + "from ni.datastore.data.v2 import TestResultUpdateBuilder\n", + "\n", + "# Keyword fields: only the fields that are passed change.\n", + "step = data_store_client.update_step(\n", + " step, status=Status.DONE, outcome=Outcome.FAILED, notes=\"Output was out of range.\"\n", + ")\n", + "print(f\"step: {step.status.name}, {step.outcome.name}, notes='{step.notes}'\")\n", + "\n", + "# Update builder: each with_ call selects one field to change.\n", + "test_result = data_store_client.update_test_result(\n", + " TestResultUpdateBuilder(test_result)\n", + " .with_status(Status.DONE)\n", + " .with_outcome(Outcome.FAILED)\n", + " .with_link(\"https://example.com/runs/1\")\n", + ")\n", + "print(f\"test result: {test_result.status.name}, {test_result.outcome.name}, link={test_result.link}\")\n", + "\n", + "# Whole object: change the returned object, then send it back.\n", + "measurements[0].notes = \"Rechecked by operator.\"\n", + "measurements[0] = data_store_client.update_measurement(measurements[0])\n", + "print(f\"measurement notes: '{measurements[0].notes}'\")\n", + "\n", + "assert test_result.status == Status.DONE\n", + "assert test_result.outcome == Outcome.FAILED" + ] + }, + { + "cell_type": "markdown", + "id": "f093a4c0", + "metadata": {}, + "source": [ + "Update multiple items in one atomic call. Pass update builders or objects, or a dictionary of fields keyed by ID:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33bdc44a", + "metadata": {}, + "outputs": [], + "source": [ + "from ni.datastore.data.v2 import MeasurementUpdateBuilder\n", + "\n", + "measurements = list(\n", + " data_store_client.batch_update_measurements(\n", + " [\n", + " MeasurementUpdateBuilder(measurements[0]).with_outcome(Outcome.PASSED),\n", + " MeasurementUpdateBuilder(measurements[1]).with_outcome(Outcome.FAILED),\n", + " MeasurementUpdateBuilder(measurements[2])\n", + " .with_outcome(Outcome.PASSED)\n", + " .with_notes(\"Borderline.\"),\n", + " ]\n", + " )\n", + ")\n", + "for measurement in measurements:\n", + " print(f\"{measurement.name}: {measurement.outcome.name}\")" + ] + }, + { + "cell_type": "markdown", + "id": "4a089811", + "metadata": {}, + "source": [ + "## Add, Replace, and Remove Conditions\n", + "\n", + "Conditions cannot be changed in place. Adding a condition with an existing name replaces it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a2fd632", + "metadata": {}, + "outputs": [], + "source": [ + "step = data_store_client.add_condition(\n", + " step.id, Condition(\"Relative Humidity\", 45.0, condition_type=\"Environment\", units=\"%\")\n", + ")\n", + "step = data_store_client.add_condition(\n", + " step.id, Condition(\"Ambient Temperature\", 24.5, condition_type=\"Environment\", units=\"degC\")\n", + ")\n", + "step = data_store_client.remove_condition(step.id, \"Test Fixture\")\n", + "\n", + "for condition in step.conditions:\n", + " print(f\"{condition.name} = {condition.value} {condition.units}\")\n", + "\n", + "condition_names = {condition.name for condition in step.conditions}\n", + "assert condition_names == {\"Ambient Temperature\", \"Relative Humidity\"}" + ] + }, + { + "cell_type": "markdown", + "id": "b2e4befe", + "metadata": {}, + "source": [ + "## Link and Unlink Metadata\n", + "\n", + "Link a software item to the test result first. Then a measurement of that test result can also link to it. Each measurement keeps its own selection: linking an item to the test result does not link it to the test result's measurements.\n", + "\n", + "To unlink, reverse the order: unlink the item from the measurement, then from the test result. An item cannot be unlinked from a test result while any of its measurements still references it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1853600", + "metadata": {}, + "outputs": [], + "source": [ + "test_result = data_store_client.add_test_result_metadata(\n", + " test_result.id, software_item_ids=[software_item_id]\n", + ")\n", + "print(f\"test result software items: {list(test_result.software_item_ids)}\")\n", + "\n", + "measurement = data_store_client.add_measurement_metadata(\n", + " measurements[0].id, software_item_ids=[software_item_id]\n", + ")\n", + "print(f\"measurement software items: {list(measurement.software_item_ids)}\")\n", + "\n", + "measurement = data_store_client.remove_measurement_metadata(\n", + " measurements[0].id, software_item_ids=[software_item_id]\n", + ")\n", + "print(f\"measurement software items after unlinking: {list(measurement.software_item_ids)}\")\n", + "\n", + "test_result = data_store_client.remove_test_result_metadata(\n", + " test_result.id, software_item_ids=[software_item_id]\n", + ")\n", + "print(f\"test result software items after unlinking: {list(test_result.software_item_ids)}\")\n", + "assert list(test_result.software_item_ids) == []" + ] + }, + { + "cell_type": "markdown", + "id": "4106a6af", + "metadata": {}, + "source": [ + "## Delete\n", + "\n", + "Delete methods accept the object or its ID. Deleting a measurement removes it. Getting it afterward fails with `NOT_FOUND`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04c5e62d", + "metadata": {}, + "outputs": [], + "source": [ + "import grpc\n", + "\n", + "data_store_client.delete_measurement(measurements[2])\n", + "\n", + "try:\n", + " data_store_client.get_measurement(measurements[2].id)\n", + "except grpc.RpcError as e:\n", + " print(f\"{e.code().name}: {e.details()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "35cbc7c2", + "metadata": {}, + "source": [ + "A step or test result that still owns data cannot be deleted unless you pass `force=True`. This protects you from deleting data by accident." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17e505c6", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " data_store_client.delete_step(step)\n", + "except grpc.RpcError as e:\n", + " print(f\"{e.code().name}: {e.details()}\")\n", + "\n", + "try:\n", + " data_store_client.delete_test_result(test_result)\n", + "except grpc.RpcError as e:\n", + " print(f\"{e.code().name}: {e.details()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "6e90f2e8", + "metadata": {}, + "source": [ + "Delete multiple items in one atomic call, or use `force=True` to also delete everything the item owns:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f696c296", + "metadata": {}, + "outputs": [], + "source": [ + "data_store_client.batch_delete_measurements(measurements[:2])\n", + "data_store_client.delete_step(step) # No measurements are left, so force is not needed.\n", + "data_store_client.delete_test_result(test_result)\n", + "\n", + "# Create two more test results, each with a step, and delete them together.\n", + "extra_test_results = data_store_client.batch_create_test_results(\n", + " [\n", + " TestResult(name=\"Delete Example A\", steps=[Step(name=\"Step A\")]),\n", + " TestResult(name=\"Delete Example B\", steps=[Step(name=\"Step B\")]),\n", + " ]\n", + ")\n", + "data_store_client.batch_delete_test_results(extra_test_results, force=True)\n", + "\n", + "remaining = data_store_client.query_test_results(\n", + " \"$filter=Name eq 'Update and Delete Example' or startswith(Name,'Delete Example')\"\n", + ")\n", + "print(f\"Remaining example test results: {len(remaining)}\")\n", + "assert len(remaining) == 0" + ] + }, + { + "cell_type": "markdown", + "id": "8ea3657c", + "metadata": {}, + "source": [ + "## Clean up" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46a5e41c", + "metadata": {}, + "outputs": [], + "source": [ + "metadata_store_client.close()\n", + "data_store_client.close()\n", + "\n", + "# Perform example-specific cleanup. This is not needed when writing production code.\n", + "if grpc_channel is not None:\n", + " grpc_channel.close()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/overview-v2/README.md b/examples/overview-v2/README.md new file mode 100644 index 0000000..506efbf --- /dev/null +++ b/examples/overview-v2/README.md @@ -0,0 +1,27 @@ +# Overview V2 Example + +`overview-v2` contains sample Python code for writing to and reading from +the NI Measurement Data Store when the service runs in V2 mode. + +It is the V2 companion of the [`overview`](../overview/) example. Data is +published with `ni.datastore.data.v2`, and metadata is created with +`ni.datastore.metadata`, because V2 reuses the V1 Metadata API. + +## Required Software + +`overview-v2` requires Python 3.10 or greater, and a Measurement Data +Services instance started in V2 mode. + +## Usage + +```python +poetry install +poetry run overview-v2 +``` + +By default, the clients use discovery to find the service. To connect to a +specific service instance, pass its gRPC target: + +```python +poetry run overview-v2 --grpc-target "[::1]:59606" +``` diff --git a/examples/overview-v2/poetry.lock b/examples/overview-v2/poetry.lock new file mode 100644 index 0000000..c0de75a --- /dev/null +++ b/examples/overview-v2/poetry.lock @@ -0,0 +1,1799 @@ +# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand. + +[[package]] +name = "ast-serialize" +version = "0.13.1" +description = "Python bindings for mypy AST serialization" +optional = false +python-versions = ">=3.7" +groups = ["lint"] +files = [ + {file = "ast_serialize-0.13.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3e129d44b0cffbea24a7f40117ce49e30993c3d119e584d4817ebd6f87246456"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:523d45bab16ddaf1399c04c4fe81bbf180f81e33c2f7bcbe76be56b0f7311ab4"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:638333fb8ae72c62753f9e794fda20290883ddc65b0d85e26e08f8bb227567ad"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc34c670747d85d4232834665e247009413eaebe47c69eaf60a5c439aaecaed9"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c8ebd419811c435812d242fb2a173e57dfede198c9ac9a0a584666ebc9b9331"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8899c038b1c15ae8b98e379057fb3ef398ceb0b21d1737a8156dd5a0923b7e51"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e14c4de98f965647a74e6eaf37e20207124e45451ccd36e84a6f6ebab45e0372"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3e8a3eff69af640db985360fef35073fb46956dfc3e5c614cf00920d2746dd"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:75d7750fe98f6f35a96c43aba8d98a2e623e22c778a0c17ab9ede92eb10f5d82"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e8ddd63e80fc280c2f790157386f27f9b9f6ccb9c4f8e803b863cbfb085769"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9221e09496fb942901993c1812ee87950cc0ecbb514c7c0f1f5fa6d78f701ed8"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:7a5c38bdb93669c596f94fb7204dd6ebe4fadb19058ac9a2faf2948c2a175f96"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:acf4e9f055d75058ab01536da330648746f2f82b7697a85230547f2d5ebe895c"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87427913c325adc811e81e7c9a757b061f47ef1e29114ac479aa0953a2687a98"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:ad88921f95b92bc144d9ad059a0e16f8c1ae69f5239f2d3c6536fd2774f73dfe"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f8b17e2921e56621bcb46792bd3e0b82ca3b85de78d5936e7b8174ba1758b0aa"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-win32.whl", hash = "sha256:2f9a9ce8b0994f757b941f7f7fe384a6666b4162b941ebc5c248081baea18e47"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5d99910e6b9a9b9360bcca2ae99aee36cc912ce058df2a57d1c33099b6cb2593"}, + {file = "ast_serialize-0.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a789cbe14d0a0e9ec9b32a456e5abc81ad8799479c3cfd88a29e3442262aebe6"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:23d768369b89d551077dc173da6892fad00443338417523b6732f4e8ba4a7ed5"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:68f8829d114dab6c9db4a8bbb8d0e342958faee1b5e15a29022678df639960f8"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e385b17d5bde892a6184003a9aff3518387908b73f8f5ea539313c07c8c0ede"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02686b281ec9b8e9614f6f0db27e12e4cac515f336c5d05b678bc9eb73ff2af5"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8ab2c2585b2e22e48cd4aff2136fce4eab4ced569b85f150ef9a5a5d6f34d49"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c652d9f5d52d8b81eff3ff2c95c9dd0f281de45f6ad5d3fa95527550c396454a"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121e1d08d58b75c15491cc3003609cbad71ac2b0016043c36d30a6b2032b6605"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:30cf89b6a6844542d116c758725d13de5b97b9410a51ee577715f761270d0c1c"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:732aaae9e498c973fc63fd7f308009b57c4afa6ff59d0b674d894e57d0bca01a"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:d9c1a920b2aa0c1e016bed7721689717034ca0dba64ac3a3ff243b003d4e8dd8"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:847cad4c22953823f5e76d0b126c6f9da670cfa45b1df41b9324e22189d77611"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:aca07de3ca8e3d9f2b716c3c31f4f384fe4bd2d03e09912417cff7a3e08036f7"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:5db12d843d931b9d1f1519ce66b9cbfe72d8563b75c1204d255322254d1964dd"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:fb8021f4dc3296de18737e3b8b34994411da725374d74a0b9eb32a738f2a23d0"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:0d764913374ba21df8cd618045b371ed104ce55c6276de7c59e554a4f5ba26fd"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:2bad302ae2427454ef9dab71bd5c3ad67188860c39d36daee38f72031edb39c4"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:032b09d86c785c3984e975378b7861f2a68ac44e22ba7b07b93a536b2524eeb8"}, + {file = "ast_serialize-0.13.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:929eda0c419e622ab49df3a4c45f81c57dcc10874bbad55d0470a4a03324dac3"}, + {file = "ast_serialize-0.13.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bf53a3ba1cbe45399fc1e4ccd684b8f2a11cca450f89706b79122f478534c6f5"}, + {file = "ast_serialize-0.13.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4bcf1d5f4eefecaa325a34ecf079b2741189e7d0821523f92cb343a389a67489"}, + {file = "ast_serialize-0.13.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1b158f1c9e3a0d386913924c57995d6df78224868a4d0f215bdd41587c97b34a"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a6201b69ce42e919f2fd9a559138d5410b5d8afff7c3a6b1f500b0481ff7d8"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eb0be64c9b8f1ed26637eef345abe75ea90351c92c85a8aec7838b3af971011"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f2427bddfca208e84de700044fadf9aef2769a70bae740a1bdabb89ee9e1117"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88df960e67184a71874e5b2ee014f87d1254a0dbc1f903c7bd7b1ab7733ecaef"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f99786e0d62082a5b121b3437f3167562133711df22128de1e9886f57f0b226"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:a01b515b25a03184eff4acef25b981ae007916379682e908fc8785b342a0bf3e"}, + {file = "ast_serialize-0.13.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:29422d7bcd9e7f39411571c36cec45ce8a7a9b3ad4b2e7aa64497c15a9e397f6"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ffb0f548a7dc126067fdfb7b2325ab5dfcdeecc29faf5e0f0461c96d6ebfa3d"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2876444a5f87a7c76f371af3c07c2fda563a08bc8af52568df812e8aba4bf568"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:885a62ec6481ce58ab24c6357360d7a82023b62dd7e5e7ceccf59e9662a3272b"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:2015fd0165e463aac3b1aadc6a1fa1e5820c14ca950821f4fe02f8a3f6a388d6"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:eb41e1906502ac80d24ff343f45008552a3f5b5158607959fe6a07dbfc6eb037"}, + {file = "ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ca9acc67f796aa2554fa3037dccc022a4b08b38b2ad153edfac9c9bb7cb1f3c6"}, + {file = "ast_serialize-0.13.1-cp39-abi3-win32.whl", hash = "sha256:4f98da562e5901d69c7dd30885594c5438f859edf354e9ac7517d188fb026c3a"}, + {file = "ast_serialize-0.13.1-cp39-abi3-win_amd64.whl", hash = "sha256:ddf7cbc9c8abf70102c4a088efe154bb86bfb825b0a10e556d2b15972cf99bd7"}, + {file = "ast_serialize-0.13.1-cp39-abi3-win_arm64.whl", hash = "sha256:48f11d96c9ec71119612f8e79a8c55c12f2b136716d68347433c561d27a25f43"}, + {file = "ast_serialize-0.13.1.tar.gz", hash = "sha256:401cc84e8888d4d76f2c8d296fa006ed1a5ff993b6aff90dd777c03973efd025"}, +] + +[[package]] +name = "better-diff" +version = "0.1.4" +description = "A simple library for printing better diffs based on the stdlib unified_diff format." +optional = false +python-versions = "<4.0,>=3.8" +groups = ["lint"] +files = [ + {file = "better_diff-0.1.4-py3-none-any.whl", hash = "sha256:06e63358b2047ae2695abd96316f47c6d3c38b9e641f53012279878d66d8792e"}, + {file = "better_diff-0.1.4.tar.gz", hash = "sha256:920ca76bdbcd2f0c361fa5d9a2d4727624a3545d6cb467b1b6616cad8a634de7"}, +] + +[[package]] +name = "black" +version = "25.12.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.10" +groups = ["lint"] +files = [ + {file = "black-25.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8"}, + {file = "black-25.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a"}, + {file = "black-25.12.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17dcc893da8d73d8f74a596f64b7c98ef5239c2cd2b053c0f25912c4494bf9ea"}, + {file = "black-25.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:09524b0e6af8ba7a3ffabdfc7a9922fb9adef60fed008c7cd2fc01f3048e6e6f"}, + {file = "black-25.12.0-cp310-cp310-win_arm64.whl", hash = "sha256:b162653ed89eb942758efeb29d5e333ca5bb90e5130216f8369857db5955a7da"}, + {file = "black-25.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a"}, + {file = "black-25.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be"}, + {file = "black-25.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b"}, + {file = "black-25.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5"}, + {file = "black-25.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655"}, + {file = "black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a"}, + {file = "black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783"}, + {file = "black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59"}, + {file = "black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892"}, + {file = "black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43"}, + {file = "black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5"}, + {file = "black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f"}, + {file = "black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf"}, + {file = "black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d"}, + {file = "black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce"}, + {file = "black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5"}, + {file = "black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f"}, + {file = "black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f"}, + {file = "black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83"}, + {file = "black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b"}, + {file = "black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828"}, + {file = "black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +pytokens = ">=0.3.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.5.0" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["lint"] +files = [ + {file = "click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360"}, + {file = "click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34"}, +] + +[[package]] +name = "flake8" +version = "5.0.4" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.6.1" +groups = ["lint"] +markers = "python_version < \"3.12\"" +files = [ + {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, + {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.9.0,<2.10.0" +pyflakes = ">=2.5.0,<2.6.0" + +[[package]] +name = "flake8" +version = "6.1.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +groups = ["lint"] +markers = "python_version >= \"3.12\"" +files = [ + {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, + {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.1.0,<3.2.0" + +[[package]] +name = "flake8-black" +version = "0.4.0" +description = "flake8 plugin to call black as a code style validator" +optional = false +python-versions = ">=3.9" +groups = ["lint"] +files = [ + {file = "flake8_black-0.4.0-py3-none-any.whl", hash = "sha256:288762d0c9ea065782d87eeecbcc20c69079d17fe1d0f0445f0eb0b0ffb80c39"}, + {file = "flake8_black-0.4.0.tar.gz", hash = "sha256:bf226868f695dee48d55ff6d7747e900709bfd6f605b7a378c70e711e3fc26cb"}, +] + +[package.dependencies] +black = ">=22.1.0" +flake8 = ">=3" +tomli = {version = "*", markers = "python_version < \"3.11\""} + +[package.extras] +develop = ["build", "twine"] + +[[package]] +name = "flake8-docstrings" +version = "1.7.0" +description = "Extension for flake8 which uses pydocstyle to check docstrings" +optional = false +python-versions = ">=3.7" +groups = ["lint"] +files = [ + {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"}, + {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"}, +] + +[package.dependencies] +flake8 = ">=3" +pydocstyle = ">=2.1" + +[[package]] +name = "flake8-import-order" +version = "0.19.2" +description = "Flake8 and pylama plugin that checks the ordering of import statements." +optional = false +python-versions = "*" +groups = ["lint"] +files = [ + {file = "flake8_import_order-0.19.2-py3-none-any.whl", hash = "sha256:2dfe60175e7195cf36d4c573861fd2e3258cd6650cbd7616da3c6b8193b29b7c"}, + {file = "flake8_import_order-0.19.2.tar.gz", hash = "sha256:133b3c55497631e4235074fc98a95078bba817832379f22a31f0ad2455bcb0b2"}, +] + +[package.dependencies] +pycodestyle = "*" +setuptools = "*" + +[[package]] +name = "flake8-tidy-imports" +version = "4.12.0" +description = "A flake8 plugin that helps you write tidier imports." +optional = false +python-versions = ">=3.9" +groups = ["lint"] +files = [ + {file = "flake8_tidy_imports-4.12.0-py3-none-any.whl", hash = "sha256:ab1e31a5ce07518a31c0a34cd92551f4c27639ae2c35a21364680a0318da312e"}, + {file = "flake8_tidy_imports-4.12.0.tar.gz", hash = "sha256:9254788c3b6862c2fcec0250d2dc9af089afebff9c5b8a8ac8b9525b059b06db"}, +] + +[package.dependencies] +flake8 = ">=3.8" + +[[package]] +name = "grpcio" +version = "1.85.0" +description = "HTTP/2-based RPC framework" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "grpcio-1.85.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:d617b017395ff7a84fe0aa682890a366afcdd502f2610029052165cd3778b561"}, + {file = "grpcio-1.85.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:ed8ca34cc644a82602097249bdd9af2526f2fe93c641439a19351f3ed2e91de3"}, + {file = "grpcio-1.85.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc1a478ce75e66ab9cbf7077bb12b2a31f75de091122d48845f281f8fd47a0d7"}, + {file = "grpcio-1.85.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3ec59a8175c0bcf51ac767c7baa9083ec369284e1055614bff246257349d4e19"}, + {file = "grpcio-1.85.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9eb6e4149d2e8ea7fc56052eabd36f1bd52a3c0a53f1eeec74528654a895456a"}, + {file = "grpcio-1.85.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5427356333810631bfc3541da05fa57bcd4a0bf846728d8512c9e4bb1f97b261"}, + {file = "grpcio-1.85.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55a1df9e50009d85fe674fbfe31710b6b3e83702c857114832449c498e78593f"}, + {file = "grpcio-1.85.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:de5b49ab15a350170905b03c03f88f277f83e1910fa87e6f12c63f2da7480a4b"}, + {file = "grpcio-1.85.0-cp310-cp310-win32.whl", hash = "sha256:b016a4663ee8ec37b0e555a65fcc129598404bd5c14d8c00d03f8da551ce0e30"}, + {file = "grpcio-1.85.0-cp310-cp310-win_amd64.whl", hash = "sha256:dbd651df4cf258f2073f8a10f878cee9b8723986ca69206a0da67a2f1651ae86"}, + {file = "grpcio-1.85.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2c6a55a4cb43fe9b14d4aae123ee655cdeb288d8810bec8f8c1adce0d4359d8e"}, + {file = "grpcio-1.85.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:299b13839203e82be15216ae4c3379801a3ad216feff05b165f14962f0939b0c"}, + {file = "grpcio-1.85.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:312939c9afac5d9d5e3b9e50c40a9ebfbe444ca35b28a99ce34903cc9279a5b5"}, + {file = "grpcio-1.85.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6127bab580cc247c6c62f460485afeba10ec7b9e1b338eef8bde1344acfd9ab2"}, + {file = "grpcio-1.85.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba535a7b78d5049101a896efbb4ef9edd471c322ea4fc0cc8245545b184ff687"}, + {file = "grpcio-1.85.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d115d1d4e31fa964a348e54445154cce1649da4204f07425028608982d056a2b"}, + {file = "grpcio-1.85.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:864607775e0166a4d3d6bd8276334d96f0b6e9e0a84f3964dc6005db07e9627b"}, + {file = "grpcio-1.85.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58c0ffd1eb2a0d7d216a0c8faefde999586fc017e735dfe9d1142a8481f7d0b6"}, + {file = "grpcio-1.85.0-cp311-cp311-win32.whl", hash = "sha256:0633c4ff112d2237d09ded3044b5acee4fb12de7b39ff4dcfb09b0f029c33992"}, + {file = "grpcio-1.85.0-cp311-cp311-win_amd64.whl", hash = "sha256:2dd69ec2998c2285c323bfa2e3acbd7f475881db97aa9359f2edcbf9ae284169"}, + {file = "grpcio-1.85.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c02d956990c55730efcca487a4eac64f8e0fcb3af7e53c9e89cd02a985669709"}, + {file = "grpcio-1.85.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b10f0f0a34ce6b122dbf5dcf8226cbe41509c602af926b1e55113ad9e49cc5f7"}, + {file = "grpcio-1.85.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b1ea46f63a7a9e19ce5cc09be0a6f9b347e0dcc14c4ab47b48488f6c093b8171"}, + {file = "grpcio-1.85.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e2f1c3089ed6ae75e24529a498a6b65941fdb6856c3416d62c88da944947fc88"}, + {file = "grpcio-1.85.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ddfe8b667889b9be5990ae674b13f0db0a9eb61972306038d0140a1da0f0b87"}, + {file = "grpcio-1.85.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17641f3a6ff3512c34e5c367805d2632a43a773d7990f1c4c55243df6bee8fbb"}, + {file = "grpcio-1.85.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1a94f32df519cb7efe3822f7f3c2daee0b0e2f01a281e1db5dbfb0efd1635b6e"}, + {file = "grpcio-1.85.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8572f9d7282cded6b018fb0147d09bbe7e59177ba6c452f1903682a80dc0857e"}, + {file = "grpcio-1.85.0-cp312-cp312-win32.whl", hash = "sha256:25007d0e4657dcdafa9a80677895b71de8c1d6335afb7fa6a4b35330e2d3127a"}, + {file = "grpcio-1.85.0-cp312-cp312-win_amd64.whl", hash = "sha256:d6c43a06a059a95f03013aa5fce9106a6a2b6b78cfff4003a53e0eb410c0e1d7"}, + {file = "grpcio-1.85.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:8f1c7cb18d7beb81aa65666b739f8a43261df7f9529628e639b578b6d344cdf9"}, + {file = "grpcio-1.85.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:874af68625a2ff2585cb112e7583c19a7cb463ab638a7fe5b8a58f76e6d4e7c2"}, + {file = "grpcio-1.85.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc2865f523feeac453f35c762a2e4155df58565229d77069b211f2e21de2e79a"}, + {file = "grpcio-1.85.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c6c39eee05f6b98266315486a93f36d73a59adc4d34f8b63f1b5725f897d8ea"}, + {file = "grpcio-1.85.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:55cee7cfe71b29f6b377681936f9a5f35e59f5c941a4d84e2712cb309753e7a9"}, + {file = "grpcio-1.85.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1a8ee918b5b18c82be2e5f30978258269b858d7e16a36e8729c09d9654bac2f1"}, + {file = "grpcio-1.85.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3c49f03d8d74196ca6d1351b749aab3fb3362c850286f28aafcb7cd773134b04"}, + {file = "grpcio-1.85.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:372e9cbf91f74127652138b020cf15c6a44541a91aee89c50a5943d1996b44aa"}, + {file = "grpcio-1.85.0-cp313-cp313-win32.whl", hash = "sha256:087f297442a62e267f37df0cd03681159c225b8e73741b94d55c515e7a4df81e"}, + {file = "grpcio-1.85.0-cp313-cp313-win_amd64.whl", hash = "sha256:09ab4af4c4c74baa9b17f9a6fcdcef5be98357774417d624191ec41690df4bb3"}, + {file = "grpcio-1.85.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:86bd1d2e197d3e4cde1c04136db48bf22a6fe873b7f55c83e9747d5b36f0cbb4"}, + {file = "grpcio-1.85.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7a24e53259c2005c77b03c48272eec4b374eba027d077e84d0e99f9e30747439"}, + {file = "grpcio-1.85.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6248ed9a86c4191a0d8d7fe54eaea0511aadcadc2ec128daa995eaac91e42880"}, + {file = "grpcio-1.85.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:c5f993fa6b3bf0af33ed7c2bc42f58d22f10964607cf614c2c08b67db80ced60"}, + {file = "grpcio-1.85.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d2ba969495b45ff5e0d10451898387427906b82e28c3c1cfae2dda006e25e18d"}, + {file = "grpcio-1.85.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9c91ad19d92ff81600c087db4829c26b47f9dafd04ed0a26c35e21a05d20f671"}, + {file = "grpcio-1.85.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:26147a7a3c9d1c865cc16b3f98e5c38e5606932aba236b7627ca7eb4378c5c7e"}, + {file = "grpcio-1.85.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:beb6ffcf5253f4247efa47837d876e42157969127657fad4a68be181830ddadd"}, + {file = "grpcio-1.85.0-cp314-cp314-win32.whl", hash = "sha256:a03fee5e413938ddb71aa2fd7c52520e2fc3aec2e9602d50464272c75b274a54"}, + {file = "grpcio-1.85.0-cp314-cp314-win_amd64.whl", hash = "sha256:a11c8c322a19d6d3cd6fb645127e8f1ca4e8041d05e41e9b24c7678ec7391021"}, + {file = "grpcio-1.85.0-cp315-cp315-linux_armv7l.whl", hash = "sha256:e0b87fde3b80955343b37cf9b9deac52432e9e7a4b3e0f92abbd61fee0f7b184"}, + {file = "grpcio-1.85.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:27b024f8420c9d63f7df9f51d4c2d2e54fcf3b26c9963041f5fc75b83f35e87b"}, + {file = "grpcio-1.85.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:dc726ae4f2f4e2215cb1b987b8ddd7f4f2f253d6ceb48cfbb9b88555d325e10a"}, + {file = "grpcio-1.85.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3b463b3b97f35149a3f69234de92eddfd7c94c9c19794e3b264ccba7b6b03fac"}, + {file = "grpcio-1.85.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:426e41a32d10f841f41fc67e4e6d150af7e7c7c3433fe61fa3a0ca19a8dc9885"}, + {file = "grpcio-1.85.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a8dd5c3823194811c5e6932b454bcaa7b8e474a7690ed4ce6246bd502849a82c"}, + {file = "grpcio-1.85.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c8accda49358f99875057ff1ec6a79fead12dac41ac63c1c1e1757a2aab14c1d"}, + {file = "grpcio-1.85.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2a04b67a146f1a95e2bd281614be89b662973535a94eac944135ea86b2e5a7ef"}, + {file = "grpcio-1.85.0-cp315-cp315-win32.whl", hash = "sha256:b2da79c303f23e66d08baaa0c78d8b8630c700e1562babb1f326ada0a97ee5d0"}, + {file = "grpcio-1.85.0-cp315-cp315-win_amd64.whl", hash = "sha256:dd88a98073ebc0e6771ff8c2fd9451fbf887f575b0da9d30246ee1b2813f6b57"}, + {file = "grpcio-1.85.0.tar.gz", hash = "sha256:8aa2647afcbe5a87783e9626a2ece6277db33c6604c21690d19b5bd15609e36d"}, +] + +[package.dependencies] +typing-extensions = ">=4.12,<5.0" + +[package.extras] +protobuf = ["grpcio-tools (>=1.85.0)"] + +[[package]] +name = "grpcio-tools" +version = "1.49.1" +description = "Protobuf code generator for gRPC" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version < \"3.12\"" +files = [ + {file = "grpcio-tools-1.49.1.tar.gz", hash = "sha256:84cc64e5b46bad43d5d7bd2fd772b656eba0366961187a847e908e2cb735db91"}, + {file = "grpcio_tools-1.49.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:2dfb6c7ece84d46bd690b23d3e060d18115c8bc5047d2e8a33e6747ed323a348"}, + {file = "grpcio_tools-1.49.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:8f452a107c054a04db2570f7851a07f060313c6e841b0d394ce6030d598290e6"}, + {file = "grpcio_tools-1.49.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:6a198871b582287213c4d70792bf275e1d7cf34eed1d019f534ddf4cd15ab039"}, + {file = "grpcio_tools-1.49.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0cca67a7d0287bdc855d81fdd38dc949c4273273a74f832f9e520abe4f20bc6"}, + {file = "grpcio_tools-1.49.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaff4c89eecb37c247b93025410db68114d97fa093cbb028e9bd7cda5912473"}, + {file = "grpcio_tools-1.49.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bb8773118ad315db317d7b22b5ff75d649ca20931733281209e7cbd8c0fad53e"}, + {file = "grpcio_tools-1.49.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7cc5534023735b8a8f56760b7c533918f874ce5a9064d7c5456d2709ae2b31f9"}, + {file = "grpcio_tools-1.49.1-cp310-cp310-win32.whl", hash = "sha256:d277642acbe305f5586f9597b78fb9970d6633eb9f89c61e429c92c296c37129"}, + {file = "grpcio_tools-1.49.1-cp310-cp310-win_amd64.whl", hash = "sha256:eed599cf08fc1a06c72492d3c5750c32f58de3750eddd984af1f257c14326701"}, + {file = "grpcio_tools-1.49.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:9e5c13809ab2f245398e8446c4c3b399a62d591db651e46806cccf52a700452e"}, + {file = "grpcio_tools-1.49.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:ab3d0ee9623720ee585fdf3753b3755d3144a4a8ae35bca8e3655fa2f41056be"}, + {file = "grpcio_tools-1.49.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ba87e3512bc91d78bf9febcfb522eadda171d2d4ddaf886066b0f01aa4929ad"}, + {file = "grpcio_tools-1.49.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e13b3643e7577a3ec13b79689eb4d7548890b1e104c04b9ed6557a3c3dd452"}, + {file = "grpcio_tools-1.49.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:324f67d9cb4b7058b6ce45352fb64c20cc1fa04c34d97ad44772cfe6a4ae0cf5"}, + {file = "grpcio_tools-1.49.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a64bab81b220c50033f584f57978ebbea575f09c1ccee765cd5c462177988098"}, + {file = "grpcio_tools-1.49.1-cp311-cp311-win32.whl", hash = "sha256:f632d376f92f23e5931697a3acf1b38df7eb719774213d93c52e02acd2d529ac"}, + {file = "grpcio_tools-1.49.1-cp311-cp311-win_amd64.whl", hash = "sha256:28ff2b978d9509474928b9c096a0cce4eaa9c8f7046136aee1545f6211ed8126"}, + {file = "grpcio_tools-1.49.1-cp37-cp37m-linux_armv7l.whl", hash = "sha256:46afd3cb7e555187451a5d283f108cdef397952a662cb48680afc615b158864a"}, + {file = "grpcio_tools-1.49.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:9284568b728e41fa8f7e9c2e7399545d605f75d8072ef0e9aa2a05655cb679eb"}, + {file = "grpcio_tools-1.49.1-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:aa34442cf787732cb41f2aa6172007e24f480b8b9d3dc5166de80d63e9072ea4"}, + {file = "grpcio_tools-1.49.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8c9eb5a4250905414cd53a68caea3eb8f0c515aadb689e6e81b71ebe9ab5c6"}, + {file = "grpcio_tools-1.49.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab15db024051bf21feb21c29cb2c3ea0a2e4f5cf341d46ef76e17fcf6aaef164"}, + {file = "grpcio_tools-1.49.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:502084b622f758bef620a9107c2db9fcdf66d26c7e0e481d6bb87db4dc917d70"}, + {file = "grpcio_tools-1.49.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4085890b77c640085f82bf1e90a0ea166ce48000bc2f5180914b974783c9c0a8"}, + {file = "grpcio_tools-1.49.1-cp37-cp37m-win32.whl", hash = "sha256:da0edb984699769ce02e18e3392d54b59a7a3f93acd285a68043f5bde4fc028e"}, + {file = "grpcio_tools-1.49.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9887cd622770271101a7dd1832845d64744c3f88fd11ccb2620394079197a42e"}, + {file = "grpcio_tools-1.49.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:8440fe7dae6a40c279e3a24b82793735babd38ecbb0d07bb712ff9c8963185d9"}, + {file = "grpcio_tools-1.49.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:b5de2bb7dd6b6231da9b1556ade981513330b740e767f1d902c71ceee0a7d196"}, + {file = "grpcio_tools-1.49.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:1e6f06a763aea7836b63d9c117347f2bf7038008ceef72758815c9e09c5fb1fc"}, + {file = "grpcio_tools-1.49.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e31562f90120318c5395aabec0f2f69ad8c14b6676996b7730d9d2eaf9415d57"}, + {file = "grpcio_tools-1.49.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49ef9a4e389a618157a9daa9fafdfeeaef1ece9adda7f50f85db928f24d4b3e8"}, + {file = "grpcio_tools-1.49.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b384cb8e8d9bcb55ee8f9b064374561c7a1a05d848249581403d36fc7060032f"}, + {file = "grpcio_tools-1.49.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:73732f77943ac3e898879cbb29c27253aa3c47566b8a59780fd24c6a54de1b66"}, + {file = "grpcio_tools-1.49.1-cp38-cp38-win32.whl", hash = "sha256:b594b2745a5ba9e7a76ce561bc5ab40bc65bb44743c505529b1e4f12af29104d"}, + {file = "grpcio_tools-1.49.1-cp38-cp38-win_amd64.whl", hash = "sha256:680fbc88f8709ddcabb88f86749f2d8e429160890cff2c70680880a6970d4eef"}, + {file = "grpcio_tools-1.49.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:e8c3869121860f6767eedb7d24fc54dfd71e737fdfbb26e1334684606f3274fd"}, + {file = "grpcio_tools-1.49.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:73e9d7c886ba10e20c97d1dab0ff961ba5800757ae5e31be21b1cda8130c52f8"}, + {file = "grpcio_tools-1.49.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:1760de2dd2c4f08de87b039043a4797f3c17193656e7e3eb84e92f0517083c0c"}, + {file = "grpcio_tools-1.49.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd4b1e216dd04d9245ee8f4e601a1f98c25e6e417ea5cf8d825c50589a8b447e"}, + {file = "grpcio_tools-1.49.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c28751ab5955cae563d07677e799233f0fe1c0fc49d9cbd61ff1957e83617f"}, + {file = "grpcio_tools-1.49.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c24239c3ee9ed16314c14b4e24437b5079ebc344f343f33629a582f8699f583b"}, + {file = "grpcio_tools-1.49.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:892d3dacf1942820f0b7a868a30e6fbcdf5bec08543b682c7274b0101cee632d"}, + {file = "grpcio_tools-1.49.1-cp39-cp39-win32.whl", hash = "sha256:704d21509ec06efc9d034dbe70e7152715aac004941f4f0f553cf3a0aff15bd5"}, + {file = "grpcio_tools-1.49.1-cp39-cp39-win_amd64.whl", hash = "sha256:1efa0c221c719433f441ac0e026fc3c4dbc9a1a08a552ecdc707775e2f2fbbae"}, +] + +[package.dependencies] +grpcio = ">=1.49.1" +protobuf = ">=4.21.3,<5.0" +setuptools = "*" + +[[package]] +name = "grpcio-tools" +version = "1.59.0" +description = "Protobuf code generator for gRPC" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version == \"3.12\"" +files = [ + {file = "grpcio-tools-1.59.0.tar.gz", hash = "sha256:aa4018f2d8662ac4d9830445d3d253a11b3e096e8afe20865547137aa1160e93"}, + {file = "grpcio_tools-1.59.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:882b809b42b5464bee55288f4e60837297f9618e53e69ae3eea6d61b05ce48fa"}, + {file = "grpcio_tools-1.59.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:4499d4bc5aa9c7b645018d8b0db4bebd663d427aabcd7bee7777046cb1bcbca7"}, + {file = "grpcio_tools-1.59.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:f381ae3ad6a5eb27aad8d810438937d8228977067c54e0bd456fce7e11fdbf3d"}, + {file = "grpcio_tools-1.59.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1c684c0d9226d04cadafced620a46ab38c346d0780eaac7448da96bf12066a3"}, + {file = "grpcio_tools-1.59.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40cbf712769242c2ba237745285ef789114d7fcfe8865fc4817d87f20015e99a"}, + {file = "grpcio_tools-1.59.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1df755951f204e65bf9232a9cac5afe7d6b8e4c87ac084d3ecd738fdc7aa4174"}, + {file = "grpcio_tools-1.59.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de156c18b0c638aaee3be6ad650c8ba7dec94ed4bac26403aec3dce95ffe9407"}, + {file = "grpcio_tools-1.59.0-cp310-cp310-win32.whl", hash = "sha256:9af7e138baa9b2895cf1f3eb718ac96fc5ae2f8e31fca405e21e0e5cd1643c52"}, + {file = "grpcio_tools-1.59.0-cp310-cp310-win_amd64.whl", hash = "sha256:f14a6e4f700dfd30ff8f0e6695f944affc16ae5a1e738666b3fae4e44b65637e"}, + {file = "grpcio_tools-1.59.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:db030140d0da2368319e2f23655df3baec278c7e0078ecbe051eaf609a69382c"}, + {file = "grpcio_tools-1.59.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:eeed386971bb8afc3ec45593df6a1154d680d87be1209ef8e782e44f85f47e64"}, + {file = "grpcio_tools-1.59.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:962d1a3067129152cee3e172213486cb218a6bad703836991f46f216caefcf00"}, + {file = "grpcio_tools-1.59.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26eb2eebf150a33ebf088e67c1acf37eb2ac4133d9bfccbaa011ad2148c08b42"}, + {file = "grpcio_tools-1.59.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b2d6da553980c590487f2e7fd3ec9c1ad8805ff2ec77977b92faa7e3ca14e1f"}, + {file = "grpcio_tools-1.59.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:335e2f355a0c544a88854e2c053aff8a3f398b84a263a96fa19d063ca1fe513a"}, + {file = "grpcio_tools-1.59.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:204e08f807b1d83f5f0efea30c4e680afe26a43dec8ba614a45fa698a7ef0a19"}, + {file = "grpcio_tools-1.59.0-cp311-cp311-win32.whl", hash = "sha256:05bf7b3ed01c8a562bb7e840f864c58acedbd6924eb616367c0bd0a760bdf483"}, + {file = "grpcio_tools-1.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:df85096fcac7cea8aa5bd84b7a39c4cdbf556b93669bb4772eb96aacd3222a4e"}, + {file = "grpcio_tools-1.59.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:240a7a3c2c54f77f1f66085a635bca72003d02f56a670e7db19aec531eda8f78"}, + {file = "grpcio_tools-1.59.0-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:6119f62c462d119c63227b9534210f0f13506a888151b9bf586f71e7edf5088b"}, + {file = "grpcio_tools-1.59.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:387662bee8e4c0b52cc0f61eaaca0ca583f5b227103f685b76083a3590a71a3e"}, + {file = "grpcio_tools-1.59.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f0da5861ee276ca68493b217daef358960e8527cc63c7cb292ca1c9c54939af"}, + {file = "grpcio_tools-1.59.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f0806de1161c7f248e4c183633ee7a58dfe45c2b77ddf0136e2e7ad0650b1b"}, + {file = "grpcio_tools-1.59.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:c683be38a9bf4024c223929b4cd2f0a0858c94e9dc8b36d7eaa5a48ce9323a6f"}, + {file = "grpcio_tools-1.59.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f965707da2b48a33128615bcfebedd215a3a30e346447e885bb3da37a143177a"}, + {file = "grpcio_tools-1.59.0-cp312-cp312-win32.whl", hash = "sha256:2ee960904dde12a7fa48e1591a5b3eeae054bdce57bacf9fd26685a98138f5bf"}, + {file = "grpcio_tools-1.59.0-cp312-cp312-win_amd64.whl", hash = "sha256:71cc6db1d66da3bc3730d9937bddc320f7b1f1dfdff6342bcb5741515fe4110b"}, + {file = "grpcio_tools-1.59.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:f6263b85261b62471cb97b7505df72d72b8b62e5e22d8184924871a6155b4dbf"}, + {file = "grpcio_tools-1.59.0-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:b8e95d921cc2a1521d4750eedefec9f16031457920a6677edebe9d1b2ad6ae60"}, + {file = "grpcio_tools-1.59.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:cb63055739808144b541986291679d643bae58755d0eb082157c4d4c04443905"}, + {file = "grpcio_tools-1.59.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c4634b3589efa156a8d5860c0a2547315bd5c9e52d14c960d716fe86e0927be"}, + {file = "grpcio_tools-1.59.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d970aa26854f535ffb94ea098aa8b43de020d9a14682e4a15dcdaeac7801b27"}, + {file = "grpcio_tools-1.59.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:821dba464d84ebbcffd9d420302404db2fa7a40c7ff4c4c4c93726f72bfa2769"}, + {file = "grpcio_tools-1.59.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0548e901894399886ff4a4cd808cb850b60c021feb4a8977a0751f14dd7e55d9"}, + {file = "grpcio_tools-1.59.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bb87158dbbb9e5a79effe78d54837599caa16df52d8d35366e06a91723b587ae"}, + {file = "grpcio_tools-1.59.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:1d551ff42962c7c333c3da5c70d5e617a87dee581fa2e2c5ae2d5137c8886779"}, + {file = "grpcio_tools-1.59.0-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:4ee443abcd241a5befb05629013fbf2eac637faa94aaa3056351aded8a31c1bc"}, + {file = "grpcio_tools-1.59.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:520c0c83ea79d14b0679ba43e19c64ca31d30926b26ad2ca7db37cbd89c167e2"}, + {file = "grpcio_tools-1.59.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fc02a6e517c34dcf885ff3b57260b646551083903e3d2c780b4971ce7d4ab7c"}, + {file = "grpcio_tools-1.59.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6aec8a4ed3808b7dfc1276fe51e3e24bec0eeaf610d395bcd42934647cf902a3"}, + {file = "grpcio_tools-1.59.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:99b3bde646720bbfb77f263f5ba3e1a0de50632d43c38d405a0ef9c7e94373cd"}, + {file = "grpcio_tools-1.59.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51d9595629998d8b519126c5a610f15deb0327cd6325ed10796b47d1d292e70b"}, + {file = "grpcio_tools-1.59.0-cp38-cp38-win32.whl", hash = "sha256:bfa4b2b7d21c5634b62e5f03462243bd705adc1a21806b5356b8ce06d902e160"}, + {file = "grpcio_tools-1.59.0-cp38-cp38-win_amd64.whl", hash = "sha256:9ed05197c5ab071e91bcef28901e97ca168c4ae94510cb67a14cb4931b94255a"}, + {file = "grpcio_tools-1.59.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:498e7be0b14385980efa681444ba481349c131fc5ec88003819f5d929646947c"}, + {file = "grpcio_tools-1.59.0-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:b519f2ecde9a579cad2f4a7057d5bb4e040ad17caab8b5e691ed7a13b9db0be9"}, + {file = "grpcio_tools-1.59.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:ef3e8aca2261f7f07436d4e2111556c1fb9bf1f9cfcdf35262743ccdee1b6ce9"}, + {file = "grpcio_tools-1.59.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a7f226b741b2ebf7e2d0779d2c9b17f446d1b839d59886c1619e62cc2ae472"}, + {file = "grpcio_tools-1.59.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:784aa52965916fec5afa1a28eeee6f0073bb43a2a1d7fedf963393898843077a"}, + {file = "grpcio_tools-1.59.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e312ddc2d8bec1a23306a661ad52734f984c9aad5d8f126ebb222a778d95407d"}, + {file = "grpcio_tools-1.59.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:868892ad9e00651a38dace3e4924bae82fc4fd4df2c65d37b74381570ee8deb1"}, + {file = "grpcio_tools-1.59.0-cp39-cp39-win32.whl", hash = "sha256:a4f6cae381f21fee1ef0a5cbbbb146680164311157ae618edf3061742d844383"}, + {file = "grpcio_tools-1.59.0-cp39-cp39-win_amd64.whl", hash = "sha256:4a10e59cca462208b489478340b52a96d64e8b8b6f1ac097f3e8cb211d3f66c0"}, +] + +[package.dependencies] +grpcio = ">=1.59.0" +protobuf = ">=4.21.6,<5.0" +setuptools = "*" + +[[package]] +name = "grpcio-tools" +version = "1.67.0" +description = "Protobuf code generator for gRPC" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version == \"3.13\"" +files = [ + {file = "grpcio_tools-1.67.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:12aa38af76b5ef00a55808c7c374ed18d5dc7cc8081b717e56da3c50df1776e2"}, + {file = "grpcio_tools-1.67.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:b0b03d055127bbc7c629454804b53b5cad2cedfcf904576d159a8a04c22b8e66"}, + {file = "grpcio_tools-1.67.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:02b0b50c59a8f7428326197027a2f586d216c46138c547f861533c46bff78bfe"}, + {file = "grpcio_tools-1.67.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2afdfe151ed9edbd4a3fd646716f83b58010769c57f9c0aa1cf4c3bfb1240a8"}, + {file = "grpcio_tools-1.67.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3eeb87575b2b360c5ef5aef22eb76cfdd6a255d2f628a48ab0e5a61a0039fb"}, + {file = "grpcio_tools-1.67.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ead78089c4771605a1ff8894e47f2267440693f1beeee06fd5a788aede83370f"}, + {file = "grpcio_tools-1.67.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0671dcdccef09ca4eb415c1d6f470a857c6486733c146676f6810a3ade1d42cb"}, + {file = "grpcio_tools-1.67.0-cp310-cp310-win32.whl", hash = "sha256:a7398d90b8c7da479aec8f853d3664d5a93c209f8ac3bd41cb7ae4e8677a45c6"}, + {file = "grpcio_tools-1.67.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7e7d70a74df7e07be7cceaa694b7e8e5f3bef8e0299906f60885ecf7a40adb4"}, + {file = "grpcio_tools-1.67.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:655716bf931a22a090134d87953710033640996d31e36f5f9b0106ff5f552d8e"}, + {file = "grpcio_tools-1.67.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:484ae782f9d3ff58e0bbb2f4cad14d5f5d9132fc701835b1dffd2c2a06f73ba6"}, + {file = "grpcio_tools-1.67.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:f3e34de876efe1273f91e25ef241e449ed7f9411472dd9ff56d2039618017c30"}, + {file = "grpcio_tools-1.67.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8301719edde2c3d388995703cdd962f558b76e9750405f772dce61402e4c3d0"}, + {file = "grpcio_tools-1.67.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1629ea246044ccd473d9ac4c9f137a440d830b5e08d35225e1b354dbbb15b75d"}, + {file = "grpcio_tools-1.67.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d77a3c5cec0065267ca1a0b2589ececd1277ce25aa67f13ec50c816ee6f26f7f"}, + {file = "grpcio_tools-1.67.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf992bcc7d9e6eaa20705056e1b955593092a38cec1746fef389d873ab2056"}, + {file = "grpcio_tools-1.67.0-cp311-cp311-win32.whl", hash = "sha256:7e6e3db119c38629e0767cdb2ee18726ecc87e2249117d4c9e7ce06ea8c894ea"}, + {file = "grpcio_tools-1.67.0-cp311-cp311-win_amd64.whl", hash = "sha256:c6c27aec301a0e6cf231f9ee1c467c64002af51170fa7c0f3bb10bbfcd03fee7"}, + {file = "grpcio_tools-1.67.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:dca7f053cbdb26a587d4410ddb893877c585fb60a31f22fdd128e4f7c4dab27c"}, + {file = "grpcio_tools-1.67.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:de8c4f68ffa690769d84329c17c7fdd5fbe4c61b8f8a0de03f1ad8ef8bb06963"}, + {file = "grpcio_tools-1.67.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:6e4ecb24c27a78f09fead45d4ed873805d6026124ccb6793b6fb93a490b78ddf"}, + {file = "grpcio_tools-1.67.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:004d6ef1b5f724480f05c0bdc904bf8c78c43d633c537d99abe51b52ce0cadeb"}, + {file = "grpcio_tools-1.67.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dd257072c86eb9b36791b3674a513a215ba76bbdd38fc228f0e8c6dc5ce3524"}, + {file = "grpcio_tools-1.67.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a8cca551317ed26e17d13b6ee27b2bd62f5fe9b3842b4e88389deb984f995848"}, + {file = "grpcio_tools-1.67.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a7ac3b4f837c693142f6688b629d1f6408f6ab250d927159b572555f5339fe25"}, + {file = "grpcio_tools-1.67.0-cp312-cp312-win32.whl", hash = "sha256:95feec33388e2a8f72c360a68efe6f0bfed9c771e94d21b7f2359d0010f60219"}, + {file = "grpcio_tools-1.67.0-cp312-cp312-win_amd64.whl", hash = "sha256:50a31d035193ebe7154181eac84734e25bdcdb36adba849d3b2adf1c3b0c382b"}, + {file = "grpcio_tools-1.67.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:9ecb7c2e5da052a3feaeaa83d8f2a946a8feec8a50751b0e6175da982b49ebb1"}, + {file = "grpcio_tools-1.67.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3c52164f2b9d41c6d75464bb45f45737dcb421e92e98d85d94fda100c67a24d8"}, + {file = "grpcio_tools-1.67.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:471f58b919767290260d427dd9b760796e6208ee5fcda2f76bb8bd585ff842ec"}, + {file = "grpcio_tools-1.67.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c6bcdf38f672721c093c92b1fb1f9a02a365acc5bd42e1c69fe6e904b26081"}, + {file = "grpcio_tools-1.67.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:833b1eb9c03d28a798294523f75294055eff78fa897adf797876337b901afeb9"}, + {file = "grpcio_tools-1.67.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:1db92ad6ade1946fc5705eb04956fcfdb3a0a4682de8dc3fce31cb97b6e4fcb8"}, + {file = "grpcio_tools-1.67.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:38128310ded818e1044c0cd0979d76f7c0d3c3946a526a8aa39cd258624c3bf3"}, + {file = "grpcio_tools-1.67.0-cp313-cp313-win32.whl", hash = "sha256:db57930dc20ab678311727883bdb9f122daf06c14f3fd3067c9ccedb7eb056c3"}, + {file = "grpcio_tools-1.67.0-cp313-cp313-win_amd64.whl", hash = "sha256:7de44d8d3bb920a4973a559f2950d03382fa4aed4880306416ffa73d24838477"}, + {file = "grpcio_tools-1.67.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:793896648734aad3ad8f26795dcdd6040aecd35efef43fcbb67d221373e6379a"}, + {file = "grpcio_tools-1.67.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:941418cba6a8adfcac3ff7ff3bdef00b55a44d673634c15bddcfa7778e49239e"}, + {file = "grpcio_tools-1.67.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:0d63ff6be6f3d0294249fc7a21f26f06c9cc209130c5328907cd678406d7d232"}, + {file = "grpcio_tools-1.67.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af80ced3ba49377ef7bec93e9ccbfa357875460e9a624ed12d9a7d5348741a76"}, + {file = "grpcio_tools-1.67.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3c3fbb4a6d10764295540579492397bc7a3334e1a92dd17a4bc7b69159cf70a"}, + {file = "grpcio_tools-1.67.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a05c10fb783f609d16e1f754ebad9bb432a1adbfc46139d154e8fd6b15f59988"}, + {file = "grpcio_tools-1.67.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ea8af001f08c678ab59e2bf2614d8b09d62210e540f7af1129c172fe4fd330c7"}, + {file = "grpcio_tools-1.67.0-cp38-cp38-win32.whl", hash = "sha256:004d329aee385fa874979196e5359a967c370d31813f61eba88043ccaa2e06d8"}, + {file = "grpcio_tools-1.67.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc43593bd051abb73a5d130fc041923144089ac459fb01165960106ebb686fd0"}, + {file = "grpcio_tools-1.67.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:d285c036ddfc2618c4db60b584409dd8313d41473bd46177c763ea22ed9aeb1f"}, + {file = "grpcio_tools-1.67.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:623fdad489447e1565d0ba5a818d54b4e74cd73800b6a32c4c009601c7f7a36c"}, + {file = "grpcio_tools-1.67.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:24536af8a5f8e532fbd996c1763eff51526d1d1563f9499ff5ffffb9a08811f3"}, + {file = "grpcio_tools-1.67.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd64a9a8eb675dd2aa59cb4b2ab025a3b02ae1bb9e483c7fb518ffa0f0755cda"}, + {file = "grpcio_tools-1.67.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f94378bc90fb008b0a56237748aa42c787fd86c392e7df3d65f0fe7fcd93844a"}, + {file = "grpcio_tools-1.67.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d0f39a9d860a6768574cd77b5d9ad81513fa1c575d3a050d4e72e6d79dcd62f3"}, + {file = "grpcio_tools-1.67.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c578f1306bfd0dd0668e24f8c04d61529928de2660217022a947a56be177bc2d"}, + {file = "grpcio_tools-1.67.0-cp39-cp39-win32.whl", hash = "sha256:0c2cf8d09bdc05e0550ad413e0bc0d552500bb7f98d36079b7b9d38e064e02f7"}, + {file = "grpcio_tools-1.67.0-cp39-cp39-win_amd64.whl", hash = "sha256:cc570bcd9c9681bb011f746ea90cc50559be629227aaaaae9fde8549525f0287"}, + {file = "grpcio_tools-1.67.0.tar.gz", hash = "sha256:181b3d4e61b83142c182ec366f3079b0023509743986e54c9465ca38cac255f8"}, +] + +[package.dependencies] +grpcio = ">=1.67.0" +protobuf = ">=5.26.1,<6.0" +setuptools = "*" + +[[package]] +name = "grpcio-tools" +version = "1.75.1" +description = "Protobuf code generator for gRPC" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.14\"" +files = [ + {file = "grpcio_tools-1.75.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:ae0f04d5ec8b8e13476bf516a08fc1de4e58c6bf79f99123a6b964ca7d02c790"}, + {file = "grpcio_tools-1.75.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:24a881ad7292e904fc256892b647da17d9137ef2e72faf8b7c8e515314ad1377"}, + {file = "grpcio_tools-1.75.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1b5810ace274dba12ecfac69ac32c8047c6ee0200a23274cb4885ed4187271f8"}, + {file = "grpcio_tools-1.75.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ab33993288b97b1180e092fa447a8ce00fbc8c59d67b23553245b88d14fe36bb"}, + {file = "grpcio_tools-1.75.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4cac693621043ef11d3ab2318e811d919779f8cd5011ba8e37f44c178c831d94"}, + {file = "grpcio_tools-1.75.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a09cd5d267b296af67116fe098633ad770bc8c19831a5f3c896f65fad90c1064"}, + {file = "grpcio_tools-1.75.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dff4bcb4d16cf9ef745c1984394ed15187e6c23d73d71377377deaf443d11358"}, + {file = "grpcio_tools-1.75.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:16d5986b37e2a9203f85e456c7ff8705b932718021d408adfe4a79e0f4d95949"}, + {file = "grpcio_tools-1.75.1-cp310-cp310-win32.whl", hash = "sha256:3fbac14998bfadc6b9140b6339dbc5f673700ebb4d45ba0c4d4fbe0ffb8559a9"}, + {file = "grpcio_tools-1.75.1-cp310-cp310-win_amd64.whl", hash = "sha256:b56e495844eb899de721eb77d9e077192bdeb40842f598481d32a8f6de3db124"}, + {file = "grpcio_tools-1.75.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:f0635231feb70a9d551452829943a1a5fa651283e7a300aadc22df5ea5da696f"}, + {file = "grpcio_tools-1.75.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:626293296ef7e2d87ab1a80b81a55eef91883c65b59a97576099a28b9535100b"}, + {file = "grpcio_tools-1.75.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:071339d90f1faab332ce4919c815a10b9c3ed2c09473f550f686bf9cc148579f"}, + {file = "grpcio_tools-1.75.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:44195f58c052fa935b78c7438c85cbcd4b273dd685028e4f6d4d7b30d47daad1"}, + {file = "grpcio_tools-1.75.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:860fafdb85726029d646c99859ff7bdca5aae61b5ff038c3bd355fc1ec6b2764"}, + {file = "grpcio_tools-1.75.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4559547a0cb3d3db1b982eea87d4656036339b400f48127fef932210672fb59e"}, + {file = "grpcio_tools-1.75.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9af65a310807d7f36a8f7cddea142fe97d6dffba74444f38870272f2e5a3a06b"}, + {file = "grpcio_tools-1.75.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c1de31aefc0585d2f915a7cd0994d153547495b8d79c44c58048a3ede0b65be"}, + {file = "grpcio_tools-1.75.1-cp311-cp311-win32.whl", hash = "sha256:efaf95fcaa5d3ac1bcfe44ceed9e2512eb95b5c8c476569bdbbe2bee4b59c8a9"}, + {file = "grpcio_tools-1.75.1-cp311-cp311-win_amd64.whl", hash = "sha256:7cefe76fc35c825f0148d60d2294a527053d0f5dd6a60352419214a8c53223c9"}, + {file = "grpcio_tools-1.75.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:49b68936cf212052eeafa50b824e17731b78d15016b235d36e0d32199000b14c"}, + {file = "grpcio_tools-1.75.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:08cb6e568e58b76a2178ad3b453845ff057131fff00f634d7e15dcd015cd455b"}, + {file = "grpcio_tools-1.75.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:168402ad29a249092673079cf46266936ec2fb18d4f854d96e9c5fa5708efa39"}, + {file = "grpcio_tools-1.75.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:bbae11c29fcf450730f021bfc14b12279f2f985e2e493ccc2f133108728261db"}, + {file = "grpcio_tools-1.75.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38c6c7d5d4800f636ee691cd073db1606d1a6a76424ca75c9b709436c9c20439"}, + {file = "grpcio_tools-1.75.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:626f6a61a8f141dde9a657775854d1c0d99509f9a2762b82aa401a635f6ec73d"}, + {file = "grpcio_tools-1.75.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f61a8334ae38d4f98c744a732b89527e5af339d17180e25fff0676060f8709b7"}, + {file = "grpcio_tools-1.75.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd0c3fb40d89a1e24a41974e77c7331e80396ab7cde39bc396a13d6b5e2a750b"}, + {file = "grpcio_tools-1.75.1-cp312-cp312-win32.whl", hash = "sha256:004bc5327593eea48abd03be3188e757c3ca0039079587a6aac24275127cac20"}, + {file = "grpcio_tools-1.75.1-cp312-cp312-win_amd64.whl", hash = "sha256:23952692160b5fe7900653dfdc9858dc78c2c42e15c27e19ee780c8917ba6028"}, + {file = "grpcio_tools-1.75.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:ca9e116aab0ecf4365fc2980f2e8ae1b22273c3847328b9a8e05cbd14345b397"}, + {file = "grpcio_tools-1.75.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:9fe87a926b65eb7f41f8738b6d03677cc43185ff77a9d9b201bdb2f673f3fa1e"}, + {file = "grpcio_tools-1.75.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:45503a6094f91b3fd31c3d9adef26ac514f102086e2a37de797e220a6791ee87"}, + {file = "grpcio_tools-1.75.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b01b60b3de67be531a39fd869d7613fa8f178aff38c05e4d8bc2fc530fa58cb5"}, + {file = "grpcio_tools-1.75.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e2b9b9488735514777d44c1e4eda813122d2c87aad219f98d5d49b359a8eab"}, + {file = "grpcio_tools-1.75.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:55e60300e62b220fabe6f062fe69f143abaeff3335f79b22b56d86254f3c3c80"}, + {file = "grpcio_tools-1.75.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:49ce00fcc6facbbf52bf376e55b8e08810cecd03dab0b3a2986d73117c6f6ee4"}, + {file = "grpcio_tools-1.75.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71e95479aea868f8c8014d9dc4267f26ee75388a0d8a552e1648cfa0b53d24b4"}, + {file = "grpcio_tools-1.75.1-cp313-cp313-win32.whl", hash = "sha256:fff9d2297416eae8861e53154ccf70a19994e5935e6c8f58ebf431f81cbd8d12"}, + {file = "grpcio_tools-1.75.1-cp313-cp313-win_amd64.whl", hash = "sha256:1849ddd508143eb48791e81d42ddc924c554d1b4900e06775a927573a8d4267f"}, + {file = "grpcio_tools-1.75.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:f281b594489184b1f9a337cdfed1fc1ddb8428f41c4b4023de81527e90b38e1e"}, + {file = "grpcio_tools-1.75.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:becf8332f391abc62bf4eea488b63be063d76a7cf2ef00b2e36c617d9ee9216b"}, + {file = "grpcio_tools-1.75.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a08330f24e5cd7b39541882a95a8ba04ffb4df79e2984aa0cd01ed26dcdccf49"}, + {file = "grpcio_tools-1.75.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6bf3742bd8f102630072ed317d1496f31c454cd85ad19d37a68bd85bf9d5f8b9"}, + {file = "grpcio_tools-1.75.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f26028949474feb380460ce52d9d090d00023940c65236294a66c42ac5850e8b"}, + {file = "grpcio_tools-1.75.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1bd68fb98bf08f11b6c3210834a14eefe585bad959bdba38e78b4ae3b04ba5bd"}, + {file = "grpcio_tools-1.75.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f1496e21586193da62c3a73cd16f9c63c5b3efd68ff06dab96dbdfefa90d40bf"}, + {file = "grpcio_tools-1.75.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:14a78b1e36310cdb3516cdf9ee2726107875e0b247e2439d62fc8dc38cf793c1"}, + {file = "grpcio_tools-1.75.1-cp314-cp314-win32.whl", hash = "sha256:0e6f916daf222002fb98f9a6f22de0751959e7e76a24941985cc8e43cea77b50"}, + {file = "grpcio_tools-1.75.1-cp314-cp314-win_amd64.whl", hash = "sha256:878c3b362264588c45eba57ce088755f8b2b54893d41cc4a68cdeea62996da5c"}, + {file = "grpcio_tools-1.75.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:eca28a90020fc1596f48cf51b02e56bc3d285f7f9ebaf0493144160d69c2cae7"}, + {file = "grpcio_tools-1.75.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:6744a14983f82e04cfd799ed779d06ee92035bb497f2d0fa84e81921a6c9c985"}, + {file = "grpcio_tools-1.75.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2a59120f17d36de6e16a058d88f2fcd255bafaccb487fea0613860a5287f77c6"}, + {file = "grpcio_tools-1.75.1-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:02b0c237882e45247570afdc34717ce80831184882186ef47afca9f8cac2f71c"}, + {file = "grpcio_tools-1.75.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4999ada9721ce2a0eae66bf7f2793bc6fe7a473eef4e38bb542d1e5c6d9f7d91"}, + {file = "grpcio_tools-1.75.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd13f0d87605eb34f8b8868e3ad9202b90e9e58417276db79c3298538d0d60e3"}, + {file = "grpcio_tools-1.75.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9555db0d2eb22850b7e9a27c0476627d483c114fcdf40d29b03aef446f5e4c43"}, + {file = "grpcio_tools-1.75.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a35800ce3ecea4aaad511bc18daccd37b1560132694f30b606f2044f1242c9a0"}, + {file = "grpcio_tools-1.75.1-cp39-cp39-win32.whl", hash = "sha256:8e7f2c1a37a5c8db92c5cba4034c370598f7458b275606f7a2a114f8c25c0326"}, + {file = "grpcio_tools-1.75.1-cp39-cp39-win_amd64.whl", hash = "sha256:0de3a82ee33d960f117ab66da51254cccd8bda9118d11ec3379f954cfbf6bc39"}, + {file = "grpcio_tools-1.75.1.tar.gz", hash = "sha256:bb78960cf3d58941e1fec70cbdaccf255918beed13c34112a6915a6d8facebd1"}, +] + +[package.dependencies] +grpcio = ">=1.75.1" +protobuf = ">=6.31.1,<7.0.0" +setuptools = "*" + +[[package]] +name = "hightime" +version = "1.0.0" +description = "Hightime Python API" +optional = false +python-versions = "<4.0,>=3.9" +groups = ["main"] +files = [ + {file = "hightime-1.0.0-py3-none-any.whl", hash = "sha256:ba86d42976c36451b14e11c736e61f296f9f00dbb79c8488e18d70c6b2dbb395"}, + {file = "hightime-1.0.0.tar.gz", hash = "sha256:480d2a03e2c3ed44916d2406d40ab6d10a276ed7f101619fc3fcc1e00c46aacf"}, +] + +[[package]] +name = "isort" +version = "9.0.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.10.0" +groups = ["lint"] +files = [ + {file = "isort-9.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3e81cae981a52f94d5b31a474e1cbb033ea9cc850bc4c922117c0534a1864dd"}, + {file = "isort-9.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cd67d39c3501d7227e8b229476da1d8679c03e0af97bd295876cf7070e5b709"}, + {file = "isort-9.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:288a320e6d52ba2d3447345390c8a8400591e4033ffbe4ce6bc3e50e5b4818e1"}, + {file = "isort-9.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:67b12d9504e5bc6359bb3bb4493f36cf1093d15477c61c349f52f7d04209fb5d"}, + {file = "isort-9.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4a54aed1bb731d7cf80ef5dfbae5b960f777cea70523b751ee6049bcb604371"}, + {file = "isort-9.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b34a165cd4e25726930ed2eed8cf2fe46fb1a5ebacd9b28eaf566b343a6457ca"}, + {file = "isort-9.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5c929e8ec9d9fb83f034d5f50895503f40c624605f552b97ad090a37e62407ca"}, + {file = "isort-9.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:5f448510ef0a92fa626a975759d76bdbe3b721c3d615da6d1010cc451de5610d"}, + {file = "isort-9.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a05dc63cb6ae2a8e62ec4184153f424b1650593e00a24e6138184c46193891e9"}, + {file = "isort-9.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af8be0b5cac101202c8255360e5de832ebbb84b2e863dc0f65dbb1a3d63dd40a"}, + {file = "isort-9.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3fe693c1e56781de387a6c206306e9e5e560cfeb4acdfd85f0c46122afd48792"}, + {file = "isort-9.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:f7a9efeb3689c7327a0d637eb4e12691e8d5ab1297caee997b144dc595ccb93f"}, + {file = "isort-9.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f65ff614632ddc3306c40f619717b3b3ca69938ffee21d97110056d52472c79a"}, + {file = "isort-9.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e5f11c7ccd5f079ac0431fe52c7b38ea5d9f4e31a1889746de81dac0e7b0a766"}, + {file = "isort-9.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810561edf6f1f5f3600f02aa709603a4360d5290c5fff2ae4b370090dd1a5445"}, + {file = "isort-9.0.2-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:e2636222848a48cadbd712280058b5da19fa147c501132e04a486a5bddcc9e28"}, + {file = "isort-9.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:71870ac3b1afdf3c259b8404c05076d3ab874122fec6f78339f1c92d2c29b012"}, + {file = "isort-9.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bd8c4fb9829a5e7117d9f71f540ff1e8caafb471e574012057ce6dc35fda2d7b"}, + {file = "isort-9.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eb3e714d64de6eba78ee29051f7fc80613c74e90c6f54f84082f59c429c0a0b"}, + {file = "isort-9.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bf3ef0a91974f29f406e25eef0e04781fd5c2254b8ab55e7655b20d8cd7c5514"}, + {file = "isort-9.0.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:cd1e0e5e61497e95a4e5be269088e6a1013f530aeccf6ebd6134f403285ecd63"}, + {file = "isort-9.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:11da67a30f5a88383c71db075488ca3d081f427f53368f90bb1d74e958a9b040"}, + {file = "isort-9.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7c2fa33e1c9fbcf9fd639997e4550515c0b712b52ed70a059124a5247825480"}, + {file = "isort-9.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e859fd72e50c27306d05185f9472ed97fae9e1cce91c0e891260d16f2ecece"}, + {file = "isort-9.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2a960e4252ac5b00f78adc0f731529e122657ee642e650896b36e1ff83028023"}, + {file = "isort-9.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:91b60ce3d96fcb0730d61fc5ab84ee5b56d676fbb92550f7ea333f58778f2f20"}, + {file = "isort-9.0.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:aa810daf72ff5d8ade462b2190dad9c0e16d6d428a3f9aea210f14cca2487d58"}, + {file = "isort-9.0.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8dde4e2d9cfb35390437353f0861ec41378f91ff958d8cd3051fb95cae59315a"}, + {file = "isort-9.0.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:4315e23e701bb1fcdfd364da59da61d78c3332c554318b7eb635ea3924d24c5e"}, + {file = "isort-9.0.2-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:1c134ef9d94943eae14bf31c634db1904dd875e6e7280a60baee10ca06132db6"}, + {file = "isort-9.0.2-cp315-cp315-win_amd64.whl", hash = "sha256:d4da51a99dfd00e5c51e507ed91ebad6aafd44dc65135c17e2ef37355cd9fa98"}, + {file = "isort-9.0.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:16436aefeebe3aa2d5d7ae1ca895b2278f770fc4a41d95c22569a30f7413ec45"}, + {file = "isort-9.0.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a36f30b6b85d9726f79c7623d35f3e966d5d7d9d0a005af91ba19988fccd038b"}, + {file = "isort-9.0.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:d03c68e9d0a83b51ed381d04b0919f2d918fb66c1ca1766761157ff44149366f"}, + {file = "isort-9.0.2-cp315-cp315t-win_amd64.whl", hash = "sha256:29669ea6c410528ffe3b632a41835757f08282257e4ddac892a5e6d01bd35201"}, + {file = "isort-9.0.2-py3-none-any.whl", hash = "sha256:6c29deeb39698a8717823b7f75b2ac58c5e8ab8dcf6cf31205a72a6617fb454e"}, + {file = "isort-9.0.2.tar.gz", hash = "sha256:d2298980ce44350f11d9d24c8150eaef1883431ec203dddbb4e9b5c3ceb54c70"}, +] + +[package.dependencies] +mypy-extensions = ">=1.1.0" + +[package.extras] +colors = ["colorama"] + +[[package]] +name = "librt" +version = "0.16.0" +description = "Mypyc runtime library" +optional = false +python-versions = ">=3.9" +groups = ["lint"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:96f576f2711f8519152ec76d0e599243555c1f07679fa73606ca8c8c868c0be6"}, + {file = "librt-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bec3818c7da7c96ceae0ef5915a3d16c52dd08f3ea913bf1fe8568c447c7978"}, + {file = "librt-0.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c79520414a3fdfc6aabd7593e6169afa14d5f8d9908d4b498db068868b08dd"}, + {file = "librt-0.16.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f81b5b19ce748ef68d4746656b7929762eb2fe99269b266e4be07e2ee4de7144"}, + {file = "librt-0.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c71d1b76210a36729fedfc5115069b50a3d8619054745f8758fe5d6f19e86671"}, + {file = "librt-0.16.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:473eebc7866bb0a0c8849a292b5e7157c1aba5d14d0f0f610c52158d6d964262"}, + {file = "librt-0.16.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4eb1313a19847089ee81e88742abedf285c60538816640b99742d8534b81d26a"}, + {file = "librt-0.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ebefd60b42e2a82b32d136bb5f7c94eadfcd29f772b547df6f3291d1ed855a1c"}, + {file = "librt-0.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c5da27e8056439f927ea896735da60e616c477a8293feaa3233d4e7781a6726"}, + {file = "librt-0.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:67e718c7a43f8db325abbbf1404e2d535f12f8f7a1a82259568385cc5274b82a"}, + {file = "librt-0.16.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:314e703f0c19320dc8094e7a784b9cf29e1b67402515abf580069a6363c0b4f1"}, + {file = "librt-0.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:943c6bbecbdf7fa575a4f2952fcfd848c88ef95507c3fca411e89d4ac3ff8143"}, + {file = "librt-0.16.0-cp310-cp310-win32.whl", hash = "sha256:7cc365f006891afb006b52d5ee5ee74c09306ffa20e2f8705a32a4450af2f3ba"}, + {file = "librt-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:0314058469f4d2fd279ce7c62ac274ac82c3918ef7db62ef0697c4c359370155"}, + {file = "librt-0.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fe4372c52d4849096c6cc1cda2817d293ec51440c890474ed59ef38d46556f18"}, + {file = "librt-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c72c5295a84bd249526da9bdca38f2e176d15c31c13bb0063c5053f4ca023421"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be56ba9c884143495b517f23fe794ae367d58cd89ea0fdd6d437e3c024a87f9f"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ef46c1a29ffb8c72e882e22618ec618778eacd0578fb22c6e7cf9c11d15f357b"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d3c94211ee0c4f8d649ec06b7c115c0ec4eadb873a0e3154ca15cef3f814b071"}, + {file = "librt-0.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94aed6a8308818b91677957d1bd03188869cd7aeb23c5dba7912a6c0402f7602"}, + {file = "librt-0.16.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:349c0bcb87ebd07481b6ff781e25cdc699723dbe2212e57dabb27f7a13b7b87d"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:001bfd59a7d45b17e3e75f2a8c6405280b35e7b84471792778e718c4f368950e"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28e038895b998d7a0c7798922ce8a1dc157675df5cf1c9ef0aca809ed804b7a1"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0dbe4096a7ecc00fa835d24510ad8545a4efef738dac96e0e63516783ccde905"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5cd5b092441053364af968ea12084692cb9d4a22f3ce9524e377880bf028761e"}, + {file = "librt-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ddeb3c9dedb461bb457c6c7d9aa7fbf35329da313d1a7543d00c8d0f3473c96"}, + {file = "librt-0.16.0-cp311-cp311-win32.whl", hash = "sha256:e05108e0849966f53a8d2d3112a7af881d0efaa479bc735bba91108f9f2350a7"}, + {file = "librt-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5f49cff01bd608ef7d97104cb035c75455e79c2d70bf4a506cf773338ac1860d"}, + {file = "librt-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d28ae980ae2218f9c5b95d191e947296f918c9bf0b400d467a9430275bbe678"}, + {file = "librt-0.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe52bf4641069e7978a14253b036cb9002def1926317e710f2e249f8a8c47742"}, + {file = "librt-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcc2c4726ced915b00de0c9856a4eeabfb3fddb93e10e0b8f735b7709358b6d"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff7baa55f8e7c69851419e50a666015d02a74198716fd45c0125a2112e0a389f"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b95d5d92ab83d39e760a52091bb1baba664f3a2351e39b1e16801e5747c2f0e9"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6d085d70bce51d43c5c7c36d63490770180d8779e71c49305c87b4213918de7"}, + {file = "librt-0.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36e53948e99bbe3ffea257124cfcae1cfb01831555c9a9c903c9f9a72db7fd07"}, + {file = "librt-0.16.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:54d11f726aae9df5a6ffbbf0a03a52449bbac84a53ef03669cb41cdfd4ae41bf"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4323193ac0cd025f85af531df8ba91bf24d1973b401697347a6282e8fd3fcf5e"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e42f8e098b9c5396fefa05fb1cc7e33b0e08fc51da106b5de4a45fd22aac6743"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:39ec1d5a14e37baf1450a6cabf03fe552340808bf1ad9d71824ab90117716459"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d1aabe3925cbb4a08d15b7b20ba4011b53019da0c4173a25155139b7b1baed65"}, + {file = "librt-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:300c3ffdc459f4a779a8411ecb188e3ac0b1ff3a3a7b099642555dedae06c69b"}, + {file = "librt-0.16.0-cp312-cp312-win32.whl", hash = "sha256:c17194318e4c0c0348b36f36c2ec7534436fe0a4c15582403162a4f08c80797a"}, + {file = "librt-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:25a58a19ea8d83b68209f04912df765e9260635ef77646542ed4b4abe6bc7940"}, + {file = "librt-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7be7cf555bc30ec12622e9447299cc4a9b8ff307548b634794353db0c2065dc"}, + {file = "librt-0.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c5e6144e68b577f157519f2ba88ca20e3ed61c29b00e5cdfa76cd2d45acf059a"}, + {file = "librt-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:33f41443a1f4e1f099331b3d8120e409fbff84b9760bc1cc9ea496f37ddaa5cc"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e510b7770bee609617a3374a96548eb114cae048023e3f049ee449e7ff2db32"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:efc49c462d4516b8a58b00b490078fa64689fd1fe66970cc190131d7afb8027e"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:92caf82ebef5e12d21c72242b70d1e92536f1711cf2a727a4c276de4b4469087"}, + {file = "librt-0.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17bac7f7a16b328fff77e440287693eb017abde913595b5827ebccbc21ecd8a6"}, + {file = "librt-0.16.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b976054553670829985ed767feb78fb6bcede0175327c4844dd5c281c1be659"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0058f9d68721094105917254c72ac0569117bb7b13b9769cf45d26d89f9d21cd"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b7beaf3f4487b7d8adef1f158b49067cb4d5a19fa7a3bf31a4e7a820e435c5"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:468df902df016a06eb0e40b0747dc8d14e47d7a38b18b63b1fb167d85cb94d63"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:aea7b1f2b125dad5de85f049136651bff256c883c65e6b9209b2da0a1ac3cdef"}, + {file = "librt-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a8afb6557920860b7a3a596eb804cf37e09e7cf8a803db2478c202acc72d8c2e"}, + {file = "librt-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:77c7a2b4fe2c1369e0d5aa1cade26740a7b14be32fbc9a5535d617d20065c39d"}, + {file = "librt-0.16.0-cp313-cp313-win32.whl", hash = "sha256:02d89c813d5ff74b17df72d3a34819d132cd168e56b81bf755b809bd9e46b8c4"}, + {file = "librt-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:14ed6ebe3e4f85f326d7920011ad30ff49ed9334e62cf88caef9ba973d9e3a92"}, + {file = "librt-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:83d4041a3d9b2fd053a8a4e1f22878b3e5833e2712956382d5c048d791454e91"}, + {file = "librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1"}, + {file = "librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86"}, + {file = "librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667"}, + {file = "librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55"}, + {file = "librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152"}, + {file = "librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839"}, + {file = "librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7"}, + {file = "librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41"}, + {file = "librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99"}, + {file = "librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc"}, + {file = "librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048"}, + {file = "librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302"}, + {file = "librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1"}, + {file = "librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7"}, + {file = "librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d"}, + {file = "librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b"}, + {file = "librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff"}, + {file = "librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154"}, + {file = "librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d"}, + {file = "librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121"}, + {file = "librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42"}, + {file = "librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a"}, + {file = "librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3"}, + {file = "librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971"}, + {file = "librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560"}, + {file = "librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b"}, + {file = "librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8"}, + {file = "librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd"}, + {file = "librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba"}, + {file = "librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9"}, + {file = "librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc"}, + {file = "librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866"}, + {file = "librt-0.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e1967e36ac4cae0c7e9615ad32e1a513cdacff79f9e8afb28bedc91caf48b4f3"}, + {file = "librt-0.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:20fe0bf9053885e21c62b3e091fb5e73e1c54d1daf2e70eb388d70763bcd4220"}, + {file = "librt-0.16.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a63610fa76524edfa605b5b259a603915c7a6e10e54f003e5503030506e81de"}, + {file = "librt-0.16.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:845a511b60ca43b9880dcc84a9784c891d6a2098c829130b320846c69c9c0c68"}, + {file = "librt-0.16.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d608f0bf3b8cbddd0067fe02cb8cab7d13e8eb9386b23a1843d4363044fd9e22"}, + {file = "librt-0.16.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df183721229ae51eef90108c115b43e98cb169b6155d34480338e5fc6616df00"}, + {file = "librt-0.16.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b6183e2e2e0ee00aac2ec07c7f7d151c97e85666b304f574b71cff0f9fccc4e"}, + {file = "librt-0.16.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:273d00be33792a15189331df10f1f1331621b043881e66c6c4377f7f776e1291"}, + {file = "librt-0.16.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aa9357a1b4d4fc787bb718a59cb1112c28c8a976d6bfa268b71cc0a4ab8f3a94"}, + {file = "librt-0.16.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:e9ce0bc440e7fd09b5f51f372f0f6640658f854b8fb05260f819cc669c93c42d"}, + {file = "librt-0.16.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:6c8893eae2fd13c5488d94056f3e6e5cf3142bfb1c4acaf136cb33d760c5964b"}, + {file = "librt-0.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7393c9a48dcce4817dbd4b0d8ff6237efe9b0a0609f5b0adaef315f8541726b5"}, + {file = "librt-0.16.0-cp39-cp39-win32.whl", hash = "sha256:71b93b42784e25b975079573c642a8fedb049a7bb31d70a51721b1666b3b2ced"}, + {file = "librt-0.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:5750a105b42a416f930edc59054927a406effb2550cd5bab92ad7a5842ed5d05"}, + {file = "librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["lint"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy" +version = "2.4.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.10" +groups = ["lint"] +files = [ + {file = "mypy-2.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffda5244fd1ad71a1e54405e35f50d09b80c3978efa120f58bd1252ae32c62d2"}, + {file = "mypy-2.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:615b03922d40e186fd1df73156473db0ba525c639bb4fd1cf28e1694879c9b23"}, + {file = "mypy-2.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13fa24f439c0e48a290a3922fa14ccd22f2762bae99d2142931b3e40a9055080"}, + {file = "mypy-2.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3adef556a19eb3b630bf86a79c29d7da3d61e541472d0d897ca01b171c0abf8f"}, + {file = "mypy-2.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:1dc0f64b0a92ae27a49d2175f0bacfa56e15bdc5b420cf92e0c1f79292219cbb"}, + {file = "mypy-2.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a378fc15fb33e321f04652c166ce73eeb8833a97c3d218132844e938cd93220"}, + {file = "mypy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d20e6c7c35fcbf2a0ebdd0eaeacfbc243009dfd33ab7822d54e213912e6dbbd"}, + {file = "mypy-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3011537be6cf1de4511c0255a324362a812b58184bbe61e15f59c8b31033bd74"}, + {file = "mypy-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a96b07a49b7b1d025ce59c1b3acbcf24bead9a83da4523c4a6bde1bb94e7a0e1"}, + {file = "mypy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7c4f8f8d1d1c0e2832d8ee7113dd08f6df6c7aad9e863fcbed9f25832be0b8c4"}, + {file = "mypy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba05652540bf12828e52abae807b024b09ca144ff4f75e2450a81d69c376425b"}, + {file = "mypy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:6306086b87cf7f8a29aa618d9fd9bffb56c59247166b9660fdb54d86d7714ecd"}, + {file = "mypy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e05ff2925d8b37ad26c80c1b9dc43ae5d455da2df1e23c24c095a6425917c57e"}, + {file = "mypy-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29243242cf72582b65f9582ad9e56e8cb281566ed3519f4cd70bb8b9f2977e90"}, + {file = "mypy-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29eb0b9427a6b11b992e452f6cceb8af724f4dceb47e779d0b35e405e996ea5e"}, + {file = "mypy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e3ebe2f72a2a1156065a9851570ffbf50c0a93cdccadef9c6e05c508a4fd10b1"}, + {file = "mypy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:236e0d68f6941992b0811128e652590f590db444ab29ad8f1324765b9298b946"}, + {file = "mypy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:82d0f94c8587ccb472622ee7795280aaa38a06640d5f45b3f16909d6dd86a989"}, + {file = "mypy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7da85fbcff6dac1abcc636707bed38b45598131fb7a605d9719c70b5cc733af8"}, + {file = "mypy-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4209da39d85cf240f762af622d8180fcdfcb4727d021f44ade62d613a1a43324"}, + {file = "mypy-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be721bd4bd57576193653b75b4af3461c9d0bf7dd8b528f782e9be210dc75bb"}, + {file = "mypy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1fde197ae65be856a034a91b70ed747a16562ca69577785f06c661548424bf1"}, + {file = "mypy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:295ecf2e57542cd836ca537486951289678c8c7d1ee6ad74ebe29b2168a003cf"}, + {file = "mypy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc378bdad4e9f12b5bd96466083d1e71acf00594ec9c7b2bdb5e02816f77f303"}, + {file = "mypy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:058165f564ccf559c68c70fec2091fca5891110480210c22594635e3f6683437"}, + {file = "mypy-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fa247e02b505a45a2775f69df38d360d197e3790bc60f717595db9eda358b6e"}, + {file = "mypy-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20e9a5cd875837520c43db98dea0b6d0c2197833d95c30127d8f570fb9b1f00b"}, + {file = "mypy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f03a7828cca2b0adcd6662aee8f2711ff8830e1027641fdea3ab0b787483566"}, + {file = "mypy-2.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9279488933040b638c0ab739084c0ca100efeea6db581bf5d7628d8e89de53fe"}, + {file = "mypy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:2106b55105ba5ea9be4f53a24517fc5fa927ff1585edc9bc1a975abb72caef89"}, + {file = "mypy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:528c8744b8b5e3ecb8774f86af38d2376216816e9908317ad055f3c9c2d74799"}, + {file = "mypy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0bb95cf34899e4619c61ab0a8667804e139e580b30d5df12af2102dfe44d0c97"}, + {file = "mypy-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d616fe84c6eab8026f8c50ab5bcb90db780d2ccd233d971e34e92bede9b359"}, + {file = "mypy-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3f86fd1313dd69d013e265f1fdcd12ea7a9d606f9875b2a3db946cd334555f3"}, + {file = "mypy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:720434d48542ecfe84d32d287b727569d3fc8f5769acd39051130e490a5c295c"}, + {file = "mypy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6e851b82c0661f69f1630fc16172c68787a6a9cf0991e7c6437d60976cdcd76"}, + {file = "mypy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3bd0e340f0ebe65c548210f53be3fd8192e83964760caf0c28bef368e68b0d37"}, + {file = "mypy-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:afa89837d9be67e0cadfa33bca3bb7efdda98c3b07e74dc3b635ebfb1c8a926a"}, + {file = "mypy-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb443e81057896132d3642d6be219e6efd158691ac7883e3ba8fcb469865f05d"}, + {file = "mypy-2.4.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f38f57d344f8b6accb40e01c3d83cfc590498231724d16c07ffb7940f157818"}, + {file = "mypy-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e76172710bd4e5eeae061abfd68347e5264632e02778be61784671ae3a2132f5"}, + {file = "mypy-2.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f83353e47ab520bf6fd4df8f5897d9fe081211f2fbc4b7d37736a3e3c166cbcf"}, + {file = "mypy-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:970b221ed5842213d98e3c480c08f795ace4b1f81fb21e1b126bd0476bce1c34"}, + {file = "mypy-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:502b94b0b331f7dafe32fd6b151797ddbb4f32385b362e722c783a025e5954a3"}, + {file = "mypy-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c9de622fd397495695d0598ddc789222bfcfec9d7c9ec3a1e385c855e3bc5e01"}, + {file = "mypy-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f459f0b4f0596d9d51fe7716b404b35287b99e77da98a7af90a65dd5fd61141"}, + {file = "mypy-2.4.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9b028548b3af480e2b1ed8df14ccaac86f99c9f600d1580770ab7ba3dcd40f0"}, + {file = "mypy-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:cb734b2668c1f40d07ce093bbeb4407e9527c67901627b0e1679825be3f09975"}, + {file = "mypy-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:172e30b8fea631fe310f0c665477f52d9ea40bb4e99e0c81dc30118563b13710"}, + {file = "mypy-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:5786ef987b3767e51aaa53f20aec104c0252b42ecda7aef8e8b4cbae279b05c5"}, + {file = "mypy-2.4.0-py3-none-any.whl", hash = "sha256:d01c5d26a352acc6d5cf3128225477e1e8465e8d3029d4c345807fbf7f3cf093"}, + {file = "mypy-2.4.0.tar.gz", hash = "sha256:77bdaebd452f43fcfc4cc3ba94352a3ea537cd01e3f2d0879f48673d2ec00d6e"}, +] + +[package.dependencies] +ast-serialize = ">=0.11.2,<1.0.0" +librt = {version = ">=0.16.0", markers = "platform_python_implementation != \"PyPy\""} +mypy_extensions = ">=1.0.0" +pathspec = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = [ + {version = ">=4.6.0", markers = "python_version < \"3.15\""}, + {version = ">=4.14.0", markers = "python_version >= \"3.15\""}, +] + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["lint"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "ni-datamonikers-v1-proto" +version = "2.0.0" +description = "Protobuf data types and service stub for NI data moniker gRPC APIs" +optional = false +python-versions = "<4.0,>=3.10" +groups = ["main"] +files = [ + {file = "ni_datamonikers_v1_proto-2.0.0-py3-none-any.whl", hash = "sha256:00775528187c2faf0938559be3dada2650dc4a085c7a7978589a09455d7edf47"}, + {file = "ni_datamonikers_v1_proto-2.0.0.tar.gz", hash = "sha256:994e2a40556b499c7ecdbf2b1b5b67468897a574b068f121dc07388012de6d23"}, +] + +[package.dependencies] +protobuf = ">=4.21" + +[[package]] +name = "ni-datastore" +version = "3.0.0.dev0" +description = "APIs for publishing and retrieving data from NI Measurement Data Services" +optional = false +python-versions = "^3.10" +groups = ["main"] +files = [] +develop = true + +[package.dependencies] +hightime = ">=1.0.0" +ni-measurements-data-v1-client = ">=1.2.0.dev0" +ni-measurements-data-v2-client = {git = "https://github.com/ni/ni-apis-python.git", branch = "users/sudharshana/add-v2-proto-and-client-package", subdirectory = "packages/ni.measurements.data.v2.client"} +ni-measurements-data-v2-proto = {git = "https://github.com/ni/ni-apis-python.git", branch = "users/sudharshana/add-v2-proto-and-client-package", subdirectory = "packages/ni.measurements.data.v2.proto"} +ni-measurements-metadata-v1-client = ">=1.0.0" +ni-protobuf-types = ">=1.3.0.dev0" +protobuf = ">=4.21" + +[package.source] +type = "directory" +url = "../.." + +[[package]] +name = "ni-grpc-extensions" +version = "1.1.0" +description = "gRPC Extensions" +optional = false +python-versions = "<4.0,>=3.10" +groups = ["main"] +files = [ + {file = "ni_grpc_extensions-1.1.0-py3-none-any.whl", hash = "sha256:db0357acd244854f4acccf202c89fe6462b4283d264ed639f4e248e6cc86bc9b"}, + {file = "ni_grpc_extensions-1.1.0.tar.gz", hash = "sha256:028ea33e5c5234bc050bf5dc99f5b61611531de8f012293e9d4c6985b7b37afb"}, +] + +[package.dependencies] +grpcio = ">=1.49.0,<2.0" +traceloggingdynamic = {version = ">=1.0", markers = "sys_platform == \"win32\""} + +[[package]] +name = "ni-measurementlink-discovery-v1-client" +version = "1.2.0" +description = "gRPC Client for NI Discovery Service" +optional = false +python-versions = "<4.0,>=3.10" +groups = ["main"] +files = [ + {file = "ni_measurementlink_discovery_v1_client-1.2.0-py3-none-any.whl", hash = "sha256:652f5bd55d0fac6f0d9fbb6207cc7ef6505a3b8746e9fe2374b5d733c1690ad4"}, + {file = "ni_measurementlink_discovery_v1_client-1.2.0.tar.gz", hash = "sha256:4de77ca1bebb19ee4906ab3c102401e63f432d668b32341807d3c05a20855139"}, +] + +[package.dependencies] +grpcio = ">=1.49.0,<2.0" +ni-grpc-extensions = ">=1.1.0" +ni-measurementlink-discovery-v1-proto = ">=1.2.0" +pywin32 = {version = ">=303", markers = "sys_platform == \"win32\""} + +[[package]] +name = "ni-measurementlink-discovery-v1-proto" +version = "1.2.0" +description = "Protobuf data types for NI discovery gRPC APIs" +optional = false +python-versions = "<4.0,>=3.10" +groups = ["main"] +files = [ + {file = "ni_measurementlink_discovery_v1_proto-1.2.0-py3-none-any.whl", hash = "sha256:06ceb4c94687b18469780739e0600f46695c73de816fef5fe1b4a3588fbe1e8f"}, + {file = "ni_measurementlink_discovery_v1_proto-1.2.0.tar.gz", hash = "sha256:5b767048c60d5dea6d66b6c1e8c1bad7b2d7a8207e88d2d81444e96dfbaa8f36"}, +] + +[package.dependencies] +protobuf = ">=4.21" + +[[package]] +name = "ni-measurements-data-v1-client" +version = "1.3.0" +description = "gRPC Client for NI Data Store Service" +optional = false +python-versions = "<4.0,>=3.10" +groups = ["main"] +files = [ + {file = "ni_measurements_data_v1_client-1.3.0-py3-none-any.whl", hash = "sha256:bf9547d6c9deb5fc88ac10cd32c34510a724266087e2d67181f03f2d26b77e41"}, + {file = "ni_measurements_data_v1_client-1.3.0.tar.gz", hash = "sha256:8110349ca982c1f12deede0352ad237d84c135f85be4712c41ee682fb4f47ada"}, +] + +[package.dependencies] +ni-measurementlink-discovery-v1-client = ">=1.1.0" +ni-measurements-data-v1-proto = ">=1.3.0" + +[[package]] +name = "ni-measurements-data-v1-proto" +version = "1.3.0" +description = "Protobuf data types and service stubs for NI data store gRPC APIs" +optional = false +python-versions = "<4.0,>=3.10" +groups = ["main"] +files = [ + {file = "ni_measurements_data_v1_proto-1.3.0-py3-none-any.whl", hash = "sha256:494f2c29b88861ef2f789431093a238f960d54af80300a9d567f46f39ec5da46"}, + {file = "ni_measurements_data_v1_proto-1.3.0.tar.gz", hash = "sha256:4547620163f8a054aa360e16afde1807f37f1f1faff623e3f24fdaeaa6bca919"}, +] + +[package.dependencies] +ni-datamonikers-v1-proto = ">=1.0.0" +ni-measurements-metadata-v1-proto = ">=1.0.0" +ni-protobuf-types = ">=1.3.0" +protobuf = ">=4.21" + +[[package]] +name = "ni-measurements-data-v2-client" +version = "1.0.0.dev0" +description = "gRPC Client for NI Data Store Service V2" +optional = false +python-versions = ">=3.10,<4.0" +groups = ["main"] +files = [] +develop = false + +[package.dependencies] +grpcio = ">=1.49.1,<2.0" +ni-grpc-extensions = ">=1.1.0" +ni-measurementlink-discovery-v1-client = ">=1.1.0" +ni-measurements-data-v2-proto = ">=1.0.0.dev0" + +[package.source] +type = "git" +url = "https://github.com/ni/ni-apis-python.git" +reference = "users/sudharshana/add-v2-proto-and-client-package" +resolved_reference = "28aee05fe1ce46d2d349311cd600bf0a47937d1d" +subdirectory = "packages/ni.measurements.data.v2.client" + +[[package]] +name = "ni-measurements-data-v2-proto" +version = "1.0.0.dev0" +description = "Protobuf data types and service stubs for NI data store V2 gRPC APIs" +optional = false +python-versions = ">=3.10,<4.0" +groups = ["main"] +files = [] +develop = false + +[package.dependencies] +grpcio = ">=1.49.1" +ni-datamonikers-v1-proto = ">=1.0.0" +ni-measurements-metadata-v1-proto = ">=1.0.0" +ni-protobuf-types = ">=1.3.0" +protobuf = ">=4.21" + +[package.source] +type = "git" +url = "https://github.com/ni/ni-apis-python.git" +reference = "users/sudharshana/add-v2-proto-and-client-package" +resolved_reference = "28aee05fe1ce46d2d349311cd600bf0a47937d1d" +subdirectory = "packages/ni.measurements.data.v2.proto" + +[[package]] +name = "ni-measurements-metadata-v1-client" +version = "1.0.0" +description = "gRPC Client for NI Metadata Store Service" +optional = false +python-versions = "<4.0,>=3.10" +groups = ["main"] +files = [ + {file = "ni_measurements_metadata_v1_client-1.0.0-py3-none-any.whl", hash = "sha256:c697ce4e98105b810f4da844a598e86e359ff6c90ca7a832e1e23a70327551a7"}, + {file = "ni_measurements_metadata_v1_client-1.0.0.tar.gz", hash = "sha256:9f9a10810c4c6693239081abbc026414a18df3e3d04daa3cd59010b1eed07f51"}, +] + +[package.dependencies] +ni-measurementlink-discovery-v1-client = ">=1.1.0" +ni-measurements-metadata-v1-proto = ">=1.0.0" + +[[package]] +name = "ni-measurements-metadata-v1-proto" +version = "1.0.0" +description = "Protobuf data types and service stub for NI metadata store gRPC APIs" +optional = false +python-versions = "<4.0,>=3.10" +groups = ["main"] +files = [ + {file = "ni_measurements_metadata_v1_proto-1.0.0-py3-none-any.whl", hash = "sha256:8844806d6775ac65144100fcd03099aea0c17ed55de696683d0663166f45cee3"}, + {file = "ni_measurements_metadata_v1_proto-1.0.0.tar.gz", hash = "sha256:3283cf7f0c452812a311b2b9274b5ba1e549f994f6a978a125f1746b85746e6f"}, +] + +[package.dependencies] +protobuf = ">=4.21" + +[[package]] +name = "ni-protobuf-types" +version = "1.3.0" +description = "Protobuf data types for NI gRPC APIs" +optional = false +python-versions = "<4.0,>=3.10" +groups = ["main"] +files = [ + {file = "ni_protobuf_types-1.3.0-py3-none-any.whl", hash = "sha256:f4c8e1e6c2f229d1d6e2dd45a21d40c61fd576e50fc4a0276a391296e1e9f478"}, + {file = "ni_protobuf_types-1.3.0.tar.gz", hash = "sha256:448c850934f57ee83862cc93fbf33c4b45b9f54d760279874cbfe8879eed412e"}, +] + +[package.dependencies] +nitypes = ">=1.1.0" +protobuf = ">=4.21" + +[[package]] +name = "ni-python-styleguide" +version = "0.5.2" +description = "NI's internal and external Python linter rules and plugins" +optional = false +python-versions = "<4.0,>=3.9" +groups = ["lint"] +files = [ + {file = "ni_python_styleguide-0.5.2-py3-none-any.whl", hash = "sha256:1d657dee729d8fd019b853bcd78478473fbffd20f9c23589146f2f9f148e1a2b"}, + {file = "ni_python_styleguide-0.5.2.tar.gz", hash = "sha256:d6bc6a190ebff283ef9a184bcef96a6849611d1556890d1423512c8c2d296a52"}, +] + +[package.dependencies] +better-diff = ">=0.1.3,<0.2.0" +black = ">=23.1,<26.0" +click = ">=7.1.2" +flake8 = [ + {version = ">=6.1,<7.0", markers = "python_version >= \"3.12\" and python_version < \"4.0\""}, + {version = ">=5.0,<6.0", markers = "python_version >= \"3.8\" and python_version < \"3.12\""}, +] +flake8-black = ">=0.2.1" +flake8-docstrings = ">=1.5.0" +flake8-import-order = ">=0.19.2,<0.20.0" +flake8-tidy-imports = ">=4.11.0" +isort = ">=5.10" +pathspec = ">=0.11.1" +pep8-naming = ">=0.11.1" +pycodestyle = [ + {version = ">=2.11,<3.0", markers = "python_version >= \"3.12\" and python_version < \"4.0\""}, + {version = ">=2.9,<3.0", markers = "python_version >= \"3.8\" and python_version < \"3.12\""}, +] +toml = ">=0.10.1" + +[[package]] +name = "nitypes" +version = "1.1.0" +description = "Data types for NI Python APIs" +optional = false +python-versions = "<4.0,>=3.9" +groups = ["main"] +files = [ + {file = "nitypes-1.1.0-py3-none-any.whl", hash = "sha256:b43ac7027e1cceeca7ceffa58f31ffadd03feeb1788ca5cb8f9d105e73893b14"}, + {file = "nitypes-1.1.0.tar.gz", hash = "sha256:f273b5131ef0d5b848c37a0a63452626caf5c2938f7744c324f7991b76fa0b60"}, +] + +[package.dependencies] +hightime = ">=0.2.2" +numpy = [ + {version = ">=2.1", markers = "python_version >= \"3.13\" and python_version < \"4.0\""}, + {version = ">=1.22", markers = "python_version >= \"3.9\" and python_version < \"3.13\""}, +] +typing-extensions = ">=4.13.2" + +[[package]] +name = "numpy" +version = "2.2.6" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "python_version < \"3.12\"" +files = [ + {file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"}, + {file = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"}, + {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163"}, + {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf"}, + {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83"}, + {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915"}, + {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680"}, + {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289"}, + {file = "numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d"}, + {file = "numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491"}, + {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a"}, + {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf"}, + {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1"}, + {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab"}, + {file = "numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47"}, + {file = "numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282"}, + {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87"}, + {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249"}, + {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49"}, + {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de"}, + {file = "numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4"}, + {file = "numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566"}, + {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f"}, + {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f"}, + {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868"}, + {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d"}, + {file = "numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd"}, + {file = "numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8"}, + {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f"}, + {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa"}, + {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571"}, + {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1"}, + {file = "numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff"}, + {file = "numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00"}, + {file = "numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd"}, +] + +[[package]] +name = "numpy" +version = "2.5.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.12" +groups = ["main"] +markers = "python_version >= \"3.12\"" +files = [ + {file = "numpy-2.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c6342f54c67093cae5c0227eb0eb772fdb79f2a2c37a6eb278b9909ee06aa356"}, + {file = "numpy-2.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b11e8fda06a7d69f15ebf542660b74466c2e51094800c1fb794f47ad4faeef17"}, + {file = "numpy-2.5.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9cb18a327b49c5c337f972b03682f6a49855525faaf3c0d3e9c96cd0fd8880a8"}, + {file = "numpy-2.5.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:aec3fc4b32ff82421274f5d205c559c51c840c8df66a78efd7f3612dd005a26a"}, + {file = "numpy-2.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe4d21ab149f15e4e6043dfb0de87e6e5f34ac176cde83060e9802981fca2ac2"}, + {file = "numpy-2.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbde6962867ee75b48b0ee29b2b9372ec5d617799dbaf38e82dc0596f2f7738a"}, + {file = "numpy-2.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:381a7a3d2e65e64c0ec302795ab9dc12bb1e73f150904699c153716177eebdaf"}, + {file = "numpy-2.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b89d0aaae2fe498c648f4c4795c084db535af5bd98ef942b2a3681fb74ce8645"}, + {file = "numpy-2.5.4-cp312-cp312-win32.whl", hash = "sha256:9968ab7e49b93ac6e1c3b2239732183152c9150f16308d30b66a372cffe3483c"}, + {file = "numpy-2.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7b1b6353e36a7e50de2973a38d705c88ee93adcf120673cee7f45a4a3fa223a"}, + {file = "numpy-2.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:aa1cce2ff3f8d953de38b76bf44602caeb69f101430208f64a10067f7cb4b1d3"}, + {file = "numpy-2.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2377da2dd3ba2c1200956acbab2a358c83b8e1f8531191672d1cd6ad83250d53"}, + {file = "numpy-2.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7415db95818b39ec475a5eea54d9e3b6bc83e3912158e46da3438cdce399804d"}, + {file = "numpy-2.5.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6d6a71b9d9a97c03633aa12565ef2825ffa036cc1d99cfd50dacf0f128af4fe2"}, + {file = "numpy-2.5.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d8200f16437b289a5bb927c6e184eccc3e8389bc0070fea4cd5b9e13c1757959"}, + {file = "numpy-2.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2e71b04c6cad90026e544501bbe0ab9290fa8a4d845e7e8c0d124fb429c988"}, + {file = "numpy-2.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ffa07666f8da0eef81d149934a626d0d95fbd6838432a33e66245423a9062c0"}, + {file = "numpy-2.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2fa3328f784fc8277fc48026f6cad516f5c561c5d8e2e39b3c9e0c8f23223b34"}, + {file = "numpy-2.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b86966fbe4ad7de710422175572bcdc75fdedadfb54bc6fab7deabccddd7780b"}, + {file = "numpy-2.5.4-cp313-cp313-win32.whl", hash = "sha256:5258bc06526964be5face2fc6f756857a3f24f21ec3e72ca131337a75b165d6c"}, + {file = "numpy-2.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:8b4d2fd2d34e5f8c9235ee787de5631a37a28402b15cb80814df973d2be54129"}, + {file = "numpy-2.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:bc39ac66a7a9a3fbd6134fda43136b60ffde99c8f4501e64e0d2b24da137babf"}, + {file = "numpy-2.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c668b2f0d651605b58892644b0e302c7157f7159544227758c896982ef384b18"}, + {file = "numpy-2.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ffa6ce09a1c6a08e9667dd9c97aa0b14184e8d18f2a14b78b2a2328c9147f076"}, + {file = "numpy-2.5.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:956555e0603a4d38019ae6925711cb9dc43195c076a928accf7ea5d50bddfe53"}, + {file = "numpy-2.5.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2c2c4afffdeb7920e445028dd71eb932cac3e704792e964bc2a232426d4f1255"}, + {file = "numpy-2.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4054173604cd8658796053f1f3bc0befb68ec1c0762c57fdad61e199256a8617"}, + {file = "numpy-2.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d549420b8858885cea8838a727842249218b9c1da24dd517e25c9c7a948310a3"}, + {file = "numpy-2.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:823874a507a84af050493b622affde94b6f7c3a0dc22cb2801381bc03b871c00"}, + {file = "numpy-2.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4e263278bfb5ee6409db8aedbc4cc32973b1b82bc1e8d3c668551d04d83a7e37"}, + {file = "numpy-2.5.4-cp314-cp314-win32.whl", hash = "sha256:cfd73180400042a7c532d30c5e287bdd03c59ff9ee1b4c0316af0539e29dfe23"}, + {file = "numpy-2.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:2ca144f15135b6212a5c47b1e2aeca6e412f102f95a2d5d88d8aec77eb255de3"}, + {file = "numpy-2.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:468397ba3c64427474706e5c9123fe266395496714dc684294eac75cd4930d1e"}, + {file = "numpy-2.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1ef3aa6d7e29bb13677323114280b05acc57607fa2300e66432d665d5418a162"}, + {file = "numpy-2.5.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:98b053943e5a0474ec0da309d2cb9d3f18ea57f8a2067c2ab7b5f763d1068380"}, + {file = "numpy-2.5.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:b64a85f40e154983960a4167d4c1d57a50c7f109b3d3264a3a984154e90a8454"}, + {file = "numpy-2.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a813ed7719bf45463c51779e6a98d0385fe905e48447526938a4b8337333d551"}, + {file = "numpy-2.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9b80cdf5cedba0e90d93fa5f9a333c4d65bd545cd669b71bb97ce2b703c9d73"}, + {file = "numpy-2.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2199ed071f460487c8db2c0e5c0b564494190edb4772fe80f9aad88b2604def5"}, + {file = "numpy-2.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:64f9c9878c1938476365e11ccfb6b770f3b9e5f045ccddc514235041e6959365"}, + {file = "numpy-2.5.4-cp314-cp314t-win32.whl", hash = "sha256:64d1c8ac28a4077cf987e0a71a7a0ef7e2df70722f07f0baa42dbb7eb6938647"}, + {file = "numpy-2.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:067374eb538c34c745436365cf7b0112595c1d326f21ce4ff340f61230239fbb"}, + {file = "numpy-2.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:e94aef2c639da4a960ad0db8e06471208d8589974953d78b61d345b4eb99e394"}, + {file = "numpy-2.5.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8dddfbee2e68d26d0d7d7d9cb247b1fd4409241cce32d815a11d97ec2cfde179"}, + {file = "numpy-2.5.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:81e3420b27048b65eb14c3acf0c174a8cb0e023277716110347d2dcb26026dad"}, + {file = "numpy-2.5.4-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:0b4724a19de67bea8cfc4970798efa78bcbbe2ac2613cfac16721a42d44de2a5"}, + {file = "numpy-2.5.4-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:2132418bf8dd124a427ca9e6a1daf9ee1a87185344c95119ceae868b99466da1"}, + {file = "numpy-2.5.4-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:325518d4245b9e331387702aa58c2ce1dc4cdcbb41dfb4ccd5dcbc7e08db1266"}, + {file = "numpy-2.5.4-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56733449d2544178beaa4545cee357370440cf056c197f9c7bfb19dbfdd0e86d"}, + {file = "numpy-2.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5ec3753760c1a6d8bb91200666e545c3a9728e6269dfb5d6ce02340996698aa3"}, + {file = "numpy-2.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b1185012870173de7ae33d370bd45b1cf5baee747ea4b97036b65f4e93016877"}, + {file = "numpy-2.5.4-cp315-cp315-win32.whl", hash = "sha256:298eca75243f2cbbfdb460560b9fb2a1792a33cf2ab4286efd43d92e8d3df508"}, + {file = "numpy-2.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:332f3378fe077dd850e677ec01bdcc4f22368fb5d50ef10b2c79230b1bf5a592"}, + {file = "numpy-2.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:d4cccbbc78717966f764cd3af4fb70276fa01fc7a2688af11c78901fa5c04f05"}, + {file = "numpy-2.5.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:950ea81d57ef070665581b6e1b5f6a029306423cd1739c5b95fe78aa30db6b9d"}, + {file = "numpy-2.5.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c05ede731b03fb1b7591faca9389ade3267d2bddf1ad8882bb3f2cc5e101694f"}, + {file = "numpy-2.5.4-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:5fbf7141bbfd63aea22f435c9062a032b9ea0082fe9845dad7f021d3f1234e71"}, + {file = "numpy-2.5.4-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:3573cd22564692a5b899ec344e5d5b9cc4576f2985b96f22af3564ed54f2710f"}, + {file = "numpy-2.5.4-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c109eac9cd439193678f69d70733c1108487546ca8eafc107b510ae10c1aecd"}, + {file = "numpy-2.5.4-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80d6ef6e8620eb2c2b4c4caad50b5935d6db3cde2d51581b55dcc79e14016d1d"}, + {file = "numpy-2.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:77045a4b175bbf5316ec08003880804336c78f92281a1b72222b274ea85ec5ac"}, + {file = "numpy-2.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:0f02a46e49cfb6c73bdb7aea1c0d3461dbae9aba613542b65f657cd3d17b9fab"}, + {file = "numpy-2.5.4-cp315-cp315t-win32.whl", hash = "sha256:ad62a416ddcf863bf44bba76fbf6b53366ab0692e294f51cae4b5fbe0d246788"}, + {file = "numpy-2.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:38f47be9f74ab870d2633b5456ae519c43758a8d1fd05342f0ce4ecc034396ee"}, + {file = "numpy-2.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:7a14a461d9340f1b46b8648578aed9cdb8b3b018a8fac6c1dde2c9192a01a87f"}, + {file = "numpy-2.5.4.tar.gz", hash = "sha256:9a94cf751c9ad8ebaa835bcd3d40dacf8534ad086b88c38029b65123c7999d2a"}, +] + +[[package]] +name = "packaging" +version = "26.3" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.9" +groups = ["lint"] +files = [ + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.9" +groups = ["lint"] +files = [ + {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"}, + {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"}, +] + +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] + +[[package]] +name = "pep8-naming" +version = "0.15.1" +description = "Check PEP-8 naming conventions, plugin for flake8" +optional = false +python-versions = ">=3.9" +groups = ["lint"] +files = [ + {file = "pep8_naming-0.15.1-py3-none-any.whl", hash = "sha256:eb63925e7fd9e028c7f7ee7b1e413ec03d1ee5de0e627012102ee0222c273c86"}, + {file = "pep8_naming-0.15.1.tar.gz", hash = "sha256:f6f4a499aba2deeda93c1f26ccc02f3da32b035c8b2db9696b730ef2c9639d29"}, +] + +[package.dependencies] +flake8 = ">=5.0.0" + +[[package]] +name = "platformdirs" +version = "4.12.4" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["lint"] +markers = "python_version < \"3.12\"" +files = [ + {file = "platformdirs-4.12.4-py3-none-any.whl", hash = "sha256:78bfb9db2a8471ed7eebe3c3c932da413911042994e699b384fbb4493fa872d7"}, + {file = "platformdirs-4.12.4.tar.gz", hash = "sha256:63743c02414e755de4e31b8f68125c1407495b86c5a006e203c01ff8b9924250"}, +] + +[[package]] +name = "platformdirs" +version = "4.13.5" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.11" +groups = ["lint"] +markers = "python_version >= \"3.12\"" +files = [ + {file = "platformdirs-4.13.5-py3-none-any.whl", hash = "sha256:53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e"}, + {file = "platformdirs-4.13.5.tar.gz", hash = "sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0"}, +] + +[[package]] +name = "protobuf" +version = "4.25.9" +description = "" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.12\"" +files = [ + {file = "protobuf-4.25.9-cp310-abi3-win32.whl", hash = "sha256:bde396f568b0b46fc8fbfe9f02facf25b6755b2578a3b8ac61e74b9d69499e03"}, + {file = "protobuf-4.25.9-cp310-abi3-win_amd64.whl", hash = "sha256:3683c05154252206f7cb2d371626514b3708199d9bcf683b503dabf3a2e38e06"}, + {file = "protobuf-4.25.9-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:9560813560e6ee72c11ca8873878bdb7ee003c96a57ebb013245fe84e2540904"}, + {file = "protobuf-4.25.9-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:999146ef02e7fa6a692477badd1528bcd7268df211852a3df2d834ba2b480791"}, + {file = "protobuf-4.25.9-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:438c636de8fb706a0de94a12a268ef1ae8f5ba5ae655a7671fcda5968ba3c9be"}, + {file = "protobuf-4.25.9-cp38-cp38-win32.whl", hash = "sha256:7f7c1abcea3fc215918fba67a2d2a80fbcccc0f84159610eb187e9bbe6f939ee"}, + {file = "protobuf-4.25.9-cp38-cp38-win_amd64.whl", hash = "sha256:79faf4e5a80b231d94dcf3a0a2917ccbacf0f586f12c9b9c91794b41b913a853"}, + {file = "protobuf-4.25.9-cp39-cp39-win32.whl", hash = "sha256:9481e80e8cffb1c492c68e7c4e6726f4ad02eebc4fa97ead7beebeaa3639511d"}, + {file = "protobuf-4.25.9-cp39-cp39-win_amd64.whl", hash = "sha256:b1d467352de666dc1b6d5740b6319d9c08cab7b21b452501e4ee5b0ac5156780"}, + {file = "protobuf-4.25.9-py3-none-any.whl", hash = "sha256:d49b615e7c935194ac161f0965699ac84df6112c378e05ec53da65d2e4cbb6d4"}, + {file = "protobuf-4.25.9.tar.gz", hash = "sha256:b0dc7e7c68de8b1ce831dacb12fb407e838edbb8b6cc0dc3a2a6b4cbf6de9cff"}, +] + +[[package]] +name = "protobuf" +version = "5.29.6" +description = "" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version == \"3.13\"" +files = [ + {file = "protobuf-5.29.6-cp310-abi3-win32.whl", hash = "sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1"}, + {file = "protobuf-5.29.6-cp310-abi3-win_amd64.whl", hash = "sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda"}, + {file = "protobuf-5.29.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269"}, + {file = "protobuf-5.29.6-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6"}, + {file = "protobuf-5.29.6-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9"}, + {file = "protobuf-5.29.6-cp38-cp38-win32.whl", hash = "sha256:36ade6ff88212e91aef4e687a971a11d7d24d6948a66751abc1b3238648f5d05"}, + {file = "protobuf-5.29.6-cp38-cp38-win_amd64.whl", hash = "sha256:831e2da16b6cc9d8f1654c041dd594eda43391affd3c03a91bea7f7f6da106d6"}, + {file = "protobuf-5.29.6-cp39-cp39-win32.whl", hash = "sha256:cb4c86de9cd8a7f3a256b9744220d87b847371c6b2f10bde87768918ef33ba49"}, + {file = "protobuf-5.29.6-cp39-cp39-win_amd64.whl", hash = "sha256:76e07e6567f8baf827137e8d5b8204b6c7b6488bbbff1bf0a72b383f77999c18"}, + {file = "protobuf-5.29.6-py3-none-any.whl", hash = "sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86"}, + {file = "protobuf-5.29.6.tar.gz", hash = "sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723"}, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +description = "" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.14\"" +files = [ + {file = "protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3"}, + {file = "protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326"}, + {file = "protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a"}, + {file = "protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2"}, + {file = "protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3"}, + {file = "protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593"}, + {file = "protobuf-6.33.6-cp39-cp39-win32.whl", hash = "sha256:bd56799fb262994b2c2faa1799693c95cc2e22c62f56fb43af311cae45d26f0e"}, + {file = "protobuf-6.33.6-cp39-cp39-win_amd64.whl", hash = "sha256:f443a394af5ed23672bc6c486be138628fbe5c651ccbc536873d7da23d1868cf"}, + {file = "protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901"}, + {file = "protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135"}, +] + +[[package]] +name = "pycodestyle" +version = "2.9.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.6" +groups = ["lint"] +markers = "python_version < \"3.12\"" +files = [ + {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, + {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, +] + +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +groups = ["lint"] +markers = "python_version >= \"3.12\"" +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + +[[package]] +name = "pydocstyle" +version = "6.3.0" +description = "Python docstring style checker" +optional = false +python-versions = ">=3.6" +groups = ["lint"] +files = [ + {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, + {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, +] + +[package.dependencies] +snowballstemmer = ">=2.2.0" + +[package.extras] +toml = ["tomli (>=1.2.3) ; python_version < \"3.11\""] + +[[package]] +name = "pyflakes" +version = "2.5.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.6" +groups = ["lint"] +markers = "python_version < \"3.12\"" +files = [ + {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, + {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, +] + +[[package]] +name = "pyflakes" +version = "3.1.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +groups = ["lint"] +markers = "python_version >= \"3.12\"" +files = [ + {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, + {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." +optional = false +python-versions = ">=3.8" +groups = ["lint"] +files = [ + {file = "pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c"}, + {file = "pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7"}, + {file = "pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2"}, + {file = "pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d"}, + {file = "pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16"}, + {file = "pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6"}, + {file = "pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1"}, + {file = "pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9"}, + {file = "pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68"}, + {file = "pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1"}, + {file = "pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4"}, + {file = "pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78"}, + {file = "pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d"}, + {file = "pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324"}, + {file = "pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9"}, + {file = "pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975"}, + {file = "pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a"}, + {file = "pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918"}, + {file = "pytokens-0.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1"}, + {file = "pytokens-0.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6"}, + {file = "pytokens-0.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037"}, + {file = "pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db"}, + {file = "pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1"}, + {file = "pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a"}, + {file = "pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de"}, + {file = "pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a"}, +] + +[package.extras] +dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] + +[[package]] +name = "pywin32" +version = "312" +description = "Python for Windows Extensions" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e"}, + {file = "pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db"}, + {file = "pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd"}, + {file = "pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c"}, + {file = "pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a"}, + {file = "pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47"}, + {file = "pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b"}, + {file = "pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc"}, + {file = "pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950"}, + {file = "pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c"}, + {file = "pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9"}, + {file = "pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831"}, + {file = "pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b"}, + {file = "pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e"}, + {file = "pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa"}, + {file = "pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed"}, + {file = "pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5"}, + {file = "pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9"}, + {file = "pywin32-312-cp39-cp39-win32.whl", hash = "sha256:d620900033cc7531e50727c3c8333091df5dd3ffe6d68cdca38c03f5821408d5"}, + {file = "pywin32-312-cp39-cp39-win_amd64.whl", hash = "sha256:dc90147579a905b8635e1b0ec6514967dcb07e6e0d9c42f1477feef14cac23bb"}, + {file = "pywin32-312-cp39-cp39-win_arm64.whl", hash = "sha256:02ebca0f0242b75292e218065004310d6a477407c09fa449bfe4f6022bc0c0fc"}, +] + +[[package]] +name = "setuptools" +version = "84.2.1" +description = "Most extensible Python build backend with support for C/C++ extension modules" +optional = false +python-versions = ">=3.10" +groups = ["main", "lint"] +files = [ + {file = "setuptools-84.2.1-py3-none-any.whl", hash = "sha256:ca140b45d5325a9478f880e351484f62fef15f336462e449ca0c03c873d589c2"}, + {file = "setuptools-84.2.1.tar.gz", hash = "sha256:3ff8b3b5492a0aa66960f4419365a86b84e013070d4313e4d4a6894f5d64d6bc"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=26.3)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=26.3)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] + +[[package]] +name = "snowballstemmer" +version = "3.1.1" +description = "This package provides 36 stemmers for 34 languages generated from Snowball algorithms." +optional = false +python-versions = ">=3.3" +groups = ["lint"] +files = [ + {file = "snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752"}, + {file = "snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260"}, +] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["lint"] +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomli" +version = "2.5.0" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["lint"] +markers = "python_version == \"3.10\"" +files = [ + {file = "tomli-2.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c4dc1c1781f2f716de763d1e9a7b34c6a894e167e291c7c5d16c72f7a9538545"}, + {file = "tomli-2.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eff8babca5a7999bc137acbc7482a8b7e17ffca5075ab41f5d770ab408c7bfef"}, + {file = "tomli-2.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86665cee9c4835b7a7f1e8ec2c719b5258d4dc782887aded5a8ae7352a96843b"}, + {file = "tomli-2.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e369fd63331746182360977b1892bfc215476a30d61612d732425311639f56"}, + {file = "tomli-2.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ad1ea345759240d6463efa0ed1c704402752e49aa21476620738d74d72d8aa1"}, + {file = "tomli-2.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96243987194634bd411066ce40c952e108f86af04db533ecd8ac3ff2a85b1885"}, + {file = "tomli-2.5.0-cp311-cp311-win32.whl", hash = "sha256:610b27d99f28ec5f191c7064a48f3ddb179a1fe6ca73d571483ae859f57b605e"}, + {file = "tomli-2.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:c804ae44fe7b4bab5da295e4f980a1ff04670bca9d23fe0a4e887e08ebd741a8"}, + {file = "tomli-2.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:cfac177ebd6236003846ea339981f71457cb6eb748f23381eb257e45092e3980"}, + {file = "tomli-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f4a40d03fb9f63424f0979855bdeaf44dd7696b8d59501822c10ed30ba532df"}, + {file = "tomli-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ebf8d19b17bd0daeb7b7dec81a946a439b753942fd0210d6e96c532249eea6b"}, + {file = "tomli-2.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf0b5e8e0f68ebb494356e577c06c139161efd8d3b9050f93b39b7c26cc54ff0"}, + {file = "tomli-2.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cf74416bdc94ae458b14e37286c1073081850ac8459a00d0c5efef5d44294c6"}, + {file = "tomli-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:61ea1ebe1e55a34ea8199cc8dbff398d35027b82271c8ac4802fd3a1fd5b1bcc"}, + {file = "tomli-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed53f7e89bb04f6d9e8e7799112360b0c4d5cbff067de0814c98c37c39b920f7"}, + {file = "tomli-2.5.0-cp312-cp312-win32.whl", hash = "sha256:e7ad033e27a516a233bea839cdb77b80146facb3b4f40bf02cd0cac165cdd5c2"}, + {file = "tomli-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd05de8c1698f8413dd7d869492693a0bf2211543b787ac78cd5e7536af1a6d7"}, + {file = "tomli-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:069435bd5480429b98c5e5afb02ab21c219b6f0064680671c6dc0d46817346ea"}, + {file = "tomli-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:943276cf269e0071948d9ff697159c1735e623c1151d88abb09b74659ef0cbea"}, + {file = "tomli-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:463b16086865b97facd8d0b3fb4cb7c544e3f58d2a69dc3113d6db9653fdb043"}, + {file = "tomli-2.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1245a6638fc4bb0a60af38a7d45413db34a13842027c77597c712c998c62fdf0"}, + {file = "tomli-2.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d8bac3d603c97e6854424e5b2b5b741bdbde387e09f162fb0446812b4a8362b"}, + {file = "tomli-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:21e4cae4114aba25aa0d4f85cdf486d290fb35c0954d7bba536248da64d43066"}, + {file = "tomli-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bbaefc84548d754be821bba7c4141c4787dda182f9e77f2f87b71213529efa7b"}, + {file = "tomli-2.5.0-cp313-cp313-win32.whl", hash = "sha256:abdbf6313b8d9efe157edeb7ab6eae4de064b1300ad31abf73755154b30abe68"}, + {file = "tomli-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd4dc129784e0c5335bd4e61dfcc4487499a013419e655cf2da1d091b7e0efdc"}, + {file = "tomli-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:69491c143d2fe063046e0301e62a810bed338fa4d1ce0fd870c27dc1e09b0d84"}, + {file = "tomli-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d3182ee2d887e507bd67319a0a61105d1dd33facc111329559a233b772c1a105"}, + {file = "tomli-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:521345fd1f19d45b8df87657aaa38b6f2ca3800059fadf428e7ebf479a383646"}, + {file = "tomli-2.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95c7614e705bfe2b04b27aa124adec59752d15813df37e2156747cab3a006b"}, + {file = "tomli-2.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac2027d37c3afbdf4bdd377f2676f6f1d2122a5be1f1137b49dced590b37e75"}, + {file = "tomli-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c414be4ed9d3cac80c42e348fa5a956117d1a48227f48026e31f59cb4a7671eb"}, + {file = "tomli-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9b03d7dc168353b4132965bde20feceabaa470e570c6f59660dfae59b1f9eeb3"}, + {file = "tomli-2.5.0-cp314-cp314-win32.whl", hash = "sha256:6f041843c4d3a37245c0c056fd955b186bf8b1fb85690cbe40b81230891dc34b"}, + {file = "tomli-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:f4b653094e18f9031102d3a1da5c729c8f222d85225b18037dac621695e46e1a"}, + {file = "tomli-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3f89d10c1ff6a38d992c27fc8a4816af71a909e08a40ec66934240b1e74347c3"}, + {file = "tomli-2.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e9e15b4a6c7dd6b85b5fbab29488a73f1f70de516942308daa266bf0e0aeb0d4"}, + {file = "tomli-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e12bbcd32897272fb05929110362ae9ff4c1b9bb26bd9e971e71dcd3275b4c3d"}, + {file = "tomli-2.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20aa36de8f2cf87237143bc1fa1aae8d6612c09118f4da21c6a684db5dd1f6f9"}, + {file = "tomli-2.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22185fad8a1e622f064e78008018a0dd3323550dcb479cb7a1d296888d74024f"}, + {file = "tomli-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:984012f71908165449a951de2050d52f276bfe3aa5d5f570f63ddad814370374"}, + {file = "tomli-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f79203b3965b4000e91808aaa7c040206093f2b8bf86f455982f2274c9ccf442"}, + {file = "tomli-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:91294a9fb94a75542f6e46e4a2ae709bd8d9b51134098cae5cf3bea5478b6d03"}, + {file = "tomli-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f15e3e0b835a6d68b10c86bf80a3149780498d6911c93c3ffd1861d19f9200f1"}, + {file = "tomli-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6664b7ae7af7294256c53960a6103077f4914cec8ff98479c352f622c6f6b2f0"}, + {file = "tomli-2.5.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc"}, + {file = "tomli-2.5.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276"}, + {file = "tomli-2.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52"}, + {file = "tomli-2.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7"}, + {file = "tomli-2.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391"}, + {file = "tomli-2.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859"}, + {file = "tomli-2.5.0-cp315-cp315-win32.whl", hash = "sha256:667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb"}, + {file = "tomli-2.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5"}, + {file = "tomli-2.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd"}, + {file = "tomli-2.5.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57"}, + {file = "tomli-2.5.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd"}, + {file = "tomli-2.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01"}, + {file = "tomli-2.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f"}, + {file = "tomli-2.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a"}, + {file = "tomli-2.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142"}, + {file = "tomli-2.5.0-cp315-cp315t-win32.whl", hash = "sha256:62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5"}, + {file = "tomli-2.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571"}, + {file = "tomli-2.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7"}, + {file = "tomli-2.5.0-py3-none-any.whl", hash = "sha256:32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b"}, + {file = "tomli-2.5.0.tar.gz", hash = "sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6"}, +] + +[[package]] +name = "traceloggingdynamic" +version = "1.0.1" +description = "Generates Event Tracing for Windows events using TraceLogging" +optional = false +python-versions = ">=3.6" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "traceloggingdynamic-1.0.1-py3-none-any.whl", hash = "sha256:0e19da491a8960725b3622366487ae35f49d8f595bb2e4e5ce1795eb5928db7c"}, + {file = "traceloggingdynamic-1.0.1.tar.gz", hash = "sha256:d9dd4b291dd04c15e34181eed06f73fdf4ffa7b1f895b78217163def48ab1a52"}, +] + +[[package]] +name = "types-grpcio" +version = "1.84.0.20261021" +description = "Typing stubs for grpcio" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "types_grpcio-1.84.0.20261021-py3-none-any.whl", hash = "sha256:0cd5f897d7485c448c4819597cb05fd17afac4620c0feaee6f7c5d0ee96a60f1"}, + {file = "types_grpcio-1.84.0.20261021.tar.gz", hash = "sha256:d06d24b31843adaf34656654a7536bd70b40425880bb6162219eb5b1a86220ae"}, +] + +[[package]] +name = "types-protobuf" +version = "7.35.1.20260906" +description = "Typing stubs for protobuf" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "types_protobuf-7.35.1.20260906-py3-none-any.whl", hash = "sha256:5155e48569e0dabff303fdf578db96cd31ea9a4a63b18018a4ceac6b0ae17462"}, + {file = "types_protobuf-7.35.1.20260906.tar.gz", hash = "sha256:efd1a3862d4c967dad5512ef8d56b1530ac84f182c41735b94004756518c4998"}, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "lint"] +files = [ + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, +] + +[metadata] +lock-version = "2.1" +python-versions = "^3.10" +content-hash = "78de1b99ca4883655ed0e08c84b8b886dce4c424851717d3de6ff2c0aeb400dc" diff --git a/examples/overview-v2/poetry.toml b/examples/overview-v2/poetry.toml new file mode 100644 index 0000000..ab1033b --- /dev/null +++ b/examples/overview-v2/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/examples/overview-v2/pyproject.toml b/examples/overview-v2/pyproject.toml new file mode 100644 index 0000000..7cd560a --- /dev/null +++ b/examples/overview-v2/pyproject.toml @@ -0,0 +1,76 @@ +[project] +name = "overview_v2_example" +version = "1.0.0" +license = "MIT" +description = "Overview of how to use the NI Measurement Data Store V2 APIs for publishing and reading data" +maintainers = [ + {name = "Michael Johansen", email = "michael.johansen@emerson.com"}, + {name = "Joel Dixon", email = "joel.dixon@emerson.com"} +] +readme = "README.md" +repository = "https://github.com/ni/datastore-python" +keywords = ["overview", "example", "v2"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Manufacturing", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: Implementation :: CPython", +] +dynamic = ["dependencies"] +requires-python = '>=3.10,<4.0' + +[[tool.poetry.packages]] +include = "src" + +[project.scripts] +overview-v2 = "src.overview:main" + +[tool.poetry] +requires-poetry = '>=2.1,<3.0' + +[tool.poetry.dependencies] +python = "^3.10" +# The V2 API is not released yet, so use the local ni-datastore code. +ni-datastore = {path = "../..", develop = true} +protobuf = { version=">=4.21" } +ni-protobuf-types = { version = ">=1.1.0" } +hightime = { version = ">=1.0.0" } +grpcio-tools = [ + { version = "1.49.1", python = ">=3.9,<3.12" }, + { version = "1.59.0", python = ">=3.12,<3.13" }, + { version = "1.67.0", python = ">=3.13,<3.14" }, + { version = "1.75.1", python = "^3.14" }, +] + +[tool.poetry.group.dev.dependencies] +types-grpcio = ">=1.0" +types-protobuf = ">=4.21" + +[tool.poetry.group.lint.dependencies] +ni-python-styleguide = ">=0.4.1" +mypy = ">=1.0" + +[build-system] +requires = ["poetry-core>=1.8"] +build-backend = "poetry.core.masonry.api" + +[tool.black] +line-length = 100 + +[tool.mypy] +mypy_path = "src" +files = "." +namespace_packages = true +strict = true +explicit_package_bases = true +exclude = ["docs"] diff --git a/examples/overview-v2/src/__init__.py b/examples/overview-v2/src/__init__.py new file mode 100644 index 0000000..eaca42a --- /dev/null +++ b/examples/overview-v2/src/__init__.py @@ -0,0 +1 @@ +"""Overview example of NI Measurement Data Store V2 usage.""" diff --git a/examples/overview-v2/src/overview.py b/examples/overview-v2/src/overview.py new file mode 100644 index 0000000..b3e7530 --- /dev/null +++ b/examples/overview-v2/src/overview.py @@ -0,0 +1,155 @@ +"""Overview example demonstrating V2 data publishing and querying.""" + +import argparse +from datetime import timezone + +import grpc +import hightime as ht +import numpy as np +from ni.datastore.data.v2 import ( + Condition, + DataStoreClient, + Outcome, + Status, + Step, + StepField, + TestResult, +) +from ni.datastore.metadata import ( + MetadataStoreClient, + Operator, + SoftwareItem, + TestStation, + Uut, + UutInstance, +) +from nitypes.waveform import AnalogWaveform, Timing + +# Remove gRPC's 4 MB receive limit so that large values, such as long waveforms, can be read. +_GRPC_OPTIONS = [("grpc.max_receive_message_length", -1)] + + +def main() -> None: + """Main function to demonstrate V2 data publishing and querying.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--grpc-target", + default="", + help="gRPC target of a V2 service, such as [::1]:59606. Uses discovery when omitted.", + ) + args = parser.parse_args() + + # The clients do not close a channel that they did not create, so close it here. + grpc_channel = ( + grpc.insecure_channel(args.grpc_target, options=_GRPC_OPTIONS) if args.grpc_target else None + ) + try: + published_measurement_id = publish_data(grpc_channel) + query_data(published_measurement_id, grpc_channel) + finally: + if grpc_channel is not None: + grpc_channel.close() + + +def publish_data(grpc_channel: grpc.Channel | None) -> str: + """Demonstrate V2 data publishing of an AnalogWaveform.""" + with ( + MetadataStoreClient(grpc_channel=grpc_channel) as metadata_store_client, + DataStoreClient(grpc_channel=grpc_channel) as data_store_client, + ): + # A UUT describes a product model that many test results share, so create it once + # and link it by ID. Each UUT instance refers to its UUT by ID. + uut_id = metadata_store_client.create_uut(Uut(model_name="NI-6508", family="Digital")) + + # Create the TestResult with its metadata inline. The service finds a matching + # operator, test station, UUT instance, or software item, or creates a new one. + # In V2, status tracks progress separately from the outcome. + test_result = TestResult( + name="sample test result", + uut_instance=UutInstance(uut_id=uut_id, serial_number="A861-42367"), + operator=Operator(name="James Bowery", role="Test Operator"), + test_station=TestStation(name="TestStation_12"), + software_items=[ + SoftwareItem(product="Windows", version="10.0.19044"), + SoftwareItem(product="Python", version="3.12"), + ], + status=Status.RUNNING, + ) + created_test_result = data_store_client.create_test_result(test_result) + print(f"created test_result_id: {created_test_result.id}") + print(f"linked operator_id: {created_test_result.operator_id}") + + # Create the step. In V2, conditions belong to the step. + step = Step( + name="Initial step", + test_result_id=created_test_result.id, + conditions=[ + Condition("Ambient Temperature", 23.0, condition_type="Environment", units="degC") + ], + ) + created_step = data_store_client.create_step(step) + + name = "data publish sample" + # Create waveform data to publish + waveform = AnalogWaveform( + sample_count=3, + raw_data=np.array([1.0, 2.0, 3.0]), + timing=Timing.create_with_regular_interval( + ht.timedelta(seconds=1e-3), ht.datetime.now(timezone.utc) + ), + ) + + # Create the measurement with the waveform data + published_measurement = data_store_client.create_measurement( + name=name, + value=waveform, + step_id=created_step.id, + units="V", + outcome=Outcome.PASSED, + ) + print( + f"Published measurement: '{published_measurement.name}' with id {published_measurement.id}" + ) + + # Mark the test result as done now that publishing is complete. + data_store_client.update_test_result( + created_test_result.id, status=Status.DONE, outcome=Outcome.PASSED + ) + + return published_measurement.id + + +def query_data(published_measurement_id: str, grpc_channel: grpc.Channel | None) -> None: + """Demonstrate querying a published AnalogWaveform measurement.""" + with ( + MetadataStoreClient(grpc_channel=grpc_channel) as metadata_store_client, + DataStoreClient(grpc_channel=grpc_channel) as data_store_client, + ): + published_measurements = data_store_client.query_measurements( + odata_query=f"$filter=Id eq {published_measurement_id}" + ) + found_measurement = next(iter(published_measurements), None) + + if found_measurement is not None: + print( + f"Found published measurement: '{found_measurement.name}' with id {found_measurement.id}" + ) + test_result = data_store_client.get_test_result(found_measurement.test_result_id) + print(f"test_result: {test_result.name} ({test_result.status.name})") + operator = metadata_store_client.get_operator(test_result.operator_id) + print(f"operator: {operator}") + + step = data_store_client.get_step( + found_measurement.step_id, read_fields=[StepField.CONDITIONS] + ) + for condition in step.conditions: + print(f"condition: {condition.name} = {condition.value} {condition.units}") + + waveform = data_store_client.read_measurement_value( + found_measurement, expected_type=AnalogWaveform + ) + print(f"published data is: {waveform.raw_data}") + + +if __name__ == "__main__": + main() diff --git a/poetry.lock b/poetry.lock index 062cc39..1cbf315 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -1365,6 +1365,53 @@ ni-measurements-metadata-v1-proto = ">=1.0.0" ni-protobuf-types = ">=1.3.0" protobuf = ">=4.21" +[[package]] +name = "ni-measurements-data-v2-client" +version = "1.0.0.dev0" +description = "gRPC Client for NI Data Store Service V2" +optional = false +python-versions = ">=3.10,<4.0" +groups = ["main"] +files = [] +develop = false + +[package.dependencies] +grpcio = ">=1.49.1,<2.0" +ni-grpc-extensions = ">=1.1.0" +ni-measurementlink-discovery-v1-client = ">=1.1.0" +ni-measurements-data-v2-proto = ">=1.0.0.dev0" + +[package.source] +type = "git" +url = "https://github.com/ni/ni-apis-python.git" +reference = "users/sudharshana/add-v2-proto-and-client-package" +resolved_reference = "ec5f96947086e862712f3def3f7f745a89cc2ef9" +subdirectory = "packages/ni.measurements.data.v2.client" + +[[package]] +name = "ni-measurements-data-v2-proto" +version = "1.0.0.dev0" +description = "Protobuf data types and service stubs for NI data store V2 gRPC APIs" +optional = false +python-versions = ">=3.10,<4.0" +groups = ["main"] +files = [] +develop = false + +[package.dependencies] +grpcio = ">=1.49.1" +ni-datamonikers-v1-proto = ">=1.0.0" +ni-measurements-metadata-v1-proto = ">=1.0.0" +ni-protobuf-types = ">=1.3.0" +protobuf = ">=4.21" + +[package.source] +type = "git" +url = "https://github.com/ni/ni-apis-python.git" +reference = "users/sudharshana/add-v2-proto-and-client-package" +resolved_reference = "ec5f96947086e862712f3def3f7f745a89cc2ef9" +subdirectory = "packages/ni.measurements.data.v2.proto" + [[package]] name = "ni-measurements-metadata-v1-client" version = "1.0.0" @@ -2656,4 +2703,4 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "e8d29f090b5bbc01cb6e3142fe7c134217fb52ef0e296c5326c603b94d9b53d2" +content-hash = "1a70727e96388ba75899aa627b0a872e8ce7ce5ab7720996236949df4466d5ce" diff --git a/pyproject.toml b/pyproject.toml index cf52c08..941faf3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ni.datastore" -version = "2.2.0.dev0" +version = "3.0.0.dev0" license = "MIT" description = "APIs for publishing and retrieving data from NI Measurement Data Services" authors = [{name = "NI", email = "opensource@ni.com"}] @@ -42,6 +42,8 @@ python = "^3.10" protobuf = {version=">=4.21"} ni-measurements-data-v1-client = { version = ">=1.2.0.dev0", allow-prereleases = true } ni-measurements-metadata-v1-client = { version = ">=1.0.0" } +ni-measurements-data-v2-client = { git = "https://github.com/ni/ni-apis-python.git", branch = "users/sudharshana/add-v2-proto-and-client-package", subdirectory = "packages/ni.measurements.data.v2.client" } +ni-measurements-data-v2-proto = { git = "https://github.com/ni/ni-apis-python.git", branch = "users/sudharshana/add-v2-proto-and-client-package", subdirectory = "packages/ni.measurements.data.v2.proto" } ni-protobuf-types = { version = ">=1.3.0.dev0", allow-prereleases = true } hightime = { version = ">=1.0.0" } @@ -112,4 +114,5 @@ testpaths = ["tests"] norecursedirs = [".venv"] filterwarnings = [ "ignore:cannot collect test class.*because it has a __init__ constructor:pytest.PytestCollectionWarning", + "ignore:cannot collect test class.*because it has a __new__ constructor:pytest.PytestCollectionWarning", ] diff --git a/src/ni/datastore/data/v2/__init__.py b/src/ni/datastore/data/v2/__init__.py new file mode 100644 index 0000000..7dc42c3 --- /dev/null +++ b/src/ni/datastore/data/v2/__init__.py @@ -0,0 +1,82 @@ +"""Public API for accessing the NI Data Store V2.""" + +from ni.datastore.data.v2._data_store_client import DataStoreClient +from ni.datastore.data.v2._types._condition import Condition +from ni.datastore.data.v2._types._create import StepCreateFields, TestResultCreateFields +from ni.datastore.data.v2._types._error_information import ErrorInformation +from ni.datastore.data.v2._types._json_value import JsonValue +from ni.datastore.data.v2._types._measurement import Measurement +from ni.datastore.data.v2._types._measurement_value import ( + MeasurementReadValue, + MeasurementValue, +) +from ni.datastore.data.v2._types._outcome import Outcome +from ni.datastore.data.v2._types._query_results import QueryResults +from ni.datastore.data.v2._types._read_fields import ( + MeasurementField, + MeasurementValueField, + StepField, + TestResultField, +) +from ni.datastore.data.v2._types._status import Status +from ni.datastore.data.v2._types._step import Step +from ni.datastore.data.v2._types._test_result import TestResult +from ni.datastore.data.v2._types._update import ( + MeasurementUpdateBuilder, + MeasurementUpdateFields, + StepUpdateBuilder, + StepUpdateFields, + TestResultUpdateBuilder, + TestResultUpdateFields, +) + +__all__ = [ + "Condition", + "DataStoreClient", + "ErrorInformation", + "JsonValue", + "Measurement", + "MeasurementField", + "MeasurementReadValue", + "MeasurementUpdateBuilder", + "MeasurementUpdateFields", + "MeasurementValue", + "MeasurementValueField", + "Outcome", + "QueryResults", + "Status", + "Step", + "StepCreateFields", + "StepField", + "StepUpdateBuilder", + "StepUpdateFields", + "TestResult", + "TestResultCreateFields", + "TestResultField", + "TestResultUpdateBuilder", + "TestResultUpdateFields", +] + +# Hide that it was not defined in this top-level package +Condition.__module__ = __name__ +DataStoreClient.__module__ = __name__ +ErrorInformation.__module__ = __name__ +JsonValue.__module__ = __name__ +Measurement.__module__ = __name__ +MeasurementField.__module__ = __name__ +MeasurementUpdateBuilder.__module__ = __name__ +MeasurementUpdateFields.__module__ = __name__ +MeasurementValueField.__module__ = __name__ +Outcome.__module__ = __name__ +QueryResults.__module__ = __name__ +Status.__module__ = __name__ +Step.__module__ = __name__ +StepCreateFields.__module__ = __name__ +StepField.__module__ = __name__ +StepUpdateBuilder.__module__ = __name__ +StepUpdateFields.__module__ = __name__ +TestResult.__module__ = __name__ +TestResultCreateFields.__module__ = __name__ +TestResultField.__module__ = __name__ +TestResultUpdateBuilder.__module__ = __name__ +TestResultUpdateFields.__module__ = __name__ diff --git a/src/ni/datastore/data/v2/_data_store_client.py b/src/ni/datastore/data/v2/_data_store_client.py new file mode 100644 index 0000000..8a3f70a --- /dev/null +++ b/src/ni/datastore/data/v2/_data_store_client.py @@ -0,0 +1,1258 @@ +"""Data store client for publishing and reading data with NI Data Store Service V2.""" + +from __future__ import annotations + +import sys +from collections.abc import Iterable, Mapping, Sequence +from enum import Enum +from types import TracebackType +from typing import Any, overload, Type, TYPE_CHECKING, TypeVar +from uuid import UUID + +import hightime as ht +from google.protobuf.field_mask_pb2 import FieldMask +from grpc import Channel +from ni.measurementlink.discovery.v1.client import DiscoveryClient +from ni.measurements.data.v2.client import DataStoreClient as DataStoreServiceClient +from ni.measurements.data.v2.data_store_pb2 import ( + Measurement as MeasurementProto, + Step as StepProto, + TestResult as TestResultProto, +) +from ni.measurements.data.v2.data_store_service_pb2 import ( + AddConditionRequest, + AddMeasurementMetadataRequest, + AddTestResultMetadataRequest, + BatchCreateMeasurementsRequest, + BatchCreateStepsRequest, + BatchCreateTestResultsRequest, + BatchDeleteMeasurementsRequest, + BatchDeleteStepsRequest, + BatchDeleteTestResultsRequest, + BatchUpdateMeasurementsRequest, + BatchUpdateStepsRequest, + BatchUpdateTestResultsRequest, + CreateMeasurementRequest, + CreateStepRequest, + CreateTestResultRequest, + DeleteMeasurementRequest, + DeleteStepRequest, + DeleteTestResultRequest, + GetMeasurementRequest, + GetStepRequest, + GetTestResultRequest, + QueryMeasurementsRequest, + QueryStepsRequest, + QueryTestResultsRequest, + ReadMeasurementValuesRequest, + RemoveConditionRequest, + RemoveMeasurementMetadataRequest, + RemoveTestResultMetadataRequest, + UpdateMeasurementRequest, + UpdateStepRequest, + UpdateTestResultRequest, +) +from ni_grpc_extensions.channelpool import GrpcChannelPool + +from ni.datastore.data.v2._grpc_conversion import ( + convert_measurement_value_from_protobuf, + id_list, + id_string, + measurement_value_from_protobuf, + string_list, +) +from ni.datastore.data.v2._types._condition import Condition +from ni.datastore.data.v2._types._create import StepCreateFields, TestResultCreateFields +from ni.datastore.data.v2._types._error_information import ErrorInformation +from ni.datastore.data.v2._types._measurement import Measurement +from ni.datastore.data.v2._types._measurement_value import MeasurementReadValue, MeasurementValue +from ni.datastore.data.v2._types._outcome import Outcome +from ni.datastore.data.v2._types._query_results import QueryResults +from ni.datastore.data.v2._types._read_fields import ( + MeasurementField, + MeasurementValueField, + StepField, + TestResultField, +) +from ni.datastore.data.v2._types._step import Step +from ni.datastore.data.v2._types._test_result import TestResult +from ni.datastore.data.v2._types._update import ( + MeasurementUpdateBuilder, + MeasurementUpdateFields, + StepUpdateBuilder, + StepUpdateFields, + TestResultUpdateBuilder, + TestResultUpdateFields, +) +from ni.datastore.data.v2._update_conversion import ( + MEASUREMENT_UPDATE_FIELDS, + populate_update, + resource_update_fields, + STEP_RELATIONSHIPS, + STEP_UPDATE_FIELDS, + TEST_RESULT_RELATIONSHIPS, + TEST_RESULT_UPDATE_FIELDS, +) +from ni.datastore.metadata import HardwareItem, SoftwareItem, TestAdapter + +if TYPE_CHECKING: + if sys.version_info >= (3, 11): + from typing import Self, Unpack + else: + from typing_extensions import Self, Unpack + +TRead = TypeVar("TRead") +_T = TypeVar("_T") + + +class DataStoreClient: + """Data store client for publishing and reading data with the V2 service.""" + + __slots__ = ("_closed", "_data_store_client") + + _DATA_STORE_CLIENT_CLOSED_ERROR = ( + "This DataStoreClient has been closed. Create a new DataStoreClient for further " + "interaction with the data store." + ) + + _closed: bool + _data_store_client: DataStoreServiceClient + + def __init__( + self, + discovery_client: DiscoveryClient | None = None, + grpc_channel: Channel | None = None, + grpc_channel_pool: GrpcChannelPool | None = None, + ) -> None: + """Initialize the DataStoreClient. + + Args: + discovery_client: An optional discovery client (recommended). + + grpc_channel: An optional data store gRPC channel. Providing this channel will bypass + discovery service resolution of the data store. + + grpc_channel_pool: An optional gRPC channel pool (recommended). + """ + self._closed = False + self._data_store_client = DataStoreServiceClient( + discovery_client=discovery_client, + grpc_channel=grpc_channel, + grpc_channel_pool=grpc_channel_pool, + ) + + def __enter__(self) -> Self: + """Enter the runtime context of the data store client.""" + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Exit the runtime context of the data store client.""" + self.close() + + def close(self) -> None: + """Close the data store client and clean up resources that it owns.""" + self._closed = True + self._data_store_client.close() + + @overload + def create_test_result(self, test_result: TestResult, /) -> TestResult: ... + + @overload + def create_test_result( + self, name: str, /, **fields: Unpack[TestResultCreateFields] + ) -> TestResult: ... + + def create_test_result( + self, test_result: TestResult | str, /, **fields: Unpack[TestResultCreateFields] + ) -> TestResult: + """Create a test result, including any nested steps. + + Pass either a TestResult, or its name and fields as keyword arguments. + + Args: + test_result: The test result to create, or its name. + + **fields: The test result fields, when a name is given. Each has the + same meaning as the matching TestResult argument. + + Returns: + TestResult: The created test result as returned by the service, including + its ID. Nested steps and expanded metadata are not included. + + Raises: + TypeError: If fields are given with a TestResult. + """ + if isinstance(test_result, TestResult): + _reject_model_fields(fields, "a TestResult", "test result") + else: + test_result = TestResult(test_result, **fields) + create_request = CreateTestResultRequest(test_result=test_result.to_protobuf()) + return TestResult.from_protobuf( + self._get_data_store_client().create_test_result(create_request) + ) + + @overload + def create_step(self, step: Step, /) -> Step: ... + + @overload + def create_step(self, name: str, /, **fields: Unpack[StepCreateFields]) -> Step: ... + + def create_step(self, step: Step | str, /, **fields: Unpack[StepCreateFields]) -> Step: + """Create a step, including any conditions, child steps, and measurements. + + Pass either a Step, or its name and fields as keyword arguments. + + Args: + step: The step to create, or its name. + + **fields: The step fields, when a name is given, such as + test_result_id. Each has the same meaning as the matching Step + argument. + + Returns: + Step: The created step as returned by the service, including its ID. + Conditions, child steps, measurements, and expanded metadata are not + included. + + Raises: + TypeError: If fields are given with a Step. + """ + if isinstance(step, Step): + _reject_model_fields(fields, "a Step", "step") + else: + step = Step(step, **fields) + create_request = CreateStepRequest(step=step.to_protobuf()) + return Step.from_protobuf(self._get_data_store_client().create_step(create_request)) + + def add_condition(self, step_id: str | UUID, condition: Condition) -> Step: + """Add a condition to a step, replacing any condition with the same name. + + Args: + step_id: The ID of the step that owns the condition. + + condition: The condition to add. + + Returns: + Step: The updated step. + """ + add_request = AddConditionRequest( + step_id=id_string(step_id), condition=condition.to_protobuf() + ) + return Step.from_protobuf(self._get_data_store_client().add_condition(add_request).step) + + def remove_condition(self, step_id: str | UUID, condition_name: str) -> Step: + """Remove a condition from a step. + + Args: + step_id: The ID of the step that owns the condition. + + condition_name: The name of the condition to remove. + + Returns: + Step: The updated step. + """ + remove_request = RemoveConditionRequest( + step_id=id_string(step_id), condition_name=condition_name + ) + return Step.from_protobuf( + self._get_data_store_client().remove_condition(remove_request).step + ) + + @overload + def create_measurement(self, measurement: Measurement, /) -> Measurement: ... + + @overload + def create_measurement( + self, + name: str, + value: MeasurementValue, + step_id: str | UUID, + *, + units: str = "", + upper_limit: MeasurementValue | None = None, + lower_limit: MeasurementValue | None = None, + comparison_type: str = "", + notes: str = "", + start_date_time: ht.datetime | None = None, + end_date_time: ht.datetime | None = None, + duration: ht.timedelta | None = None, + outcome: Outcome = Outcome.UNSPECIFIED, + error_information: ErrorInformation | None = None, + extension: Mapping[str, Any] | None = None, + schema_id: str = "", + software_item_ids: Iterable[str | UUID] = (), + hardware_item_ids: Iterable[str | UUID] = (), + test_adapter_ids: Iterable[str | UUID] = (), + software_items: Iterable[SoftwareItem] = (), + hardware_items: Iterable[HardwareItem] = (), + test_adapters: Iterable[TestAdapter] = (), + ) -> Measurement: ... + + def create_measurement(self, *args: Any, **kwargs: Any) -> Measurement: + """Create a measurement associated with a test step. + + Pass either a Measurement, or the measurement fields as arguments. + + Args: + measurement: The measurement to create. It must include a step ID and + value. Cannot be combined with the other arguments. + + name: The name used for grouping conceptually alike measurements. + + value: The measured value. Supported types are bool, int, float, str, + Scalar, Vector, iterables of scalar values, AnalogWaveform, + ComplexWaveform, Spectrum, DigitalWaveform, XYData, and JSON + mappings. + + step_id: The ID of the step that owns the measurement. + + units: Engineering units of the value. Defaults to the units of a + Scalar value. + + upper_limit: The publisher-supplied upper limit. Any supported value + type is accepted. The service stores it but does not compare it + with the value. + + lower_limit: The publisher-supplied lower limit. Any supported value + type is accepted. The service stores it but does not compare it + with the value. + + comparison_type: The publisher-defined comparison operation. The + service stores it but does not derive the outcome from it. + + notes: Any notes to associate with the measurement. + + start_date_time: The start time of the measurement. + + end_date_time: The end time of the measurement. + + duration: The duration of the measurement. + + outcome: The outcome of the measurement. + + error_information: Error details for the measurement. + + extension: Custom JSON data for the measurement. + + schema_id: Schema that governs the extension. Reserved for future use. + + software_item_ids: The software item IDs for the measurement. These + must already be linked to the owning test result. + + hardware_item_ids: The hardware item IDs for the measurement. These + must already be linked to the owning test result. + + test_adapter_ids: The test adapter IDs for the measurement. These + must already be linked to the owning test result. + + software_items: Software items to find or create as part of this + request. Each item must already be linked to the owning test result. + + hardware_items: Hardware items to find or create as part of this + request. Each item must already be linked to the owning test result. + + test_adapters: Test adapters to find or create as part of this + request. Each item must already be linked to the owning test result. + + Returns: + Measurement: The created measurement as returned by the service, + including its ID. Expanded metadata is not included. + + Raises: + TypeError: If fields are given with a Measurement. + """ + if args and isinstance(args[0], Measurement): + _reject_model_fields( + {**kwargs, **dict(enumerate(args[1:]))}, "a Measurement", "measurement" + ) + measurement: Measurement = args[0] + else: + measurement = _measurement_from_fields(*args, **kwargs) + create_request = CreateMeasurementRequest(measurement=measurement.to_protobuf()) + return Measurement.from_protobuf( + self._get_data_store_client().create_measurement(create_request) + ) + + def batch_create_test_results(self, test_results: Iterable[TestResult]) -> Sequence[TestResult]: + """Create multiple test results, including any nested steps, in one atomic call. + + Args: + test_results: The test results to create. + + Returns: + Sequence[TestResult]: The created test results, in request order. Nested + steps and expanded metadata are not included. + """ + batch_request = BatchCreateTestResultsRequest( + requests=[ + CreateTestResultRequest(test_result=test_result.to_protobuf()) + for test_result in test_results + ] + ) + batch_response = self._get_data_store_client().batch_create_test_results(batch_request) + return [ + TestResult.from_protobuf(test_result) for test_result in batch_response.test_results + ] + + def batch_create_steps(self, steps: Iterable[Step]) -> Sequence[Step]: + """Create multiple steps, including any nested children, in one atomic call. + + Args: + steps: The steps to create. + + Returns: + Sequence[Step]: The created steps, in request order. Conditions, child + steps, measurements, and expanded metadata are not included. + """ + batch_request = BatchCreateStepsRequest( + requests=[CreateStepRequest(step=step.to_protobuf()) for step in steps] + ) + batch_response = self._get_data_store_client().batch_create_steps(batch_request) + return [Step.from_protobuf(step) for step in batch_response.steps] + + def batch_create_measurements( + self, measurements: Iterable[Measurement] + ) -> Sequence[Measurement]: + """Create multiple measurements in one atomic call. + + Args: + measurements: The measurements to create. Each must include a step ID and + value. They may target different steps or test results. + + Returns: + Sequence[Measurement]: The created measurements, in request order. + Expanded metadata is not included. + """ + batch_request = BatchCreateMeasurementsRequest( + requests=[ + CreateMeasurementRequest(measurement=measurement.to_protobuf()) + for measurement in measurements + ] + ) + batch_response = self._get_data_store_client().batch_create_measurements(batch_request) + return [ + Measurement.from_protobuf(measurement) for measurement in batch_response.measurements + ] + + @overload + def read_measurement_value( + self, + read_source: Measurement | str | UUID, + expected_type: Type[TRead], + ) -> TRead: ... + + @overload + def read_measurement_value( + self, + read_source: Measurement | str | UUID, + ) -> MeasurementReadValue: ... + + def read_measurement_value( + self, + read_source: Measurement | str | UUID, + expected_type: Type[TRead] | None = None, + ) -> TRead | MeasurementReadValue: + """Read the value of a measurement. + + Args: + read_source: The measurement to read, or its ID. Only the ID is used. + + expected_type: Optional type to validate the returned data against. + + Returns: + The value converted to a nitypes or Python value. A single value is + always returned as a Scalar, with or without units. A list of + values is returned as a Vector. + + Raises: + TypeError: If expected_type is provided and the actual data type + doesn't match. + """ + read_request = ReadMeasurementValuesRequest( + measurement_id=_resource_id(read_source), read_mask=FieldMask(paths=["value"]) + ) + response = self._get_data_store_client().read_measurement_values(read_request) + read_value = convert_measurement_value_from_protobuf(response.value) + if expected_type is not None and not isinstance(read_value, expected_type): + raise TypeError(f"Expected type {expected_type}, got {type(read_value)}") + return read_value + + def read_measurement_values( + self, + read_source: Measurement | str | UUID, + *, + read_fields: Iterable[MeasurementValueField] = (), + ) -> Measurement: + """Read the value and limits of a measurement. + + Args: + read_source: The measurement to read, or its ID. Only the ID is used. + + read_fields: Optional fields to read, such as MeasurementValueField.VALUE. + The value and both limits are read when empty. + + Returns: + Measurement: A measurement with id, value, upper_limit, and + lower_limit set. A limit that was not published is None. + """ + measurement_id = _resource_id(read_source) + read_request = ReadMeasurementValuesRequest( + measurement_id=measurement_id, read_mask=_read_mask(read_fields) + ) + response = self._get_data_store_client().read_measurement_values(read_request) + return Measurement( + id=measurement_id, + value=measurement_value_from_protobuf(response, "value"), + upper_limit=measurement_value_from_protobuf(response, "upper_limit"), + lower_limit=measurement_value_from_protobuf(response, "lower_limit"), + ) + + def get_test_result( + self, test_result_id: str | UUID, *, read_fields: Iterable[TestResultField | str] = () + ) -> TestResult: + """Get the test result with the given ID. + + Args: + test_result_id: The ID of the test result. + + read_fields: Optional fields to return, such as TestResultField.NAME. Field + names as strings are also accepted. When empty, the service returns its + default fields. + + Returns: + TestResult: The requested test result. + """ + get_request = GetTestResultRequest( + test_result_id=id_string(test_result_id), read_mask=_read_mask(read_fields) + ) + return TestResult.from_protobuf(self._get_data_store_client().get_test_result(get_request)) + + def get_step(self, step_id: str | UUID, *, read_fields: Iterable[StepField | str] = ()) -> Step: + """Get the step with the given ID. + + Args: + step_id: The ID of the step. + + read_fields: Optional fields to return, such as StepField.CONDITIONS. Field + names as strings are also accepted. When empty, the service returns its + default fields. + + Returns: + Step: The requested step. + """ + get_request = GetStepRequest(step_id=id_string(step_id), read_mask=_read_mask(read_fields)) + return Step.from_protobuf(self._get_data_store_client().get_step(get_request)) + + def get_measurement( + self, measurement_id: str | UUID, *, read_fields: Iterable[MeasurementField | str] = () + ) -> Measurement: + """Get the measurement with the given ID. + + Args: + measurement_id: The ID of the measurement. + + read_fields: Optional fields to return, such as MeasurementField.CONDITIONS. + Field names as strings are also accepted. When empty, the service returns + its default fields. + + Returns: + Measurement: The requested measurement. + """ + get_request = GetMeasurementRequest( + measurement_id=id_string(measurement_id), read_mask=_read_mask(read_fields) + ) + return Measurement.from_protobuf(self._get_data_store_client().get_measurement(get_request)) + + def query_test_results( + self, odata_query: str = "", *, read_fields: Iterable[TestResultField | str] = () + ) -> QueryResults[TestResult]: + """Query test results using OData query syntax. + + Args: + odata_query: An OData query string. An empty string returns all test + results. $select, $count, $expand, and $apply are not supported. + + read_fields: Optional fields to return for each test result, such as + TestResultField.NAME. Field names as strings are also accepted. + + Returns: + QueryResults[TestResult]: The matching test results, with next_link + set when more results are available. + """ + query_request = QueryTestResultsRequest( + odata_query=odata_query, read_mask=_read_mask(read_fields) + ) + query_response = self._get_data_store_client().query_test_results(query_request) + return QueryResults( + (TestResult.from_protobuf(proto) for proto in query_response.test_results), + query_response.next_link, + ) + + def query_steps( + self, odata_query: str = "", *, read_fields: Iterable[StepField | str] = () + ) -> QueryResults[Step]: + """Query steps using OData query syntax. + + Args: + odata_query: An OData query string. An empty string returns all + steps. $select, $count, $expand, and $apply are not supported. + + read_fields: Optional fields to return for each step, such as + StepField.CONDITIONS. Field names as strings are also accepted. + + Returns: + QueryResults[Step]: The matching steps, with next_link set when more + results are available. + """ + query_request = QueryStepsRequest( + odata_query=odata_query, read_mask=_read_mask(read_fields) + ) + query_response = self._get_data_store_client().query_steps(query_request) + return QueryResults( + (Step.from_protobuf(proto) for proto in query_response.steps), + query_response.next_link, + ) + + def query_measurements( + self, odata_query: str = "", *, read_fields: Iterable[MeasurementField | str] = () + ) -> QueryResults[Measurement]: + """Query measurements using OData query syntax. + + Args: + odata_query: An OData query string. An empty string returns all + measurements. $select, $count, $expand, and $apply are not + supported. + + read_fields: Optional fields to return for each measurement, such as + MeasurementField.CONDITIONS. Field names as strings are also accepted. + + Returns: + QueryResults[Measurement]: The matching measurements, with next_link + set when more results are available. + """ + query_request = QueryMeasurementsRequest( + odata_query=odata_query, read_mask=_read_mask(read_fields) + ) + query_response = self._get_data_store_client().query_measurements(query_request) + return QueryResults( + (Measurement.from_protobuf(proto) for proto in query_response.measurements), + query_response.next_link, + ) + + @overload + def update_test_result(self, test_result: TestResultUpdateBuilder, /) -> TestResult: ... + + @overload + def update_test_result( + self, test_result: TestResult | str | UUID, /, **fields: Unpack[TestResultUpdateFields] + ) -> TestResult: ... + + def update_test_result( + self, + test_result: TestResult | TestResultUpdateBuilder | str | UUID, + /, + **fields: Unpack[TestResultUpdateFields], + ) -> TestResult: + """Update mutable test result fields. + + Args: + test_result: What to update. One of: + + * A TestResultUpdateBuilder. Only the fields selected on the + builder change. + * An ID or a TestResult with keyword fields, such as + link="..." or outcome=Outcome.FAILED. Only those fields + change. A value of None clears the field. + * A TestResult on its own. Every updatable field is set from + it, so empty fields are cleared. Stage 2 fields are sent only + when they have a value. When a relationship has both an ID + and inline metadata, only the ID is sent. + + **fields: The fields to change. Not allowed with a builder. + + Returns: + TestResult: The updated test result. + + Raises: + TypeError: If keyword fields are given with a builder. + ValueError: If no fields are given or a field cannot be updated. + """ + update_request = _test_result_update(test_result, fields) + return TestResult.from_protobuf( + self._get_data_store_client().update_test_result(update_request) + ) + + @overload + def update_step(self, step: StepUpdateBuilder, /) -> Step: ... + + @overload + def update_step( + self, step: Step | str | UUID, /, **fields: Unpack[StepUpdateFields] + ) -> Step: ... + + def update_step( + self, step: Step | StepUpdateBuilder | str | UUID, /, **fields: Unpack[StepUpdateFields] + ) -> Step: + """Update mutable step fields. + + Args: + step: What to update. One of: + + * A StepUpdateBuilder. Only the fields selected on the builder + change. + * An ID or a Step with keyword fields, such as notes="...". + Only those fields change. A value of None clears the field. + * A Step on its own. Every updatable field is set from it, so + empty fields are cleared. Stage 2 fields are sent only when + they have a value. When both test_id and test are set, only + test_id is sent. + + **fields: The fields to change. Not allowed with a builder. + + Returns: + Step: The updated step. + + Raises: + TypeError: If keyword fields are given with a builder. + ValueError: If no fields are given or a field cannot be updated. + """ + update_request = _step_update(step, fields) + return Step.from_protobuf(self._get_data_store_client().update_step(update_request)) + + @overload + def update_measurement(self, measurement: MeasurementUpdateBuilder, /) -> Measurement: ... + + @overload + def update_measurement( + self, measurement: Measurement | str | UUID, /, **fields: Unpack[MeasurementUpdateFields] + ) -> Measurement: ... + + def update_measurement( + self, + measurement: Measurement | MeasurementUpdateBuilder | str | UUID, + /, + **fields: Unpack[MeasurementUpdateFields], + ) -> Measurement: + """Update mutable measurement fields. + + The value, units, and limits cannot be updated. + + Args: + measurement: What to update. One of: + + * A MeasurementUpdateBuilder. Only the fields selected on the + builder change. + * An ID or a Measurement with keyword fields, such as + notes="...". Only those fields change. A value of None + clears the field. + * A Measurement on its own. Every updatable field is set from + it, so empty fields are cleared. duration is sent only when + it has a value. + + **fields: The fields to change. Not allowed with a builder. + + Returns: + Measurement: The updated measurement metadata. + + Raises: + TypeError: If keyword fields are given with a builder. + ValueError: If no fields are given or a field cannot be updated. + """ + update_request = _measurement_update(measurement, fields) + return Measurement.from_protobuf( + self._get_data_store_client().update_measurement(update_request) + ) + + @overload + def batch_update_test_results( + self, updates: Mapping[UUID, TestResultUpdateFields] + ) -> Sequence[TestResult]: ... + + @overload + def batch_update_test_results( + self, updates: Mapping[str, TestResultUpdateFields] + ) -> Sequence[TestResult]: ... + + @overload + def batch_update_test_results( + self, updates: Iterable[TestResult | TestResultUpdateBuilder] + ) -> Sequence[TestResult]: ... + + def batch_update_test_results( + self, + updates: ( + Mapping[str, TestResultUpdateFields] + | Mapping[UUID, TestResultUpdateFields] + | Iterable[TestResult | TestResultUpdateBuilder] + ), + ) -> Sequence[TestResult]: + """Update multiple test results in one atomic call. + + Args: + updates: Either the fields to change, keyed by test result ID, or + builders and test results. Each item follows the + update_test_result rules. + + Returns: + Sequence[TestResult]: The updated test results, in request order. + + Raises: + ValueError: If no updates are given, or an update has no fields or + a field that cannot be updated. + """ + requests = [ + _test_result_update(target, fields) for target, fields in _update_items(updates) + ] + batch_request = BatchUpdateTestResultsRequest(requests=requests) + batch_response = self._get_data_store_client().batch_update_test_results(batch_request) + return [TestResult.from_protobuf(proto) for proto in batch_response.test_results] + + @overload + def batch_update_steps(self, updates: Mapping[UUID, StepUpdateFields]) -> Sequence[Step]: ... + + @overload + def batch_update_steps(self, updates: Mapping[str, StepUpdateFields]) -> Sequence[Step]: ... + + @overload + def batch_update_steps(self, updates: Iterable[Step | StepUpdateBuilder]) -> Sequence[Step]: ... + + def batch_update_steps( + self, + updates: ( + Mapping[str, StepUpdateFields] + | Mapping[UUID, StepUpdateFields] + | Iterable[Step | StepUpdateBuilder] + ), + ) -> Sequence[Step]: + """Update multiple steps in one atomic call. + + Args: + updates: Either the fields to change, keyed by step ID, or builders + and steps. Each item follows the update_step rules. + + Returns: + Sequence[Step]: The updated steps, in request order. + + Raises: + ValueError: If no updates are given, or an update has no fields or + a field that cannot be updated. + """ + requests = [_step_update(target, fields) for target, fields in _update_items(updates)] + batch_request = BatchUpdateStepsRequest(requests=requests) + batch_response = self._get_data_store_client().batch_update_steps(batch_request) + return [Step.from_protobuf(proto) for proto in batch_response.steps] + + @overload + def batch_update_measurements( + self, updates: Mapping[UUID, MeasurementUpdateFields] + ) -> Sequence[Measurement]: ... + + @overload + def batch_update_measurements( + self, updates: Mapping[str, MeasurementUpdateFields] + ) -> Sequence[Measurement]: ... + + @overload + def batch_update_measurements( + self, updates: Iterable[Measurement | MeasurementUpdateBuilder] + ) -> Sequence[Measurement]: ... + + def batch_update_measurements( + self, + updates: ( + Mapping[str, MeasurementUpdateFields] + | Mapping[UUID, MeasurementUpdateFields] + | Iterable[Measurement | MeasurementUpdateBuilder] + ), + ) -> Sequence[Measurement]: + """Update multiple measurements in one atomic call. + + Args: + updates: Either the fields to change, keyed by measurement ID, or + builders and measurements. Each item follows the + update_measurement rules. + + Returns: + Sequence[Measurement]: The updated measurements, in request order. + + Raises: + ValueError: If no updates are given, or an update has no fields or + a field that cannot be updated. + """ + requests = [ + _measurement_update(target, fields) for target, fields in _update_items(updates) + ] + batch_request = BatchUpdateMeasurementsRequest(requests=requests) + batch_response = self._get_data_store_client().batch_update_measurements(batch_request) + return [Measurement.from_protobuf(proto) for proto in batch_response.measurements] + + def add_test_result_metadata( + self, + test_result_id: str | UUID, + *, + software_item_ids: Iterable[str | UUID] = (), + hardware_item_ids: Iterable[str | UUID] = (), + test_adapter_ids: Iterable[str | UUID] = (), + ) -> TestResult: + """Link existing software items, hardware items, or test adapters to a test result. + + The items become available to the test result's measurements, but are not + linked to them. Link an item to a specific measurement with + add_measurement_metadata. + + Args: + test_result_id: The ID of the test result. + + software_item_ids: The software item IDs to link. + + hardware_item_ids: The hardware item IDs to link. + + test_adapter_ids: The test adapter IDs to link. + + Returns: + TestResult: The updated test result. + """ + add_request = AddTestResultMetadataRequest( + test_result_id=id_string(test_result_id), + software_item_ids=id_list(software_item_ids, "software_item_ids"), + hardware_item_ids=id_list(hardware_item_ids, "hardware_item_ids"), + test_adapter_ids=id_list(test_adapter_ids, "test_adapter_ids"), + ) + return TestResult.from_protobuf( + self._get_data_store_client().add_test_result_metadata(add_request).test_result + ) + + def remove_test_result_metadata( + self, + test_result_id: str | UUID, + *, + software_item_ids: Iterable[str | UUID] = (), + hardware_item_ids: Iterable[str | UUID] = (), + test_adapter_ids: Iterable[str | UUID] = (), + ) -> TestResult: + """Unlink software items, hardware items, or test adapters from a test result. + + An item cannot be unlinked from a test result while a measurement of that + test result still references it. Unlink it from those measurements first + with remove_measurement_metadata. + + Args: + test_result_id: The ID of the test result. + + software_item_ids: The software item IDs to unlink. + + hardware_item_ids: The hardware item IDs to unlink. + + test_adapter_ids: The test adapter IDs to unlink. + + Returns: + TestResult: The updated test result. + """ + remove_request = RemoveTestResultMetadataRequest( + test_result_id=id_string(test_result_id), + software_item_ids=id_list(software_item_ids, "software_item_ids"), + hardware_item_ids=id_list(hardware_item_ids, "hardware_item_ids"), + test_adapter_ids=id_list(test_adapter_ids, "test_adapter_ids"), + ) + return TestResult.from_protobuf( + self._get_data_store_client().remove_test_result_metadata(remove_request).test_result + ) + + def add_measurement_metadata( + self, + measurement_id: str | UUID, + *, + software_item_ids: Iterable[str | UUID] = (), + hardware_item_ids: Iterable[str | UUID] = (), + test_adapter_ids: Iterable[str | UUID] = (), + ) -> Measurement: + """Link software items, hardware items, or test adapters to a measurement. + + Each item must already be linked to the measurement's owning test result. + + Args: + measurement_id: The ID of the measurement. + + software_item_ids: The software item IDs to link. + + hardware_item_ids: The hardware item IDs to link. + + test_adapter_ids: The test adapter IDs to link. + + Returns: + Measurement: The updated measurement. + """ + add_request = AddMeasurementMetadataRequest( + measurement_id=id_string(measurement_id), + software_item_ids=id_list(software_item_ids, "software_item_ids"), + hardware_item_ids=id_list(hardware_item_ids, "hardware_item_ids"), + test_adapter_ids=id_list(test_adapter_ids, "test_adapter_ids"), + ) + return Measurement.from_protobuf( + self._get_data_store_client().add_measurement_metadata(add_request).measurement + ) + + def remove_measurement_metadata( + self, + measurement_id: str | UUID, + *, + software_item_ids: Iterable[str | UUID] = (), + hardware_item_ids: Iterable[str | UUID] = (), + test_adapter_ids: Iterable[str | UUID] = (), + ) -> Measurement: + """Unlink software items, hardware items, or test adapters from a measurement. + + Args: + measurement_id: The ID of the measurement. + + software_item_ids: The software item IDs to unlink. + + hardware_item_ids: The hardware item IDs to unlink. + + test_adapter_ids: The test adapter IDs to unlink. + + Returns: + Measurement: The updated measurement. + """ + remove_request = RemoveMeasurementMetadataRequest( + measurement_id=id_string(measurement_id), + software_item_ids=id_list(software_item_ids, "software_item_ids"), + hardware_item_ids=id_list(hardware_item_ids, "hardware_item_ids"), + test_adapter_ids=id_list(test_adapter_ids, "test_adapter_ids"), + ) + return Measurement.from_protobuf( + self._get_data_store_client().remove_measurement_metadata(remove_request).measurement + ) + + def delete_test_result( + self, test_result: TestResult | str | UUID, /, *, force: bool = False + ) -> None: + """Delete a test result. + + Args: + test_result: The test result to delete, or its ID. + + force: If True, also delete the steps, measurements, and conditions that the + test result owns. If False, the service rejects the delete with + FAILED_PRECONDITION when owned children exist. + """ + delete_request = DeleteTestResultRequest( + test_result_id=_resource_id(test_result), force=force + ) + self._get_data_store_client().delete_test_result(delete_request) + + def delete_step(self, step: Step | str | UUID, /, *, force: bool = False) -> None: + """Delete a step. + + A step that has child steps cannot be deleted. + + Args: + step: The step to delete, or its ID. + + force: If True, also delete the measurements of the step. If False, the + service rejects the delete with FAILED_PRECONDITION when measurements exist. + """ + delete_request = DeleteStepRequest(step_id=_resource_id(step), force=force) + self._get_data_store_client().delete_step(delete_request) + + def delete_measurement(self, measurement: Measurement | str | UUID, /) -> None: + """Delete a measurement. + + Args: + measurement: The measurement to delete, or its ID. + """ + delete_request = DeleteMeasurementRequest(measurement_id=_resource_id(measurement)) + self._get_data_store_client().delete_measurement(delete_request) + + def batch_delete_test_results( + self, + test_results: Iterable[TestResult | str | UUID], + *, + force: bool | Iterable[bool] = False, + ) -> None: + """Delete multiple test results in one atomic call. + + Args: + test_results: The test results to delete, or their IDs. + + force: One flag for all test results, or one flag per test result. + See delete_test_result for details on force behavior. + + Raises: + ValueError: If one flag per item is given and the counts differ. + """ + batch_request = BatchDeleteTestResultsRequest( + requests=[ + DeleteTestResultRequest(test_result_id=test_result_id, force=item_force) + for test_result_id, item_force in _with_force(test_results, force, "test_results") + ] + ) + self._get_data_store_client().batch_delete_test_results(batch_request) + + def batch_delete_steps( + self, steps: Iterable[Step | str | UUID], *, force: bool | Iterable[bool] = False + ) -> None: + """Delete multiple steps in one atomic call. + + Args: + steps: The steps to delete, or their IDs. A step and its ancestor or + descendant cannot be deleted in the same call. + + force: One flag for all steps, or one flag per step. See delete_step + for details on force behavior. + + Raises: + ValueError: If one flag per item is given and the counts differ. + """ + batch_request = BatchDeleteStepsRequest( + requests=[ + DeleteStepRequest(step_id=step_id, force=item_force) + for step_id, item_force in _with_force(steps, force, "steps") + ] + ) + self._get_data_store_client().batch_delete_steps(batch_request) + + def batch_delete_measurements(self, measurements: Iterable[Measurement | str | UUID]) -> None: + """Delete multiple measurements in one atomic call. + + Args: + measurements: The measurements to delete, or their IDs. + """ + batch_request = BatchDeleteMeasurementsRequest( + requests=[ + DeleteMeasurementRequest(measurement_id=measurement_id) + for measurement_id in _resource_ids(measurements, "measurements") + ] + ) + self._get_data_store_client().batch_delete_measurements(batch_request) + + def _get_data_store_client(self) -> DataStoreServiceClient: + if self._closed: + raise RuntimeError(self._DATA_STORE_CLIENT_CLOSED_ERROR) + return self._data_store_client + + +def _read_mask(fields: Iterable[str]) -> FieldMask | None: + # Send the field name of an enum member, not its str() representation. + paths = [ + str(field.value) if isinstance(field, Enum) else field + for field in string_list(fields, "read_fields") + ] + return FieldMask(paths=paths) if paths else None + + +def _with_force( + resources: Iterable[TestResult | Step | str | UUID], force: bool | Iterable[bool], name: str +) -> list[tuple[str, bool]]: + id_list = _resource_ids(resources, name) + if isinstance(force, bool): + return [(resource_id, force) for resource_id in id_list] + force_list = list(force) + if len(force_list) != len(id_list): + raise ValueError( + f"Expected {len(id_list)} force flags, one per ID, but got {len(force_list)}." + ) + return list(zip(id_list, force_list)) + + +def _update_items( + updates: Mapping[str, Mapping[str, Any]] | Mapping[UUID, Mapping[str, Any]] | Iterable[_T], +) -> list[tuple[_T | str | UUID, Mapping[str, Any]]]: + items: list[tuple[_T | str | UUID, Mapping[str, Any]]] + if isinstance(updates, Mapping): + items = [(resource_id, fields) for resource_id, fields in updates.items()] + else: + items = [(target, {}) for target in updates] + if not items: + raise ValueError("At least one update must be supplied for a batch update.") + return items + + +def _test_result_update( + target: TestResult | TestResultUpdateBuilder | str | UUID, fields: Mapping[str, Any] +) -> UpdateTestResultRequest: + if isinstance(target, TestResultUpdateBuilder): + _reject_builder_fields(fields) + return target.build_update() + if isinstance(target, TestResult) and not fields: + fields = resource_update_fields( + target, + TestResultProto.DESCRIPTOR, + TEST_RESULT_UPDATE_FIELDS, + TEST_RESULT_RELATIONSHIPS, + ) + test_result = TestResultProto(id=_resource_id(target)) + mask = populate_update(test_result, fields, TEST_RESULT_UPDATE_FIELDS) + return UpdateTestResultRequest(test_result=test_result, update_mask=mask) + + +def _step_update( + target: Step | StepUpdateBuilder | str | UUID, fields: Mapping[str, Any] +) -> UpdateStepRequest: + if isinstance(target, StepUpdateBuilder): + _reject_builder_fields(fields) + return target.build_update() + if isinstance(target, Step) and not fields: + fields = resource_update_fields( + target, StepProto.DESCRIPTOR, STEP_UPDATE_FIELDS, STEP_RELATIONSHIPS + ) + step = StepProto(id=_resource_id(target)) + mask = populate_update(step, fields, STEP_UPDATE_FIELDS) + return UpdateStepRequest(step=step, update_mask=mask) + + +def _measurement_update( + target: Measurement | MeasurementUpdateBuilder | str | UUID, fields: Mapping[str, Any] +) -> UpdateMeasurementRequest: + if isinstance(target, MeasurementUpdateBuilder): + _reject_builder_fields(fields) + return target.build_update() + if isinstance(target, Measurement) and not fields: + fields = resource_update_fields( + target, MeasurementProto.DESCRIPTOR, MEASUREMENT_UPDATE_FIELDS, {} + ) + measurement = MeasurementProto(id=_resource_id(target)) + mask = populate_update(measurement, fields, MEASUREMENT_UPDATE_FIELDS) + return UpdateMeasurementRequest(measurement=measurement, update_mask=mask) + + +def _resource_id(target: TestResult | Step | Measurement | str | UUID) -> str: + return id_string(target) if isinstance(target, (str, UUID)) else target.id + + +def _reject_builder_fields(fields: Mapping[str, Any]) -> None: + if fields: + raise TypeError( + "Keyword fields cannot be combined with an update builder. " + "Select the fields on the builder instead." + ) + + +def _resource_ids( + resources: Iterable[TestResult | Step | Measurement | str | UUID], name: str +) -> list[str]: + # A str is itself an iterable of one-character strings. + if isinstance(resources, (str, UUID)): + raise TypeError( + f"{name} must be a list or other iterable of resources or IDs, " + f"not a single {type(resources).__name__}." + ) + return [_resource_id(resource) for resource in resources] + + +def _reject_model_fields(fields: Mapping[Any, Any], model: str, resource_name: str) -> None: + if fields: + raise TypeError( + f"Pass either {model} or the {resource_name} fields as arguments, not both." + ) + + +def _measurement_from_fields( + name: str, value: MeasurementValue, step_id: str | UUID, **fields: Any +) -> Measurement: + return Measurement(name=name, value=value, step_id=step_id, **fields) diff --git a/src/ni/datastore/data/v2/_grpc_conversion.py b/src/ni/datastore/data/v2/_grpc_conversion.py new file mode 100644 index 0000000..4868559 --- /dev/null +++ b/src/ni/datastore/data/v2/_grpc_conversion.py @@ -0,0 +1,246 @@ +"""Conversion helpers for V2 measurement values.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping +from typing import Any, cast +from uuid import UUID + +import hightime as ht +import numpy as np +from google.protobuf import json_format +from google.protobuf.message import Message +from google.protobuf.struct_pb2 import Struct +from ni.measurements.data.v2.data_store_pb2 import MeasurementValue as MeasurementValueProto +from ni.protobuf.types.precision_duration_conversion import ( + hightime_timedelta_from_protobuf, + hightime_timedelta_to_protobuf, +) +from ni.protobuf.types.precision_duration_pb2 import PrecisionDuration +from ni.protobuf.types.precision_timestamp_conversion import ( + hightime_datetime_from_protobuf, + hightime_datetime_to_protobuf, +) +from ni.protobuf.types.precision_timestamp_pb2 import PrecisionTimestamp +from ni.protobuf.types.scalar_conversion import scalar_from_protobuf, scalar_to_protobuf +from ni.protobuf.types.vector_conversion import vector_from_protobuf, vector_to_protobuf +from ni.protobuf.types.waveform_conversion import ( + digital_waveform_from_protobuf, + digital_waveform_to_protobuf, + float32_analog_waveform_from_protobuf, + float32_analog_waveform_to_protobuf, + float32_complex_waveform_from_protobuf, + float32_complex_waveform_to_protobuf, + float32_spectrum_from_protobuf, + float32_spectrum_to_protobuf, + float64_analog_waveform_from_protobuf, + float64_analog_waveform_to_protobuf, + float64_complex_waveform_from_protobuf, + float64_complex_waveform_to_protobuf, + float64_spectrum_from_protobuf, + float64_spectrum_to_protobuf, + int16_analog_waveform_from_protobuf, + int16_analog_waveform_to_protobuf, + int16_complex_waveform_from_protobuf, + int16_complex_waveform_to_protobuf, +) +from ni.protobuf.types.xydata_conversion import ( + float64_xydata_from_protobuf, + float64_xydata_to_protobuf, +) +from nitypes.complex import ComplexInt32DType +from nitypes.scalar import Scalar +from nitypes.vector import Vector +from nitypes.waveform import AnalogWaveform, ComplexWaveform, DigitalWaveform, Spectrum +from nitypes.xy_data import XYData + +from ni.datastore.data.v2._types._json_value import JsonValue, json_value_from_protobuf +from ni.datastore.data.v2._types._measurement_value import MeasurementReadValue + + +def populate_measurement_value(measurement_value: MeasurementValueProto, value: object) -> None: + """Convert a supported Python value into a V2 measurement value.""" + if isinstance(value, bool): + measurement_value.scalar.bool_value = value + elif isinstance(value, int): + measurement_value.scalar.sint32_value = value + elif isinstance(value, float): + measurement_value.scalar.double_value = value + elif isinstance(value, str): + measurement_value.scalar.string_value = value + elif isinstance(value, Scalar): + measurement_value.scalar.CopyFrom(scalar_to_protobuf(value)) + elif isinstance(value, Vector): + measurement_value.vector.CopyFrom(vector_to_protobuf(value)) + elif isinstance(value, AnalogWaveform): + if value.dtype == np.float64: + measurement_value.double_analog_waveform.CopyFrom( + float64_analog_waveform_to_protobuf(value) + ) + elif value.dtype == np.float32: + measurement_value.float_analog_waveform.CopyFrom( + float32_analog_waveform_to_protobuf(value) + ) + elif value.dtype == np.int16: + measurement_value.i16_analog_waveform.CopyFrom(int16_analog_waveform_to_protobuf(value)) + else: + raise TypeError(f"Unsupported AnalogWaveform dtype: {value.dtype}") + elif isinstance(value, ComplexWaveform): + if value.dtype == np.complex128: + measurement_value.double_complex_waveform.CopyFrom( + float64_complex_waveform_to_protobuf(value) + ) + elif value.dtype == np.complex64: + measurement_value.float_complex_waveform.CopyFrom( + float32_complex_waveform_to_protobuf(value) + ) + elif value.dtype == ComplexInt32DType: + measurement_value.i16_complex_waveform.CopyFrom( + int16_complex_waveform_to_protobuf(value) + ) + else: + raise TypeError(f"Unsupported ComplexWaveform dtype: {value.dtype}") + elif isinstance(value, Spectrum): + if value.dtype == np.float64: + measurement_value.double_spectrum.CopyFrom(float64_spectrum_to_protobuf(value)) + elif value.dtype == np.float32: + measurement_value.float_spectrum.CopyFrom(float32_spectrum_to_protobuf(value)) + else: + raise TypeError(f"Unsupported Spectrum dtype: {value.dtype}") + elif isinstance(value, DigitalWaveform): + measurement_value.digital_waveform.CopyFrom(digital_waveform_to_protobuf(value)) + elif isinstance(value, XYData): + if value.dtype != np.float64: + raise TypeError(f"Unsupported XYData dtype: {value.dtype}") + measurement_value.x_y_data.CopyFrom(float64_xydata_to_protobuf(value)) + elif isinstance(value, JsonValue): + measurement_value.json_value.CopyFrom(value.to_protobuf()) + elif isinstance(value, Mapping): + json_format.ParseDict(cast(dict[str, Any], dict(value)), measurement_value.json_value.value) + elif isinstance(value, Iterable): + try: + vector = Vector(cast(Iterable[bool | int | float | str], value)) + except (TypeError, ValueError) as exc: + raise TypeError( + "Iterable measurement values must contain only bool, int, float, or str values." + ) from exc + measurement_value.vector.CopyFrom(vector_to_protobuf(vector)) + else: + raise TypeError( + f"Unsupported measurement value type: {type(value)}. Please consult the documentation." + ) + + +def convert_measurement_value_from_protobuf( + measurement_value: MeasurementValueProto, +) -> MeasurementReadValue: + """Convert a V2 measurement value into a useful Python value.""" + value_type = measurement_value.WhichOneof("value") + if value_type == "scalar": + return scalar_from_protobuf(measurement_value.scalar) + if value_type == "vector": + return vector_from_protobuf(measurement_value.vector) + if value_type == "double_analog_waveform": + return float64_analog_waveform_from_protobuf(measurement_value.double_analog_waveform) + if value_type == "float_analog_waveform": + return float32_analog_waveform_from_protobuf(measurement_value.float_analog_waveform) + if value_type == "i16_analog_waveform": + return int16_analog_waveform_from_protobuf(measurement_value.i16_analog_waveform) + if value_type == "double_complex_waveform": + return float64_complex_waveform_from_protobuf(measurement_value.double_complex_waveform) + if value_type == "float_complex_waveform": + return float32_complex_waveform_from_protobuf(measurement_value.float_complex_waveform) + if value_type == "i16_complex_waveform": + return int16_complex_waveform_from_protobuf(measurement_value.i16_complex_waveform) + if value_type == "double_spectrum": + return float64_spectrum_from_protobuf(measurement_value.double_spectrum) + if value_type == "float_spectrum": + return float32_spectrum_from_protobuf(measurement_value.float_spectrum) + if value_type == "digital_waveform": + return digital_waveform_from_protobuf(measurement_value.digital_waveform) + if value_type == "x_y_data": + return float64_xydata_from_protobuf(measurement_value.x_y_data) + if value_type == "json_value": + return json_value_from_protobuf(measurement_value.json_value) + raise TypeError(f"Unsupported or missing measurement value type: {value_type}.") + + +def measurement_value_to_protobuf(value: object) -> MeasurementValueProto | None: + """Convert an optional measurement value, upper limit, or lower limit to protobuf.""" + if value is None: + return None + measurement_value = MeasurementValueProto() + populate_measurement_value(measurement_value, value) + return measurement_value + + +def measurement_value_from_protobuf(message: Message, field: str) -> MeasurementReadValue | None: + """Convert an optional MeasurementValue field, or return None when it has no inline value.""" + if not message.HasField(field): + return None + measurement_value = cast(MeasurementValueProto, getattr(message, field)) + # Non-scalar payloads may be returned only as a data_moniker reference. + if measurement_value.WhichOneof("value") in (None, "data_moniker"): + return None + return convert_measurement_value_from_protobuf(measurement_value) + + +def timestamp_to_protobuf(value: ht.datetime | None) -> PrecisionTimestamp | None: + """Convert an optional hightime datetime to a PrecisionTimestamp.""" + return hightime_datetime_to_protobuf(value) if value is not None else None + + +def timestamp_from_protobuf(message: Message, field: str) -> ht.datetime | None: + """Convert an optional PrecisionTimestamp field to a hightime datetime.""" + if not message.HasField(field): + return None + return hightime_datetime_from_protobuf(getattr(message, field)) + + +def duration_to_protobuf(value: ht.timedelta | None) -> PrecisionDuration | None: + """Convert an optional hightime timedelta to a PrecisionDuration.""" + return hightime_timedelta_to_protobuf(value) if value is not None else None + + +def duration_from_protobuf(message: Message, field: str) -> ht.timedelta | None: + """Convert an optional PrecisionDuration field to a hightime timedelta.""" + if not message.HasField(field): + return None + return hightime_timedelta_from_protobuf(getattr(message, field)) + + +def extension_to_protobuf(value: Mapping[str, Any]) -> Struct | None: + """Convert an extension mapping to a protobuf Struct, or None when empty.""" + if not value: + return None + struct = Struct() + json_format.ParseDict(dict(value), struct) + return struct + + +def extension_from_protobuf(value: Struct) -> dict[str, Any]: + """Convert a protobuf Struct extension to a Python dictionary.""" + return json_format.MessageToDict(value) + + +def string_list(values: Iterable[str] | None, name: str) -> list[str]: + """Convert an iterable of strings to a list, rejecting a single string.""" + # A str is itself an iterable of one-character strings, so list("smoke") would + # silently become ["s", "m", "o", "k", "e"]. + if isinstance(values, str): + raise TypeError(f"{name} must be a list or other iterable of strings, not a single str.") + return list(values) if values is not None else [] + + +def id_string(value: str | UUID) -> str: + """Convert a resource or metadata ID to its string form.""" + return str(value) if isinstance(value, UUID) else value + + +def id_list(values: Iterable[str | UUID] | None, name: str) -> list[str]: + """Convert an iterable of IDs to a list of strings, rejecting a single ID.""" + if isinstance(values, (str, UUID)): + raise TypeError( + f"{name} must be a list or other iterable of IDs, not a single {type(values).__name__}." + ) + return [id_string(value) for value in values] if values is not None else [] diff --git a/src/ni/datastore/data/v2/_types/__init__.py b/src/ni/datastore/data/v2/_types/__init__.py new file mode 100644 index 0000000..78643dc --- /dev/null +++ b/src/ni/datastore/data/v2/_types/__init__.py @@ -0,0 +1 @@ +"""Types for use in accessing the NI Data Store V2.""" diff --git a/src/ni/datastore/data/v2/_types/_condition.py b/src/ni/datastore/data/v2/_types/_condition.py new file mode 100644 index 0000000..c94a044 --- /dev/null +++ b/src/ni/datastore/data/v2/_types/_condition.py @@ -0,0 +1,145 @@ +"""Condition data type for the V2 Data Store Client.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +from google.protobuf import json_format +from ni.measurements.data.v2.data_store_pb2 import Condition as ConditionProto +from ni.protobuf.types.scalar_conversion import scalar_from_protobuf, scalar_to_protobuf +from nitypes.scalar import Scalar + +from ni.datastore.data.v2._types._json_value import JsonValue, json_value_from_protobuf + +_UNIT_DESCRIPTION = "NI_UnitDescription" + + +class Condition: + """An operational condition, such as a temperature or supply voltage, owned by a step. + + Conditions are immutable. Adding a condition with the same name to a step + replaces the step's association with a new condition value. + """ + + __slots__ = ( + "name", + "_value", + "condition_type", + "value_label", + "units", + ) + + def __init__( + self, + name: str, + value: object, + *, + condition_type: str = "", + value_label: str = "", + units: str = "", + ) -> None: + """Initialize a Condition instance. + + Args: + name: The condition name, unique within a step. + value: A bool, int, float, str, or Scalar value, a JSON mapping, or a + JsonValue. A scalar value reads back as a plain Python value, or as + a Scalar when it has extended properties other than units. + condition_type (optional): The type of the condition. For example, + "Environment" or "Setup". + value_label (optional): A label for the value. + units (optional): The engineering units of the value. Defaults to the + units of a Scalar value. + """ + self.name = name + self.condition_type = condition_type + self.value_label = value_label + self.units = units + self.value = value + + @property + def value(self) -> object: + """The condition value. + + Setting a Scalar value also sets units to the Scalar units when units is empty. + """ + return self._value + + @value.setter + def value(self, value: object) -> None: + self._value = value + if isinstance(value, Scalar) and not self.units: + self.units = value.units + + @staticmethod + def from_protobuf(condition_proto: ConditionProto) -> "Condition": + """Create a Condition instance from a protobuf Condition message.""" + value_kind = condition_proto.value.WhichOneof("value") + value: object = None + if value_kind == "scalar": + scalar = scalar_from_protobuf(condition_proto.value.scalar) + # Keep the Scalar only when it carries properties a plain value cannot. + has_extra_properties = any( + key != _UNIT_DESCRIPTION for key in scalar.extended_properties + ) + value = scalar if has_extra_properties else scalar.value + elif value_kind == "json_value": + value = json_value_from_protobuf(condition_proto.value.json_value) + return Condition( + name=condition_proto.name, + value=value, + condition_type=condition_proto.condition_type, + value_label=condition_proto.value_label, + units=condition_proto.units, + ) + + def to_protobuf(self) -> ConditionProto: + """Convert this Condition instance to a protobuf Condition message.""" + condition_proto = ConditionProto( + name=self.name, + condition_type=self.condition_type, + value_label=self.value_label, + units=self.units, + ) + value = self.value + if isinstance(value, bool): + condition_proto.value.scalar.bool_value = value + elif isinstance(value, int): + condition_proto.value.scalar.sint32_value = value + elif isinstance(value, float): + condition_proto.value.scalar.double_value = value + elif isinstance(value, str): + condition_proto.value.scalar.string_value = value + elif isinstance(value, Scalar): + condition_proto.value.scalar.CopyFrom(scalar_to_protobuf(value)) + if not self.units: + condition_proto.units = value.units + elif isinstance(value, JsonValue): + condition_proto.value.json_value.CopyFrom(value.to_protobuf()) + elif isinstance(value, Mapping): + json_format.ParseDict( + cast(dict[str, Any], dict(value)), condition_proto.value.json_value.value + ) + else: + raise TypeError( + "Condition values must be bool, int, float, str, Scalar, a JSON mapping, " + "or a JsonValue." + ) + return condition_proto + + def __eq__(self, other: object) -> bool: + """Determine equality.""" + if not isinstance(other, Condition): + return NotImplemented + return ( + self.name == other.name + and self.value == other.value + and self.condition_type == other.condition_type + and self.value_label == other.value_label + and self.units == other.units + ) + + def __str__(self) -> str: + """Return a string representation of the Condition.""" + return str(self.to_protobuf()) diff --git a/src/ni/datastore/data/v2/_types/_create.py b/src/ni/datastore/data/v2/_types/_create.py new file mode 100644 index 0000000..1f6c62f --- /dev/null +++ b/src/ni/datastore/data/v2/_types/_create.py @@ -0,0 +1,91 @@ +"""Typed create fields for the V2 Data Store Client.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping +from typing import Any, TypedDict +from uuid import UUID + +import hightime as ht + +from ni.datastore.data.v2._types._condition import Condition +from ni.datastore.data.v2._types._error_information import ErrorInformation +from ni.datastore.data.v2._types._measurement import Measurement +from ni.datastore.data.v2._types._outcome import Outcome +from ni.datastore.data.v2._types._status import Status +from ni.datastore.data.v2._types._step import Step +from ni.datastore.metadata import ( + HardwareItem, + Operator, + SoftwareItem, + Test, + TestAdapter, + TestDescription, + TestStation, + Uut, + UutInstance, +) + + +class TestResultCreateFields(TypedDict, total=False): + """The test result fields that create_test_result accepts as keyword arguments. + + Each field has the same meaning and default as the matching TestResult + constructor argument. + """ + + uut_instance_id: str | UUID + uut_id: str | UUID + operator_id: str | UUID + test_station_id: str | UUID + test_description_id: str | UUID + uut_instance: UutInstance | None + uut: Uut | None + operator: Operator | None + test_station: TestStation | None + test_description: TestDescription | None + software_item_ids: Iterable[str | UUID] | None + hardware_item_ids: Iterable[str | UUID] | None + test_adapter_ids: Iterable[str | UUID] | None + software_items: Iterable[SoftwareItem] | None + hardware_items: Iterable[HardwareItem] | None + test_adapters: Iterable[TestAdapter] | None + start_date_time: ht.datetime | None + end_date_time: ht.datetime | None + duration: ht.timedelta | None + outcome: Outcome + status: Status + link: str + keywords: Iterable[str] | None + extension: Mapping[str, Any] | None + schema_id: str + error_information: ErrorInformation | None + steps: Iterable[Step] | None + + +class StepCreateFields(TypedDict, total=False): + """The step fields that create_step accepts as keyword arguments. + + Each field has the same meaning and default as the matching Step + constructor argument. + """ + + parent_step_id: str | UUID + test_result_id: str | UUID + test_id: str | UUID + test: Test | None + step_type: str + notes: str + start_date_time: ht.datetime | None + end_date_time: ht.datetime | None + duration: ht.timedelta | None + outcome: Outcome + status: Status + link: str + keywords: Iterable[str] | None + extension: Mapping[str, Any] | None + schema_id: str + error_information: ErrorInformation | None + conditions: Iterable[Condition] | None + measurements: Iterable[Measurement] | None + children: Iterable[Step] | None diff --git a/src/ni/datastore/data/v2/_types/_error_information.py b/src/ni/datastore/data/v2/_types/_error_information.py new file mode 100644 index 0000000..a7be544 --- /dev/null +++ b/src/ni/datastore/data/v2/_types/_error_information.py @@ -0,0 +1,66 @@ +"""Error Information data type for the V2 Data Store Client.""" + +from __future__ import annotations + +from ni.measurements.data.v2.data_store_pb2 import ( + ErrorInformation as ErrorInformationProto, +) + + +class ErrorInformation: + """Represents error details reported for a test result, step, or measurement.""" + + __slots__ = ( + "error_code", + "message", + "source", + ) + + def __init__( + self, + *, + error_code: int = 0, + message: str = "", + source: str = "", + ) -> None: + """Initialize an ErrorInformation instance. + + Args: + error_code: The numeric error code associated with the error. + message: A descriptive message explaining the error. + source: The source or location where the error occurred. + """ + self.error_code = error_code + self.message = message + self.source = source + + @staticmethod + def from_protobuf(error_information_proto: ErrorInformationProto) -> "ErrorInformation": + """Create an ErrorInformation instance from a protobuf ErrorInformation message.""" + return ErrorInformation( + error_code=error_information_proto.error_code, + message=error_information_proto.message, + source=error_information_proto.source, + ) + + def to_protobuf(self) -> ErrorInformationProto: + """Convert this ErrorInformation instance to a protobuf ErrorInformation message.""" + return ErrorInformationProto( + error_code=self.error_code, + message=self.message, + source=self.source, + ) + + def __eq__(self, other: object) -> bool: + """Determine equality.""" + if not isinstance(other, ErrorInformation): + return NotImplemented + return ( + self.error_code == other.error_code + and self.message == other.message + and self.source == other.source + ) + + def __str__(self) -> str: + """Return a string representation of the ErrorInformation.""" + return str(self.to_protobuf()) diff --git a/src/ni/datastore/data/v2/_types/_json_value.py b/src/ni/datastore/data/v2/_types/_json_value.py new file mode 100644 index 0000000..2b173a8 --- /dev/null +++ b/src/ni/datastore/data/v2/_types/_json_value.py @@ -0,0 +1,107 @@ +"""JSON value data type for the V2 Data Store Client.""" + +from __future__ import annotations + +from collections.abc import Mapping, MutableMapping +from typing import Any, cast + +from google.protobuf import json_format +from ni.measurements.data.v2.data_store_pb2 import JsonValue as JsonValueProto +from ni.protobuf.types.attribute_value_pb2 import AttributeValue + +AttributeValueType = bool | int | float | str + + +class JsonValue: + """Structured JSON used as a measurement value, limit, or condition value. + + A plain dictionary can be used instead when no attributes or schema are needed. + """ + + __slots__ = ("_value", "_attributes", "schema_id") + + @property + def value(self) -> MutableMapping[str, Any]: + """The JSON object.""" + return self._value + + @property + def attributes(self) -> MutableMapping[str, AttributeValueType]: + """Attributes that describe the JSON value.""" + return self._attributes + + def __init__( + self, + value: Mapping[str, Any], + *, + attributes: Mapping[str, AttributeValueType] | None = None, + schema_id: str = "", + ) -> None: + """Initialize a JsonValue instance. + + Args: + value: The JSON object. + attributes (optional): Attributes that describe the JSON value. + schema_id (optional): Schema for the JSON content. Reserved for future use. + """ + self._value: MutableMapping[str, Any] = dict(value) + self._attributes: MutableMapping[str, AttributeValueType] = ( + dict(attributes) if attributes is not None else {} + ) + self.schema_id = schema_id + + @staticmethod + def from_protobuf(json_value_proto: JsonValueProto) -> "JsonValue": + """Create a JsonValue instance from a protobuf JsonValue message.""" + return JsonValue( + json_format.MessageToDict(json_value_proto.value), + attributes={ + key: getattr(attribute, cast(str, attribute.WhichOneof("attribute"))) + for key, attribute in json_value_proto.attributes.items() + }, + schema_id=json_value_proto.schema_id, + ) + + def to_protobuf(self) -> JsonValueProto: + """Convert this JsonValue to a protobuf JsonValue message.""" + json_value_proto = JsonValueProto(schema_id=self.schema_id) + json_format.ParseDict(dict(self.value), json_value_proto.value) + for key, attribute in self.attributes.items(): + json_value_proto.attributes[key].CopyFrom(_attribute_to_protobuf(attribute)) + return json_value_proto + + def __eq__(self, other: object) -> bool: + """Determine equality.""" + if not isinstance(other, JsonValue): + return NotImplemented + return ( + self.value == other.value + and self.attributes == other.attributes + and self.schema_id == other.schema_id + ) + + def __repr__(self) -> str: + """Return a representation of the JsonValue.""" + return ( + f"JsonValue({dict(self.value)!r}, attributes={dict(self.attributes)!r}, " + f"schema_id={self.schema_id!r})" + ) + + +def json_value_from_protobuf(json_value_proto: JsonValueProto) -> dict[str, Any] | JsonValue: + """Return a plain dictionary, or a JsonValue when attributes or a schema are present.""" + if json_value_proto.attributes or json_value_proto.schema_id: + return JsonValue.from_protobuf(json_value_proto) + return json_format.MessageToDict(json_value_proto.value) + + +def _attribute_to_protobuf(value: AttributeValueType) -> AttributeValue: + if isinstance(value, bool): + return AttributeValue(bool_value=value) + if isinstance(value, int): + return AttributeValue(integer_value=value) + if isinstance(value, float): + return AttributeValue(double_value=value) + if isinstance(value, str): + return AttributeValue(string_value=value) + raise TypeError(f"JsonValue attributes must be bool, int, float, or str, not {type(value)}.") diff --git a/src/ni/datastore/data/v2/_types/_measurement.py b/src/ni/datastore/data/v2/_types/_measurement.py new file mode 100644 index 0000000..2cb45bb --- /dev/null +++ b/src/ni/datastore/data/v2/_types/_measurement.py @@ -0,0 +1,383 @@ +"""Measurement data type for the V2 Data Store Client.""" + +from __future__ import annotations + +from typing import Any, Iterable, Mapping, MutableMapping, MutableSequence, Sequence +from uuid import UUID + +import hightime as ht +from ni.measurements.data.v2.data_store_pb2 import Measurement as MeasurementProto +from nitypes.scalar import Scalar + +from ni.datastore.data.v2._grpc_conversion import ( + duration_from_protobuf, + duration_to_protobuf, + extension_from_protobuf, + extension_to_protobuf, + id_list, + id_string, + measurement_value_from_protobuf, + measurement_value_to_protobuf, + timestamp_from_protobuf, + timestamp_to_protobuf, +) +from ni.datastore.data.v2._types._condition import Condition +from ni.datastore.data.v2._types._error_information import ErrorInformation +from ni.datastore.data.v2._types._measurement_value import MeasurementValue +from ni.datastore.data.v2._types._outcome import Outcome +from ni.datastore.metadata import HardwareItem, SoftwareItem, TestAdapter + + +class Measurement: + """A measurement stored in the data store. + + The value and limits are sent on create. Reads return them only when the + service includes an inline value; use DataStoreClient.read_measurement_value + or DataStoreClient.read_measurement_values to read them reliably. + """ + + __slots__ = ( + "id", + "test_result_id", + "step_id", + "name", + "_value", + "upper_limit", + "lower_limit", + "value_type", + "units", + "notes", + "start_date_time", + "end_date_time", + "duration", + "outcome", + "error_information", + "_extension", + "schema_id", + "_software_item_ids", + "_hardware_item_ids", + "_test_adapter_ids", + "_software_items", + "_hardware_items", + "_test_adapters", + "comparison_type", + "_conditions", + "created_at", + "updated_at", + "created_by", + "updated_by", + ) + + units: str + + @property + def extension(self) -> MutableMapping[str, Any]: + """Custom JSON data associated with the measurement.""" + return self._extension + + @property + def software_item_ids(self) -> MutableSequence[str]: + """The software item IDs associated with the measurement.""" + return self._software_item_ids + + @property + def hardware_item_ids(self) -> MutableSequence[str]: + """The hardware item IDs associated with the measurement.""" + return self._hardware_item_ids + + @property + def test_adapter_ids(self) -> MutableSequence[str]: + """The test adapter IDs associated with the measurement.""" + return self._test_adapter_ids + + @property + def software_items(self) -> MutableSequence[SoftwareItem]: + """Software items to find or create on create, or expand on read.""" + return self._software_items + + @property + def hardware_items(self) -> MutableSequence[HardwareItem]: + """Hardware items to find or create on create, or expand on read.""" + return self._hardware_items + + @property + def test_adapters(self) -> MutableSequence[TestAdapter]: + """Test adapters to find or create on create, or expand on read.""" + return self._test_adapters + + @property + def conditions(self) -> Sequence[Condition]: + """Read-only output-only conditions copied from the owning step.""" + return self._conditions + + @property + def value(self) -> MeasurementValue | None: + """The measured value. + + Setting a Scalar value also sets units to the Scalar units when units is empty. + """ + return self._value + + @value.setter + def value(self, value: MeasurementValue | None) -> None: + self._value = value + if isinstance(value, Scalar) and not self.units: + self.units = value.units + + def __init__( + self, + *, + id: str | UUID = "", + test_result_id: str | UUID = "", + step_id: str | UUID = "", + name: str = "", + value: MeasurementValue | None = None, + upper_limit: MeasurementValue | None = None, + lower_limit: MeasurementValue | None = None, + value_type: str = "", + units: str = "", + notes: str = "", + start_date_time: ht.datetime | None = None, + end_date_time: ht.datetime | None = None, + duration: ht.timedelta | None = None, + outcome: Outcome = Outcome.UNSPECIFIED, + error_information: ErrorInformation | None = None, + extension: Mapping[str, Any] | None = None, + schema_id: str = "", + software_item_ids: Iterable[str | UUID] | None = None, + hardware_item_ids: Iterable[str | UUID] | None = None, + test_adapter_ids: Iterable[str | UUID] | None = None, + software_items: Iterable[SoftwareItem] | None = None, + hardware_items: Iterable[HardwareItem] | None = None, + test_adapters: Iterable[TestAdapter] | None = None, + comparison_type: str = "", + conditions: Iterable[Condition] | None = None, + created_at: ht.datetime | None = None, + updated_at: ht.datetime | None = None, + created_by: str = "", + updated_by: str = "", + ) -> None: + """Initialize a Measurement instance. + + Output-only fields are accepted so a Measurement can represent a service + response. They are not included in protobuf requests. Update conditions by + calling DataStoreClient.add_condition or remove_condition on the owning step. + + Args: + id (optional): Unique identifier of the measurement. + test_result_id (optional): ID of the test result the measurement belongs to. + step_id (optional): ID of the step the measurement belongs to. + name (optional): The name of the measurement. + value (optional): The measured value. See DataStoreClient.create_measurement + for the supported types. + upper_limit (optional): The publisher-supplied upper limit. Any value type + supported for value is accepted. The service stores it but does not + compare it with the value. + lower_limit (optional): The publisher-supplied lower limit. Any value type + supported for value is accepted. The service stores it but does not + compare it with the value. + value_type (optional): Output only. The service-generated value type. + units (optional): Engineering units of the value. Defaults to the units of a + Scalar value. + notes (optional): Additional notes about the measurement. + start_date_time (optional): The start date and time of the measurement. + end_date_time (optional): The end date and time of the measurement. + duration (optional): The duration of the measurement. + outcome (optional): The outcome of the measurement. + error_information (optional): Error details for the measurement. + extension (optional): Custom JSON data for the measurement. + schema_id (optional): Schema that governs the extension. Reserved for + future use. + software_item_ids (optional): IDs of software items used for the measurement. + hardware_item_ids (optional): IDs of hardware items used for the measurement. + test_adapter_ids (optional): IDs of test adapters used for the measurement. + software_items (optional): Software items to find or create on create. Each + item must already be linked to the owning test result. + hardware_items (optional): Hardware items to find or create on create. Each + item must already be linked to the owning test result. + test_adapters (optional): Test adapters to find or create on create. Each + item must already be linked to the owning test result. + comparison_type (optional): The publisher-defined comparison operation. + conditions (optional): Output only. Read-only conditions copied from the + owning step. + created_at (optional): Output only. When the measurement was created. + updated_at (optional): Output only. When the measurement was last updated. + created_by (optional): Output only. Who created the measurement. + updated_by (optional): Output only. Who last updated the measurement. + """ + self.id = id_string(id) + self.test_result_id = id_string(test_result_id) + self.step_id = id_string(step_id) + self.name = name + self.units = units + self.value = value + self.upper_limit = upper_limit + self.lower_limit = lower_limit + self.value_type = value_type + self.notes = notes + self.start_date_time = start_date_time + self.end_date_time = end_date_time + self.duration = duration + self.outcome = outcome + self.error_information = error_information + self._extension: MutableMapping[str, Any] = dict(extension) if extension is not None else {} + self.schema_id = schema_id + self._software_item_ids: MutableSequence[str] = id_list( + software_item_ids, "software_item_ids" + ) + self._hardware_item_ids: MutableSequence[str] = id_list( + hardware_item_ids, "hardware_item_ids" + ) + self._test_adapter_ids: MutableSequence[str] = id_list(test_adapter_ids, "test_adapter_ids") + self._software_items: MutableSequence[SoftwareItem] = ( + list(software_items) if software_items is not None else [] + ) + self._hardware_items: MutableSequence[HardwareItem] = ( + list(hardware_items) if hardware_items is not None else [] + ) + self._test_adapters: MutableSequence[TestAdapter] = ( + list(test_adapters) if test_adapters is not None else [] + ) + self.comparison_type = comparison_type + self._conditions: tuple[Condition, ...] = ( + tuple(conditions) if conditions is not None else () + ) + self.created_at = created_at + self.updated_at = updated_at + self.created_by = created_by + self.updated_by = updated_by + + @staticmethod + def from_protobuf(measurement_proto: MeasurementProto) -> "Measurement": + """Create a Measurement instance from a protobuf Measurement message.""" + return Measurement( + id=measurement_proto.id, + test_result_id=measurement_proto.test_result_id, + step_id=measurement_proto.step_id, + name=measurement_proto.name, + value=measurement_value_from_protobuf(measurement_proto, "value"), + upper_limit=measurement_value_from_protobuf(measurement_proto, "upper_limit"), + lower_limit=measurement_value_from_protobuf(measurement_proto, "lower_limit"), + value_type=measurement_proto.value_type, + units=measurement_proto.units, + notes=measurement_proto.notes, + start_date_time=timestamp_from_protobuf(measurement_proto, "start_date_time"), + end_date_time=timestamp_from_protobuf(measurement_proto, "end_date_time"), + duration=duration_from_protobuf(measurement_proto, "duration"), + outcome=Outcome.from_protobuf(measurement_proto.outcome), + error_information=( + ErrorInformation.from_protobuf(measurement_proto.error_information) + if measurement_proto.HasField("error_information") + else None + ), + extension=extension_from_protobuf(measurement_proto.extension), + schema_id=measurement_proto.schema_id, + software_item_ids=measurement_proto.software_item_ids, + hardware_item_ids=measurement_proto.hardware_item_ids, + test_adapter_ids=measurement_proto.test_adapter_ids, + software_items=[ + SoftwareItem.from_protobuf(item) for item in measurement_proto.software_items + ], + hardware_items=[ + HardwareItem.from_protobuf(item) for item in measurement_proto.hardware_items + ], + test_adapters=[ + TestAdapter.from_protobuf(adapter) for adapter in measurement_proto.test_adapters + ], + comparison_type=measurement_proto.comparison_type, + conditions=[ + Condition.from_protobuf(condition) for condition in measurement_proto.conditions + ], + created_at=timestamp_from_protobuf(measurement_proto, "created_at"), + updated_at=timestamp_from_protobuf(measurement_proto, "updated_at"), + created_by=measurement_proto.created_by, + updated_by=measurement_proto.updated_by, + ) + + def to_protobuf(self) -> MeasurementProto: + """Convert this Measurement to a protobuf Measurement message. + + Output-only fields, such as value_type and conditions, are not included in + protobuf requests. + """ + return MeasurementProto( + id=self.id, + test_result_id=self.test_result_id, + step_id=self.step_id, + name=self.name, + value=measurement_value_to_protobuf(self.value), + upper_limit=measurement_value_to_protobuf(self.upper_limit), + lower_limit=measurement_value_to_protobuf(self.lower_limit), + units=self.units or (self.value.units if isinstance(self.value, Scalar) else ""), + notes=self.notes, + start_date_time=timestamp_to_protobuf(self.start_date_time), + end_date_time=timestamp_to_protobuf(self.end_date_time), + duration=duration_to_protobuf(self.duration), + outcome=self.outcome.to_protobuf(), + error_information=( + self.error_information.to_protobuf() if self.error_information is not None else None + ), + extension=extension_to_protobuf(self.extension), + schema_id=self.schema_id, + software_item_ids=self.software_item_ids, + hardware_item_ids=self.hardware_item_ids, + test_adapter_ids=self.test_adapter_ids, + software_items=[item.to_protobuf() for item in self.software_items], + hardware_items=[item.to_protobuf() for item in self.hardware_items], + test_adapters=[adapter.to_protobuf() for adapter in self.test_adapters], + comparison_type=self.comparison_type, + ) + + def __eq__(self, other: object) -> bool: + """Determine equality.""" + if not isinstance(other, Measurement): + return NotImplemented + return ( + self.id == other.id + and self.test_result_id == other.test_result_id + and self.step_id == other.step_id + and self.name == other.name + and _values_equal(self.value, other.value) + and _values_equal(self.upper_limit, other.upper_limit) + and _values_equal(self.lower_limit, other.lower_limit) + and self.value_type == other.value_type + and self.units == other.units + and self.notes == other.notes + and self.start_date_time == other.start_date_time + and self.end_date_time == other.end_date_time + and self.duration == other.duration + and self.outcome == other.outcome + and self.error_information == other.error_information + and self.extension == other.extension + and self.schema_id == other.schema_id + and self.software_item_ids == other.software_item_ids + and self.hardware_item_ids == other.hardware_item_ids + and self.test_adapter_ids == other.test_adapter_ids + and self.software_items == other.software_items + and self.hardware_items == other.hardware_items + and self.test_adapters == other.test_adapters + and self.comparison_type == other.comparison_type + and self.conditions == other.conditions + and self.created_at == other.created_at + and self.updated_at == other.updated_at + and self.created_by == other.created_by + and self.updated_by == other.updated_by + ) + + def __str__(self) -> str: + """Return a string representation of the Measurement.""" + return str(self.to_protobuf()) + + +def _values_equal(left: object, right: object) -> bool: + # Waveform equality returns arrays, so compare through the protobuf form. + if left is None or right is None: + return left is right + return measurement_value_to_protobuf(_as_scalar(left)) == measurement_value_to_protobuf( + _as_scalar(right) + ) + + +def _as_scalar(value: object) -> object: + # Normalize plain values so protobuf comparison includes Scalar unit attributes. + return Scalar(value) if isinstance(value, (bool, int, float, str)) else value diff --git a/src/ni/datastore/data/v2/_types/_measurement_value.py b/src/ni/datastore/data/v2/_types/_measurement_value.py new file mode 100644 index 0000000..4f1c06d --- /dev/null +++ b/src/ni/datastore/data/v2/_types/_measurement_value.py @@ -0,0 +1,45 @@ +"""Measurement value types for the V2 Data Store Client.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping +from typing import Any, TypeAlias, Union + +from nitypes.scalar import Scalar +from nitypes.vector import Vector +from nitypes.waveform import AnalogWaveform, ComplexWaveform, DigitalWaveform, Spectrum +from nitypes.xy_data import XYData + +from ni.datastore.data.v2._types._json_value import JsonValue + +MeasurementReadValue: TypeAlias = Union[ + Scalar[Any], + Vector[Any], + AnalogWaveform[Any], + ComplexWaveform[Any], + Spectrum[Any], + DigitalWaveform[Any], + XYData[Any], + dict[str, Any], + JsonValue, +] +"""A measurement value or limit read from the data store. + +A single value is always read as a Scalar and a list of values as a Vector. JSON +is read as a dictionary, or as a JsonValue when it has attributes or a schema. +""" + +MeasurementValue: TypeAlias = Union[ + bool, + int, + float, + str, + MeasurementReadValue, + Mapping[str, Any], + Iterable[Union[bool, int, float, str]], +] +"""A measurement value or limit that can be written to the data store. + +Includes everything that can be read, plus plain bool, int, float, and str +values, JSON mappings, and lists of plain values. +""" diff --git a/src/ni/datastore/data/v2/_types/_outcome.py b/src/ni/datastore/data/v2/_types/_outcome.py new file mode 100644 index 0000000..e3187c3 --- /dev/null +++ b/src/ni/datastore/data/v2/_types/_outcome.py @@ -0,0 +1,47 @@ +"""Outcome data type for the V2 Data Store Client.""" + +from __future__ import annotations + +from enum import IntEnum + +from ni.measurements.data.v2.data_store_pb2 import Outcome as OutcomeProto + + +class Outcome(IntEnum): + """Represents the terminal result of a test result, step, or measurement.""" + + UNSPECIFIED = OutcomeProto.OUTCOME_UNSPECIFIED + """The outcome is not specified.""" + + PASSED = OutcomeProto.OUTCOME_PASSED + """The operation passed.""" + + FAILED = OutcomeProto.OUTCOME_FAILED + """The operation failed.""" + + SKIPPED = OutcomeProto.OUTCOME_SKIPPED + """The operation was skipped.""" + + TERMINATED = OutcomeProto.OUTCOME_TERMINATED + """The operation was terminated.""" + + ERRORED = OutcomeProto.OUTCOME_ERRORED + """The operation reported an error.""" + + TIMED_OUT = OutcomeProto.OUTCOME_TIMED_OUT + """The operation timed out.""" + + USER_DEFINED = OutcomeProto.OUTCOME_USER_DEFINED + """The outcome is defined by the user.""" + + @classmethod + def from_protobuf(cls, outcome_proto: OutcomeProto.ValueType) -> "Outcome": + """Create an Outcome from a protobuf Outcome value.""" + try: + return cls(outcome_proto) + except ValueError as e: + raise ValueError(f"Unknown outcome value: {outcome_proto}") from e + + def to_protobuf(self) -> OutcomeProto.ValueType: + """Convert this Outcome to a protobuf Outcome value.""" + return OutcomeProto.ValueType(self.value) diff --git a/src/ni/datastore/data/v2/_types/_query_results.py b/src/ni/datastore/data/v2/_types/_query_results.py new file mode 100644 index 0000000..3562c22 --- /dev/null +++ b/src/ni/datastore/data/v2/_types/_query_results.py @@ -0,0 +1,29 @@ +"""Query results data type for the V2 Data Store Client.""" + +from __future__ import annotations + +from collections.abc import Iterable +from typing import TypeVar + +T = TypeVar("T") + + +class QueryResults(list[T]): + """One page of query results with a continuation link when more results exist. + + Page size is not configured by this class; it contains the items and link + returned by the query. + """ + + next_link: str + """The link for retrieving the next page, or an empty string when there is none.""" + + def __init__(self, items: Iterable[T] = (), next_link: str = "") -> None: + """Initialize a QueryResults instance. + + Args: + items: The results on this page. + next_link: The OData link to the next page, or an empty string. + """ + super().__init__(items) + self.next_link = next_link diff --git a/src/ni/datastore/data/v2/_types/_read_fields.py b/src/ni/datastore/data/v2/_types/_read_fields.py new file mode 100644 index 0000000..1f33c60 --- /dev/null +++ b/src/ni/datastore/data/v2/_types/_read_fields.py @@ -0,0 +1,117 @@ +"""Field names that can be selected when reading V2 data store resources.""" + +from __future__ import annotations + +from enum import Enum + + +class TestResultField(str, Enum): + """A test result field that can be selected with read_fields.""" + + ID = "id" + UUT_INSTANCE_ID = "uut_instance_id" + OPERATOR_ID = "operator_id" + TEST_STATION_ID = "test_station_id" + TEST_DESCRIPTION_ID = "test_description_id" + SOFTWARE_ITEM_IDS = "software_item_ids" + HARDWARE_ITEM_IDS = "hardware_item_ids" + TEST_ADAPTER_IDS = "test_adapter_ids" + NAME = "name" + START_DATE_TIME = "start_date_time" + END_DATE_TIME = "end_date_time" + OUTCOME = "outcome" + STATUS = "status" + ERROR_INFORMATION = "error_information" + LINK = "link" + EXTENSION = "extension" + SCHEMA_ID = "schema_id" + UUT_ID = "uut_id" + KEYWORDS = "keywords" + DURATION = "duration" + CREATED_AT = "created_at" + UPDATED_AT = "updated_at" + CREATED_BY = "created_by" + UPDATED_BY = "updated_by" + STEPS = "steps" + UUT_INSTANCE = "uut_instance" + OPERATOR = "operator" + TEST_STATION = "test_station" + TEST_DESCRIPTION = "test_description" + UUT = "uut" + SOFTWARE_ITEMS = "software_items" + HARDWARE_ITEMS = "hardware_items" + TEST_ADAPTERS = "test_adapters" + + +class StepField(str, Enum): + """A step field that can be selected with read_fields.""" + + ID = "id" + PARENT_STEP_ID = "parent_step_id" + TEST_RESULT_ID = "test_result_id" + TEST_ID = "test_id" + NAME = "name" + STEP_TYPE = "step_type" + NOTES = "notes" + START_DATE_TIME = "start_date_time" + END_DATE_TIME = "end_date_time" + OUTCOME = "outcome" + STATUS = "status" + ERROR_INFORMATION = "error_information" + LINK = "link" + EXTENSION = "extension" + SCHEMA_ID = "schema_id" + KEYWORDS = "keywords" + DURATION = "duration" + PATH = "path" + PATH_IDS = "path_ids" + CREATED_AT = "created_at" + UPDATED_AT = "updated_at" + CREATED_BY = "created_by" + UPDATED_BY = "updated_by" + CONDITIONS = "conditions" + MEASUREMENTS = "measurements" + CHILDREN = "children" + TEST = "test" + + +class MeasurementField(str, Enum): + """A measurement field that can be selected with read_fields.""" + + ID = "id" + TEST_RESULT_ID = "test_result_id" + STEP_ID = "step_id" + NAME = "name" + VALUE_TYPE = "value_type" + VALUE = "value" + UNITS = "units" + UPPER_LIMIT = "upper_limit" + LOWER_LIMIT = "lower_limit" + SOFTWARE_ITEM_IDS = "software_item_ids" + HARDWARE_ITEM_IDS = "hardware_item_ids" + TEST_ADAPTER_IDS = "test_adapter_ids" + NOTES = "notes" + START_DATE_TIME = "start_date_time" + END_DATE_TIME = "end_date_time" + OUTCOME = "outcome" + ERROR_INFORMATION = "error_information" + EXTENSION = "extension" + SCHEMA_ID = "schema_id" + DURATION = "duration" + CONDITIONS = "conditions" + CREATED_AT = "created_at" + UPDATED_AT = "updated_at" + CREATED_BY = "created_by" + UPDATED_BY = "updated_by" + SOFTWARE_ITEMS = "software_items" + HARDWARE_ITEMS = "hardware_items" + TEST_ADAPTERS = "test_adapters" + COMPARISON_TYPE = "comparison_type" + + +class MeasurementValueField(str, Enum): + """A measurement value field that can be selected with read_measurement_values.""" + + VALUE = "value" + UPPER_LIMIT = "upper_limit" + LOWER_LIMIT = "lower_limit" diff --git a/src/ni/datastore/data/v2/_types/_status.py b/src/ni/datastore/data/v2/_types/_status.py new file mode 100644 index 0000000..b0a4b27 --- /dev/null +++ b/src/ni/datastore/data/v2/_types/_status.py @@ -0,0 +1,41 @@ +"""Status data type for the V2 Data Store Client.""" + +from __future__ import annotations + +from enum import IntEnum + +from ni.measurements.data.v2.data_store_pb2 import Status as StatusProto + + +class Status(IntEnum): + """Represents lifecycle progress, independent of the terminal Outcome.""" + + UNSPECIFIED = StatusProto.STATUS_UNSPECIFIED + """The status is not specified.""" + + NOT_STARTED = StatusProto.STATUS_NOT_STARTED + """The operation has not started.""" + + LOOPING = StatusProto.STATUS_LOOPING + """The operation is looping.""" + + RUNNING = StatusProto.STATUS_RUNNING + """The operation is running.""" + + WAITING = StatusProto.STATUS_WAITING + """The operation is waiting.""" + + DONE = StatusProto.STATUS_DONE + """The operation is done.""" + + @classmethod + def from_protobuf(cls, status_proto: StatusProto.ValueType) -> "Status": + """Create a Status from a protobuf Status value.""" + try: + return cls(status_proto) + except ValueError as e: + raise ValueError(f"Unknown status value: {status_proto}") from e + + def to_protobuf(self) -> StatusProto.ValueType: + """Convert this Status to a protobuf Status value.""" + return StatusProto.ValueType(self.value) diff --git a/src/ni/datastore/data/v2/_types/_step.py b/src/ni/datastore/data/v2/_types/_step.py new file mode 100644 index 0000000..48637c0 --- /dev/null +++ b/src/ni/datastore/data/v2/_types/_step.py @@ -0,0 +1,304 @@ +"""Step data type for the V2 Data Store Client.""" + +from __future__ import annotations + +from typing import Any, Iterable, Mapping, MutableMapping, MutableSequence +from uuid import UUID + +import hightime as ht +from ni.measurements.data.v2.data_store_pb2 import Step as StepProto + +from ni.datastore.data.v2._grpc_conversion import ( + duration_from_protobuf, + duration_to_protobuf, + extension_from_protobuf, + extension_to_protobuf, + id_string, + string_list, + timestamp_from_protobuf, + timestamp_to_protobuf, +) +from ni.datastore.data.v2._types._condition import Condition +from ni.datastore.data.v2._types._error_information import ErrorInformation +from ni.datastore.data.v2._types._measurement import Measurement +from ni.datastore.data.v2._types._outcome import Outcome +from ni.datastore.data.v2._types._status import Status +from ni.datastore.metadata import Test + + +class Step: + """Information about one execution of a test step. + + A step belongs to a test result and owns measurements and conditions. Steps + can be nested by using parent_step_id, or created together by using + children and measurements. + """ + + __slots__ = ( + "name", + "id", + "parent_step_id", + "test_result_id", + "test_id", + "test", + "step_type", + "notes", + "start_date_time", + "end_date_time", + "duration", + "outcome", + "status", + "link", + "_keywords", + "_extension", + "schema_id", + "error_information", + "_conditions", + "_measurements", + "_children", + "_path", + "_path_ids", + "created_at", + "updated_at", + "created_by", + "updated_by", + ) + + @property + def keywords(self) -> MutableSequence[str]: + """Labels used to classify the step.""" + return self._keywords + + @property + def extension(self) -> MutableMapping[str, Any]: + """Custom JSON data associated with the step.""" + return self._extension + + @property + def conditions(self) -> MutableSequence[Condition]: + """The conditions owned by the step.""" + return self._conditions + + @property + def measurements(self) -> MutableSequence[Measurement]: + """Measurements created with the step, or expand on read.""" + return self._measurements + + @property + def children(self) -> MutableSequence[Step]: + """Child steps created with the step, or expand on read.""" + return self._children + + @property + def path(self) -> MutableSequence[str]: + """Output only. Names of this Step and its ancestors, ordered root-first.""" + return self._path + + @property + def path_ids(self) -> MutableSequence[str]: + """Output only. IDs of this Step and its ancestors, ordered root-first.""" + return self._path_ids + + def __init__( + self, + name: str, + *, + id: str | UUID = "", + parent_step_id: str | UUID = "", + test_result_id: str | UUID = "", + test_id: str | UUID = "", + test: Test | None = None, + step_type: str = "", + notes: str = "", + start_date_time: ht.datetime | None = None, + end_date_time: ht.datetime | None = None, + duration: ht.timedelta | None = None, + outcome: Outcome = Outcome.UNSPECIFIED, + status: Status = Status.UNSPECIFIED, + link: str = "", + keywords: Iterable[str] | None = None, + extension: Mapping[str, Any] | None = None, + schema_id: str = "", + error_information: ErrorInformation | None = None, + conditions: Iterable[Condition] | None = None, + measurements: Iterable[Measurement] | None = None, + children: Iterable[Step] | None = None, + path: Iterable[str] | None = None, + path_ids: Iterable[str] | None = None, + created_at: ht.datetime | None = None, + updated_at: ht.datetime | None = None, + created_by: str = "", + updated_by: str = "", + ) -> None: + """Initialize a Step instance. + + Args: + name: Human-readable name of the step. + id (optional): Unique identifier for the step. + parent_step_id (optional): ID of the parent step if this is a nested step. + test_result_id (optional): ID of the test result this step belongs to. + test_id (optional): ID or registered alias of the test associated with this + step. Reads return the ID. + test (optional): Test to find or create on create, or expand on read. + step_type (optional): Free-form classification of the step. + notes (optional): Additional notes about the step. + start_date_time (optional): The start date and time of the step execution. + end_date_time (optional): The end date and time of the step execution. + duration (optional): The duration of the step execution. + outcome (optional): The terminal outcome of the step. + status (optional): The lifecycle status of the step. + link (optional): A URI associated with the step. + keywords (optional): Labels used to classify the step. + extension (optional): Custom JSON data for the step. + schema_id (optional): Schema that governs the extension. Reserved for + future use. + error_information (optional): Error details for the step. + conditions (optional): Conditions created with the step. + measurements (optional): Measurements created with the step. + children (optional): Child steps created with the step. + path (optional): Output only. The step-name path, including this step. + path_ids (optional): Output only. The step-identifier path, including this step. + created_at (optional): Output only. When the step was created. + updated_at (optional): Output only. When the step was last updated. + created_by (optional): Output only. Who created the step. + updated_by (optional): Output only. Who last updated the step. + """ + self.name = name + self.id = id_string(id) + self.parent_step_id = id_string(parent_step_id) + self.test_result_id = id_string(test_result_id) + self.test_id = id_string(test_id) + self.test = test + self.step_type = step_type + self.notes = notes + self.start_date_time = start_date_time + self.end_date_time = end_date_time + self.duration = duration + self.outcome = outcome + self.status = status + self.link = link + self._keywords: MutableSequence[str] = string_list(keywords, "keywords") + self._extension: MutableMapping[str, Any] = dict(extension) if extension is not None else {} + self.schema_id = schema_id + self.error_information = error_information + self._conditions: MutableSequence[Condition] = ( + list(conditions) if conditions is not None else [] + ) + self._measurements: MutableSequence[Measurement] = ( + list(measurements) if measurements is not None else [] + ) + self._children: MutableSequence[Step] = list(children) if children is not None else [] + self._path: MutableSequence[str] = string_list(path, "path") + self._path_ids: MutableSequence[str] = string_list(path_ids, "path_ids") + self.created_at = created_at + self.updated_at = updated_at + self.created_by = created_by + self.updated_by = updated_by + + @staticmethod + def from_protobuf(step_proto: StepProto) -> "Step": + """Create a Step instance from a protobuf Step message.""" + return Step( + name=step_proto.name, + id=step_proto.id, + parent_step_id=step_proto.parent_step_id, + test_result_id=step_proto.test_result_id, + test_id=step_proto.test_id, + test=Test.from_protobuf(step_proto.test) if step_proto.HasField("test") else None, + step_type=step_proto.step_type, + notes=step_proto.notes, + start_date_time=timestamp_from_protobuf(step_proto, "start_date_time"), + end_date_time=timestamp_from_protobuf(step_proto, "end_date_time"), + duration=duration_from_protobuf(step_proto, "duration"), + outcome=Outcome.from_protobuf(step_proto.outcome), + status=Status.from_protobuf(step_proto.status), + link=step_proto.link, + keywords=step_proto.keywords, + extension=extension_from_protobuf(step_proto.extension), + schema_id=step_proto.schema_id, + error_information=( + ErrorInformation.from_protobuf(step_proto.error_information) + if step_proto.HasField("error_information") + else None + ), + conditions=[Condition.from_protobuf(condition) for condition in step_proto.conditions], + measurements=[ + Measurement.from_protobuf(measurement) for measurement in step_proto.measurements + ], + children=[Step.from_protobuf(child) for child in step_proto.children], + path=step_proto.path, + path_ids=step_proto.path_ids, + created_at=timestamp_from_protobuf(step_proto, "created_at"), + updated_at=timestamp_from_protobuf(step_proto, "updated_at"), + created_by=step_proto.created_by, + updated_by=step_proto.updated_by, + ) + + def to_protobuf(self) -> StepProto: + """Convert this Step to a protobuf Step message. + + Output-only fields, such as path and created_at, are not included. + """ + return StepProto( + name=self.name, + id=self.id, + parent_step_id=self.parent_step_id, + test_result_id=self.test_result_id, + test_id=self.test_id, + test=self.test.to_protobuf() if self.test is not None else None, + step_type=self.step_type, + notes=self.notes, + start_date_time=timestamp_to_protobuf(self.start_date_time), + end_date_time=timestamp_to_protobuf(self.end_date_time), + duration=duration_to_protobuf(self.duration), + outcome=self.outcome.to_protobuf(), + status=self.status.to_protobuf(), + link=self.link, + keywords=self.keywords, + extension=extension_to_protobuf(self.extension), + schema_id=self.schema_id, + error_information=( + self.error_information.to_protobuf() if self.error_information is not None else None + ), + conditions=[condition.to_protobuf() for condition in self.conditions], + measurements=[measurement.to_protobuf() for measurement in self.measurements], + children=[child.to_protobuf() for child in self.children], + ) + + def __eq__(self, other: object) -> bool: + """Determine equality.""" + if not isinstance(other, Step): + return NotImplemented + return ( + self.name == other.name + and self.id == other.id + and self.parent_step_id == other.parent_step_id + and self.test_result_id == other.test_result_id + and self.test_id == other.test_id + and self.test == other.test + and self.step_type == other.step_type + and self.notes == other.notes + and self.start_date_time == other.start_date_time + and self.end_date_time == other.end_date_time + and self.duration == other.duration + and self.outcome == other.outcome + and self.status == other.status + and self.link == other.link + and self.keywords == other.keywords + and self.extension == other.extension + and self.schema_id == other.schema_id + and self.error_information == other.error_information + and self.conditions == other.conditions + and self.measurements == other.measurements + and self.children == other.children + and self.path == other.path + and self.path_ids == other.path_ids + and self.created_at == other.created_at + and self.updated_at == other.updated_at + and self.created_by == other.created_by + and self.updated_by == other.updated_by + ) + + def __str__(self) -> str: + """Return a string representation of the Step.""" + return str(self.to_protobuf()) diff --git a/src/ni/datastore/data/v2/_types/_test_result.py b/src/ni/datastore/data/v2/_types/_test_result.py new file mode 100644 index 0000000..ed71cd6 --- /dev/null +++ b/src/ni/datastore/data/v2/_types/_test_result.py @@ -0,0 +1,394 @@ +"""Test Result data type for the V2 Data Store Client.""" + +from __future__ import annotations + +from typing import Any, Iterable, Mapping, MutableMapping, MutableSequence +from uuid import UUID + +import hightime as ht +from ni.measurements.data.v2.data_store_pb2 import TestResult as TestResultProto + +from ni.datastore.data.v2._grpc_conversion import ( + duration_from_protobuf, + duration_to_protobuf, + extension_from_protobuf, + extension_to_protobuf, + id_list, + id_string, + string_list, + timestamp_from_protobuf, + timestamp_to_protobuf, +) +from ni.datastore.data.v2._types._error_information import ErrorInformation +from ni.datastore.data.v2._types._outcome import Outcome +from ni.datastore.data.v2._types._status import Status +from ni.datastore.data.v2._types._step import Step +from ni.datastore.metadata import ( + HardwareItem, + Operator, + SoftwareItem, + TestAdapter, + TestDescription, + TestStation, + Uut, + UutInstance, +) + + +class TestResult: + """Information about one execution of a test process. + + A test result records the UUT, operator, test station, and other metadata + for a test run, along with its timing, status, and outcome. Steps, and the + measurements and conditions they own, belong to a test result. + """ + + __slots__ = ( + "name", + "id", + "uut_instance_id", + "uut_id", + "operator_id", + "test_station_id", + "test_description_id", + "uut_instance", + "uut", + "operator", + "test_station", + "test_description", + "_software_item_ids", + "_hardware_item_ids", + "_test_adapter_ids", + "_software_items", + "_hardware_items", + "_test_adapters", + "start_date_time", + "end_date_time", + "duration", + "outcome", + "status", + "link", + "_keywords", + "_extension", + "schema_id", + "error_information", + "_steps", + "created_at", + "updated_at", + "created_by", + "updated_by", + ) + + @property + def software_item_ids(self) -> MutableSequence[str]: + """The software item IDs associated with the test result.""" + return self._software_item_ids + + @property + def hardware_item_ids(self) -> MutableSequence[str]: + """The hardware item IDs associated with the test result.""" + return self._hardware_item_ids + + @property + def test_adapter_ids(self) -> MutableSequence[str]: + """The test adapter IDs associated with the test result.""" + return self._test_adapter_ids + + @property + def software_items(self) -> MutableSequence[SoftwareItem]: + """Software items to find or create on create, or expand on read.""" + return self._software_items + + @property + def hardware_items(self) -> MutableSequence[HardwareItem]: + """Hardware items to find or create on create, or expand on read.""" + return self._hardware_items + + @property + def test_adapters(self) -> MutableSequence[TestAdapter]: + """Test adapters to find or create on create, or expand on read.""" + return self._test_adapters + + @property + def steps(self) -> MutableSequence[Step]: + """Steps created with the test result, or expand on read.""" + return self._steps + + @property + def keywords(self) -> MutableSequence[str]: + """Labels used to classify the test result.""" + return self._keywords + + @property + def extension(self) -> MutableMapping[str, Any]: + """Custom JSON data associated with the test result.""" + return self._extension + + def __init__( + self, + name: str, + *, + id: str | UUID = "", + uut_instance_id: str | UUID = "", + uut_id: str | UUID = "", + operator_id: str | UUID = "", + test_station_id: str | UUID = "", + test_description_id: str | UUID = "", + uut_instance: UutInstance | None = None, + uut: Uut | None = None, + operator: Operator | None = None, + test_station: TestStation | None = None, + test_description: TestDescription | None = None, + software_item_ids: Iterable[str | UUID] | None = None, + hardware_item_ids: Iterable[str | UUID] | None = None, + test_adapter_ids: Iterable[str | UUID] | None = None, + software_items: Iterable[SoftwareItem] | None = None, + hardware_items: Iterable[HardwareItem] | None = None, + test_adapters: Iterable[TestAdapter] | None = None, + start_date_time: ht.datetime | None = None, + end_date_time: ht.datetime | None = None, + duration: ht.timedelta | None = None, + outcome: Outcome = Outcome.UNSPECIFIED, + status: Status = Status.UNSPECIFIED, + link: str = "", + keywords: Iterable[str] | None = None, + extension: Mapping[str, Any] | None = None, + schema_id: str = "", + error_information: ErrorInformation | None = None, + steps: Iterable[Step] | None = None, + created_at: ht.datetime | None = None, + updated_at: ht.datetime | None = None, + created_by: str = "", + updated_by: str = "", + ) -> None: + """Initialize a TestResult instance. + + Args: + name: Human-readable name for the test result. + id (optional): Unique identifier for the test result. + uut_instance_id (optional): ID or registered alias of the UUT instance that + was tested. Reads return the ID. + uut_id (optional): ID of the UUT product definition. + operator_id (optional): ID or registered alias of the operator who ran the + test. Reads return the ID. + test_station_id (optional): ID or registered alias of the test station used. + Reads return the ID. + test_description_id (optional): ID or registered alias of the test + description executed. Reads return the ID. + uut_instance (optional): UUT instance to find or create on create, or expand + on read. + uut (optional): UUT to find or create on create, or expand on read. + operator (optional): Operator to find or create on create, or expand on read. + test_station (optional): Test station to find or create on create, or expand + on read. + test_description (optional): Test description to find or create on create, or + expand on read. + software_item_ids (optional): IDs of software items used in the test. + hardware_item_ids (optional): IDs of hardware items used in the test. + test_adapter_ids (optional): IDs of test adapters used in the test. + software_items (optional): Software items to find or create on create. + hardware_items (optional): Hardware items to find or create on create. + test_adapters (optional): Test adapters to find or create on create. + start_date_time (optional): The start date and time of the test execution. + end_date_time (optional): The end date and time of the test execution. + duration (optional): The duration of the test execution. + outcome (optional): The terminal outcome of the test execution. + status (optional): The lifecycle status of the test execution. + link (optional): A URI associated with the test result. + keywords (optional): Labels used to classify the test result. + extension (optional): Custom JSON data for the test result. + schema_id (optional): Schema that governs the extension. Reserved for + future use. + error_information (optional): Error details for the test result. + steps (optional): Steps created with the test result. + created_at (optional): Output only. When the test result was created. + updated_at (optional): Output only. When the test result was last updated. + created_by (optional): Output only. Who created the test result. + updated_by (optional): Output only. Who last updated the test result. + """ + self.name = name + self.id = id_string(id) + self.uut_instance_id = id_string(uut_instance_id) + self.uut_id = id_string(uut_id) + self.operator_id = id_string(operator_id) + self.test_station_id = id_string(test_station_id) + self.test_description_id = id_string(test_description_id) + self.uut_instance = uut_instance + self.uut = uut + self.operator = operator + self.test_station = test_station + self.test_description = test_description + self._software_item_ids: MutableSequence[str] = id_list( + software_item_ids, "software_item_ids" + ) + self._hardware_item_ids: MutableSequence[str] = id_list( + hardware_item_ids, "hardware_item_ids" + ) + self._test_adapter_ids: MutableSequence[str] = id_list(test_adapter_ids, "test_adapter_ids") + self._software_items: MutableSequence[SoftwareItem] = ( + list(software_items) if software_items is not None else [] + ) + self._hardware_items: MutableSequence[HardwareItem] = ( + list(hardware_items) if hardware_items is not None else [] + ) + self._test_adapters: MutableSequence[TestAdapter] = ( + list(test_adapters) if test_adapters is not None else [] + ) + self.start_date_time = start_date_time + self.end_date_time = end_date_time + self.duration = duration + self.outcome = outcome + self.status = status + self.link = link + self._keywords: MutableSequence[str] = string_list(keywords, "keywords") + self._extension: MutableMapping[str, Any] = dict(extension) if extension is not None else {} + self.schema_id = schema_id + self.error_information = error_information + self._steps: MutableSequence[Step] = list(steps) if steps is not None else [] + self.created_at = created_at + self.updated_at = updated_at + self.created_by = created_by + self.updated_by = updated_by + + @staticmethod + def from_protobuf(test_result_proto: TestResultProto) -> "TestResult": + """Create a TestResult instance from a protobuf TestResult message.""" + proto = test_result_proto + return TestResult( + name=proto.name, + id=proto.id, + uut_instance_id=proto.uut_instance_id, + uut_id=proto.uut_id, + operator_id=proto.operator_id, + test_station_id=proto.test_station_id, + test_description_id=proto.test_description_id, + uut_instance=( + UutInstance.from_protobuf(proto.uut_instance) + if proto.HasField("uut_instance") + else None + ), + uut=Uut.from_protobuf(proto.uut) if proto.HasField("uut") else None, + operator=Operator.from_protobuf(proto.operator) if proto.HasField("operator") else None, + test_station=( + TestStation.from_protobuf(proto.test_station) + if proto.HasField("test_station") + else None + ), + test_description=( + TestDescription.from_protobuf(proto.test_description) + if proto.HasField("test_description") + else None + ), + software_item_ids=proto.software_item_ids, + hardware_item_ids=proto.hardware_item_ids, + test_adapter_ids=proto.test_adapter_ids, + software_items=[SoftwareItem.from_protobuf(item) for item in proto.software_items], + hardware_items=[HardwareItem.from_protobuf(item) for item in proto.hardware_items], + test_adapters=[TestAdapter.from_protobuf(adapter) for adapter in proto.test_adapters], + start_date_time=timestamp_from_protobuf(proto, "start_date_time"), + end_date_time=timestamp_from_protobuf(proto, "end_date_time"), + duration=duration_from_protobuf(proto, "duration"), + outcome=Outcome.from_protobuf(proto.outcome), + status=Status.from_protobuf(proto.status), + link=proto.link, + keywords=proto.keywords, + extension=extension_from_protobuf(proto.extension), + schema_id=proto.schema_id, + error_information=( + ErrorInformation.from_protobuf(proto.error_information) + if proto.HasField("error_information") + else None + ), + steps=[Step.from_protobuf(step) for step in proto.steps], + created_at=timestamp_from_protobuf(proto, "created_at"), + updated_at=timestamp_from_protobuf(proto, "updated_at"), + created_by=proto.created_by, + updated_by=proto.updated_by, + ) + + def to_protobuf(self) -> TestResultProto: + """Convert this TestResult to a protobuf TestResult message. + + Output-only fields, such as created_at, are not included. + """ + return TestResultProto( + name=self.name, + id=self.id, + uut_instance_id=self.uut_instance_id, + uut_id=self.uut_id, + operator_id=self.operator_id, + test_station_id=self.test_station_id, + test_description_id=self.test_description_id, + uut_instance=_optional_to_protobuf(self.uut_instance), + uut=_optional_to_protobuf(self.uut), + operator=_optional_to_protobuf(self.operator), + test_station=_optional_to_protobuf(self.test_station), + test_description=_optional_to_protobuf(self.test_description), + software_item_ids=self.software_item_ids, + hardware_item_ids=self.hardware_item_ids, + test_adapter_ids=self.test_adapter_ids, + software_items=[item.to_protobuf() for item in self.software_items], + hardware_items=[item.to_protobuf() for item in self.hardware_items], + test_adapters=[adapter.to_protobuf() for adapter in self.test_adapters], + start_date_time=timestamp_to_protobuf(self.start_date_time), + end_date_time=timestamp_to_protobuf(self.end_date_time), + duration=duration_to_protobuf(self.duration), + outcome=self.outcome.to_protobuf(), + status=self.status.to_protobuf(), + link=self.link, + keywords=self.keywords, + extension=extension_to_protobuf(self.extension), + schema_id=self.schema_id, + error_information=( + self.error_information.to_protobuf() if self.error_information is not None else None + ), + steps=[step.to_protobuf() for step in self.steps], + ) + + def __eq__(self, other: object) -> bool: + """Determine equality.""" + if not isinstance(other, TestResult): + return NotImplemented + return ( + self.name == other.name + and self.id == other.id + and self.uut_instance_id == other.uut_instance_id + and self.uut_id == other.uut_id + and self.operator_id == other.operator_id + and self.test_station_id == other.test_station_id + and self.test_description_id == other.test_description_id + and self.uut_instance == other.uut_instance + and self.uut == other.uut + and self.operator == other.operator + and self.test_station == other.test_station + and self.test_description == other.test_description + and self.software_item_ids == other.software_item_ids + and self.hardware_item_ids == other.hardware_item_ids + and self.test_adapter_ids == other.test_adapter_ids + and self.software_items == other.software_items + and self.hardware_items == other.hardware_items + and self.test_adapters == other.test_adapters + and self.start_date_time == other.start_date_time + and self.end_date_time == other.end_date_time + and self.duration == other.duration + and self.outcome == other.outcome + and self.status == other.status + and self.link == other.link + and self.keywords == other.keywords + and self.extension == other.extension + and self.schema_id == other.schema_id + and self.error_information == other.error_information + and self.steps == other.steps + and self.created_at == other.created_at + and self.updated_at == other.updated_at + and self.created_by == other.created_by + and self.updated_by == other.updated_by + ) + + def __str__(self) -> str: + """Return a string representation of the TestResult.""" + return str(self.to_protobuf()) + + +def _optional_to_protobuf(item: Any) -> Any: + return item.to_protobuf() if item is not None else None diff --git a/src/ni/datastore/data/v2/_types/_update.py b/src/ni/datastore/data/v2/_types/_update.py new file mode 100644 index 0000000..e2e72b6 --- /dev/null +++ b/src/ni/datastore/data/v2/_types/_update.py @@ -0,0 +1,431 @@ +"""Update builders and typed update fields for the V2 Data Store Client.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping +from typing import Any, TypedDict +from uuid import UUID + +import hightime as ht +from google.protobuf.field_mask_pb2 import FieldMask +from ni.measurements.data.v2.data_store_pb2 import ( + Measurement as MeasurementProto, + Step as StepProto, + TestResult as TestResultProto, +) +from ni.measurements.data.v2.data_store_service_pb2 import ( + UpdateMeasurementRequest, + UpdateStepRequest, + UpdateTestResultRequest, +) + +from ni.datastore.data.v2._grpc_conversion import id_string, string_list +from ni.datastore.data.v2._types._error_information import ErrorInformation +from ni.datastore.data.v2._types._measurement import Measurement +from ni.datastore.data.v2._types._outcome import Outcome +from ni.datastore.data.v2._types._status import Status +from ni.datastore.data.v2._types._step import Step +from ni.datastore.data.v2._types._test_result import TestResult +from ni.datastore.data.v2._update_conversion import ( + MEASUREMENT_UPDATE_FIELDS, + populate_update, + STEP_UPDATE_FIELDS, + TEST_RESULT_UPDATE_FIELDS, +) +from ni.datastore.metadata import ( + Operator, + Test, + TestDescription, + TestStation, + Uut, + UutInstance, +) + + +class TestResultUpdateFields(TypedDict, total=False): + """The test result fields that update_test_result accepts as keyword arguments. + + A value of None clears the field. uut_id, uut, keywords, and duration are + Stage 2 fields that the service may not support yet. + """ + + uut_instance_id: str | UUID | None + uut_id: str | UUID | None + operator_id: str | UUID | None + test_station_id: str | UUID | None + test_description_id: str | UUID | None + uut_instance: UutInstance | None + uut: Uut | None + operator: Operator | None + test_station: TestStation | None + test_description: TestDescription | None + start_date_time: ht.datetime | None + end_date_time: ht.datetime | None + duration: ht.timedelta | None + outcome: Outcome | None + status: Status | None + error_information: ErrorInformation | None + link: str | None + keywords: Iterable[str] | None + extension: Mapping[str, Any] | None + + +class StepUpdateFields(TypedDict, total=False): + """The step fields that update_step accepts as keyword arguments. + + A value of None clears the field. keywords and duration are Stage 2 fields + that the service may not support yet. + """ + + test_id: str | UUID | None + test: Test | None + notes: str | None + start_date_time: ht.datetime | None + end_date_time: ht.datetime | None + duration: ht.timedelta | None + outcome: Outcome | None + status: Status | None + error_information: ErrorInformation | None + link: str | None + keywords: Iterable[str] | None + extension: Mapping[str, Any] | None + + +class MeasurementUpdateFields(TypedDict, total=False): + """The measurement fields that update_measurement accepts as keyword arguments. + + A value of None clears the field. duration is a Stage 2 field that the + service may not support yet. + """ + + notes: str | None + start_date_time: ht.datetime | None + end_date_time: ht.datetime | None + duration: ht.timedelta | None + outcome: Outcome | None + error_information: ErrorInformation | None + extension: Mapping[str, Any] | None + + +class TestResultUpdateBuilder: + """Builds an update for an existing test result. + + Each with method sets a field and adds it to the update mask. Only the + selected fields change. None clears a selected field. For a relationship + that can be set by ID or by inline metadata, such as operator_id and + operator, the last call wins and the other form is not sent. Values are + copied when each with method is called. + + Example: + update = TestResultUpdateBuilder(test_result).with_outcome(Outcome.PASSED) + updated = data_store_client.update_test_result(update) + """ + + __slots__ = ("_test_result", "_paths") + + def __init__(self, test_result: TestResult | str | UUID) -> None: + """Start an update for a test result without selecting any fields. + + Args: + test_result: The test result to update, or its ID. + + Raises: + ValueError: If the ID is empty. + """ + self._test_result = TestResultProto(id=_target_id(test_result, "test result")) + self._paths: dict[str, None] = {} + + def with_start_date_time(self, start_date_time: ht.datetime | None) -> TestResultUpdateBuilder: + """Set the start date and time, or clear it with None.""" + return self._select("start_date_time", start_date_time) + + def with_end_date_time(self, end_date_time: ht.datetime | None) -> TestResultUpdateBuilder: + """Set the end date and time, or clear it with None.""" + return self._select("end_date_time", end_date_time) + + def with_duration(self, duration: ht.timedelta | None) -> TestResultUpdateBuilder: + """Set the duration, or clear it with None. The service may not support this yet.""" + return self._select("duration", duration) + + def with_status(self, status: Status) -> TestResultUpdateBuilder: + """Set the lifecycle status.""" + return self._select("status", status) + + def with_outcome(self, outcome: Outcome) -> TestResultUpdateBuilder: + """Set the outcome.""" + return self._select("outcome", outcome) + + def with_error_information( + self, error_information: ErrorInformation | None + ) -> TestResultUpdateBuilder: + """Set the error information, or clear it with None.""" + return self._select("error_information", error_information) + + def with_extension(self, extension: Mapping[str, Any] | None) -> TestResultUpdateBuilder: + """Replace the whole extension JSON document, or clear it with None.""" + return self._select("extension", None if extension is None else dict(extension)) + + def with_link(self, link: str | None) -> TestResultUpdateBuilder: + """Set the link, or clear it with None.""" + return self._select("link", link) + + def with_keywords(self, keywords: Iterable[str] | None) -> TestResultUpdateBuilder: + """Replace the keywords, or clear them with None. The service may not support this yet.""" + return self._select( + "keywords", None if keywords is None else string_list(keywords, "keywords") + ) + + def with_uut_instance_id(self, uut_instance_id: str | UUID | None) -> TestResultUpdateBuilder: + """Link a UUT instance by ID or alias, or unlink it with None.""" + return self._select("uut_instance_id", uut_instance_id, "uut_instance") + + def with_uut_instance(self, uut_instance: UutInstance | None) -> TestResultUpdateBuilder: + """Link a UUT instance found or created from inline metadata, or unlink it with None.""" + return self._select("uut_instance", uut_instance, "uut_instance_id") + + def with_uut_id(self, uut_id: str | UUID | None) -> TestResultUpdateBuilder: + """Link a UUT by ID or alias, or unlink it with None. + + The service may not support this yet. + """ + return self._select("uut_id", uut_id, "uut") + + def with_uut(self, uut: Uut | None) -> TestResultUpdateBuilder: + """Link a UUT from inline metadata, or unlink it with None. + + The service may not support this yet. + """ + return self._select("uut", uut, "uut_id") + + def with_operator_id(self, operator_id: str | UUID | None) -> TestResultUpdateBuilder: + """Link an operator by ID or alias, or unlink it with None.""" + return self._select("operator_id", operator_id, "operator") + + def with_operator(self, operator: Operator | None) -> TestResultUpdateBuilder: + """Link an operator found or created from inline metadata, or unlink it with None.""" + return self._select("operator", operator, "operator_id") + + def with_test_station_id(self, test_station_id: str | UUID | None) -> TestResultUpdateBuilder: + """Link a test station by ID or alias, or unlink it with None.""" + return self._select("test_station_id", test_station_id, "test_station") + + def with_test_station(self, test_station: TestStation | None) -> TestResultUpdateBuilder: + """Link a test station found or created from inline metadata, or unlink it with None.""" + return self._select("test_station", test_station, "test_station_id") + + def with_test_description_id( + self, test_description_id: str | UUID | None + ) -> TestResultUpdateBuilder: + """Link a test description by ID or alias, or unlink it with None.""" + return self._select("test_description_id", test_description_id, "test_description") + + def with_test_description( + self, test_description: TestDescription | None + ) -> TestResultUpdateBuilder: + """Link a test description found or created from inline metadata, or unlink it with None.""" + return self._select("test_description", test_description, "test_description_id") + + def build_update(self) -> UpdateTestResultRequest: + """Build the update request. + + Raises: + ValueError: If no fields are selected. + """ + return UpdateTestResultRequest( + test_result=self._test_result, update_mask=_update_mask(self._paths) + ) + + def _select(self, name: str, value: object, other: str = "") -> TestResultUpdateBuilder: + _select(self._test_result, self._paths, TEST_RESULT_UPDATE_FIELDS, name, value, other) + return self + + +class StepUpdateBuilder: + """Builds an update for an existing step. + + Each with method sets a field and adds it to the update mask. Only the + selected fields change. None clears a selected field. For the test, set by + test_id or by inline metadata, the last call wins and the other form is not + sent. Values are copied when each with method is called. + + Example: + update = StepUpdateBuilder(step).with_outcome(Outcome.PASSED).with_notes("Verified") + updated = data_store_client.update_step(update) + """ + + __slots__ = ("_step", "_paths") + + def __init__(self, step: Step | str | UUID) -> None: + """Start an update for a step without selecting any fields. + + Args: + step: The step to update, or its ID. + + Raises: + ValueError: If the ID is empty. + """ + self._step = StepProto(id=_target_id(step, "step")) + self._paths: dict[str, None] = {} + + def with_start_date_time(self, start_date_time: ht.datetime | None) -> StepUpdateBuilder: + """Set the start date and time, or clear it with None.""" + return self._select("start_date_time", start_date_time) + + def with_end_date_time(self, end_date_time: ht.datetime | None) -> StepUpdateBuilder: + """Set the end date and time, or clear it with None.""" + return self._select("end_date_time", end_date_time) + + def with_duration(self, duration: ht.timedelta | None) -> StepUpdateBuilder: + """Set the duration, or clear it with None. The service may not support this yet.""" + return self._select("duration", duration) + + def with_status(self, status: Status) -> StepUpdateBuilder: + """Set the lifecycle status.""" + return self._select("status", status) + + def with_outcome(self, outcome: Outcome) -> StepUpdateBuilder: + """Set the outcome.""" + return self._select("outcome", outcome) + + def with_error_information( + self, error_information: ErrorInformation | None + ) -> StepUpdateBuilder: + """Set the error information, or clear it with None.""" + return self._select("error_information", error_information) + + def with_extension(self, extension: Mapping[str, Any] | None) -> StepUpdateBuilder: + """Replace the whole extension JSON document, or clear it with None.""" + return self._select("extension", None if extension is None else dict(extension)) + + def with_notes(self, notes: str | None) -> StepUpdateBuilder: + """Set the notes, or clear them with None.""" + return self._select("notes", notes) + + def with_link(self, link: str | None) -> StepUpdateBuilder: + """Set the link, or clear it with None.""" + return self._select("link", link) + + def with_keywords(self, keywords: Iterable[str] | None) -> StepUpdateBuilder: + """Replace the keywords, or clear them with None. The service may not support this yet.""" + return self._select( + "keywords", None if keywords is None else string_list(keywords, "keywords") + ) + + def with_test_id(self, test_id: str | UUID | None) -> StepUpdateBuilder: + """Link a test by ID or alias, or unlink it with None.""" + return self._select("test_id", test_id, "test") + + def with_test(self, test: Test | None) -> StepUpdateBuilder: + """Link a test found or created from inline metadata, or unlink it with None.""" + return self._select("test", test, "test_id") + + def build_update(self) -> UpdateStepRequest: + """Build the update request. + + Raises: + ValueError: If no fields are selected. + """ + return UpdateStepRequest(step=self._step, update_mask=_update_mask(self._paths)) + + def _select(self, name: str, value: object, other: str = "") -> StepUpdateBuilder: + _select(self._step, self._paths, STEP_UPDATE_FIELDS, name, value, other) + return self + + +class MeasurementUpdateBuilder: + """Builds an update for an existing measurement. + + Each with method sets a field and adds it to the update mask. Only the + selected fields change. None clears a selected field. Values are copied when + each with method is called. The value, units, and limits cannot be updated. + + Example: + update = MeasurementUpdateBuilder(measurement).with_notes("Rechecked") + updated = data_store_client.update_measurement(update) + """ + + __slots__ = ("_measurement", "_paths") + + def __init__(self, measurement: Measurement | str | UUID) -> None: + """Start an update for a measurement without selecting any fields. + + Args: + measurement: The measurement to update, or its ID. + + Raises: + ValueError: If the ID is empty. + """ + self._measurement = MeasurementProto(id=_target_id(measurement, "measurement")) + self._paths: dict[str, None] = {} + + def with_start_date_time(self, start_date_time: ht.datetime | None) -> MeasurementUpdateBuilder: + """Set the start date and time, or clear it with None.""" + return self._select("start_date_time", start_date_time) + + def with_end_date_time(self, end_date_time: ht.datetime | None) -> MeasurementUpdateBuilder: + """Set the end date and time, or clear it with None.""" + return self._select("end_date_time", end_date_time) + + def with_duration(self, duration: ht.timedelta | None) -> MeasurementUpdateBuilder: + """Set the duration, or clear it with None. The service may not support this yet.""" + return self._select("duration", duration) + + def with_outcome(self, outcome: Outcome) -> MeasurementUpdateBuilder: + """Set the outcome.""" + return self._select("outcome", outcome) + + def with_error_information( + self, error_information: ErrorInformation | None + ) -> MeasurementUpdateBuilder: + """Set the error information, or clear it with None.""" + return self._select("error_information", error_information) + + def with_extension(self, extension: Mapping[str, Any] | None) -> MeasurementUpdateBuilder: + """Replace the whole extension JSON document, or clear it with None.""" + return self._select("extension", None if extension is None else dict(extension)) + + def with_notes(self, notes: str | None) -> MeasurementUpdateBuilder: + """Set the notes, or clear them with None.""" + return self._select("notes", notes) + + def build_update(self) -> UpdateMeasurementRequest: + """Build the update request. + + Raises: + ValueError: If no fields are selected. + """ + return UpdateMeasurementRequest( + measurement=self._measurement, update_mask=_update_mask(self._paths) + ) + + def _select(self, name: str, value: object) -> MeasurementUpdateBuilder: + _select(self._measurement, self._paths, MEASUREMENT_UPDATE_FIELDS, name, value, "") + return self + + +def _target_id(target: TestResult | Step | Measurement | str | UUID, resource_name: str) -> str: + resource_id = id_string(target) if isinstance(target, (str, UUID)) else target.id + if not resource_id.strip(): + raise ValueError(f"The {resource_name} ID must not be empty.") + return resource_id + + +def _select( + resource: TestResultProto | StepProto | MeasurementProto, + paths: dict[str, None], + allowed: frozenset[str], + name: str, + value: object, + other: str, +) -> None: + if other: + resource.ClearField(other) # type: ignore[arg-type] + paths.pop(other, None) + populate_update(resource, {name: value}, allowed) + paths[name] = None + + +def _update_mask(paths: Mapping[str, None]) -> FieldMask: + if not paths: + raise ValueError("At least one field must be selected before building an update.") + return FieldMask(paths=list(paths)) diff --git a/src/ni/datastore/data/v2/_update_conversion.py b/src/ni/datastore/data/v2/_update_conversion.py new file mode 100644 index 0000000..2b7f0de --- /dev/null +++ b/src/ni/datastore/data/v2/_update_conversion.py @@ -0,0 +1,158 @@ +"""Conversion of update fields to V2 update requests.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping +from typing import Any, cast +from uuid import UUID + +import hightime as ht +from google.protobuf import json_format +from google.protobuf.descriptor import Descriptor +from google.protobuf.field_mask_pb2 import FieldMask +from ni.measurements.data.v2.data_store_pb2 import ( + Measurement as MeasurementProto, + Step as StepProto, + TestResult as TestResultProto, +) +from ni.protobuf.types.precision_duration_conversion import hightime_timedelta_to_protobuf +from ni.protobuf.types.precision_timestamp_conversion import hightime_datetime_to_protobuf + +from ni.datastore.data.v2._grpc_conversion import id_string, string_list +from ni.datastore.data.v2._types._error_information import ErrorInformation + +# Mutable update-mask paths from the V2 mutability design, including Stage 2 resource fields. +TEST_RESULT_UPDATE_FIELDS = frozenset( + { + "uut_instance_id", + "uut_id", + "operator_id", + "test_station_id", + "test_description_id", + "uut_instance", + "uut", + "operator", + "test_station", + "test_description", + "start_date_time", + "end_date_time", + "duration", + "outcome", + "status", + "error_information", + "link", + "keywords", + "extension", + } +) +STEP_UPDATE_FIELDS = frozenset( + { + "test_id", + "test", + "notes", + "start_date_time", + "end_date_time", + "duration", + "outcome", + "status", + "error_information", + "link", + "keywords", + "extension", + } +) +MEASUREMENT_UPDATE_FIELDS = frozenset( + { + "notes", + "outcome", + "error_information", + "extension", + "start_date_time", + "end_date_time", + "duration", + } +) + +# Stage 2 fields that the service does not accept in an update mask yet. A whole-resource +# update selects them only when they have a value, so it still works with the current service. +_STAGE_2_UPDATE_FIELDS = frozenset({"uut_id", "uut", "keywords", "duration"}) + +# Relationships that are set by ID or alias, or by inline metadata, but never both. +TEST_RESULT_RELATIONSHIPS = { + "uut_instance_id": "uut_instance", + "uut_id": "uut", + "operator_id": "operator", + "test_station_id": "test_station", + "test_description_id": "test_description", +} +STEP_RELATIONSHIPS = {"test_id": "test"} + +# Update fields that take a metadata model, such as Operator, and are sent as its protobuf. +_METADATA_UPDATE_FIELDS = frozenset( + {"uut_instance", "uut", "operator", "test_station", "test_description", "test"} +) + + +def populate_update( + resource: TestResultProto | StepProto | MeasurementProto, + fields: Mapping[str, object], + allowed: frozenset[str], +) -> FieldMask: + """Copy update field values into a protobuf resource and return the update mask.""" + if not fields: + raise ValueError("At least one field must be supplied for an update.") + unknown_fields = fields.keys() - allowed + if unknown_fields: + raise ValueError(f"Fields cannot be updated: {', '.join(sorted(unknown_fields))}.") + + target = cast(Any, resource) + for name, value in fields.items(): + if value is None: + target.ClearField(name) + elif name in {"start_date_time", "end_date_time"}: + getattr(target, name).CopyFrom(hightime_datetime_to_protobuf(cast(ht.datetime, value))) + elif name == "duration": + target.duration.CopyFrom(hightime_timedelta_to_protobuf(cast(ht.timedelta, value))) + elif name == "keywords": + target.keywords[:] = string_list(cast(Iterable[str], value), "keywords") + elif name == "extension": + json_format.ParseDict(dict(cast(Mapping[str, Any], value)), target.extension) + elif name == "error_information": + target.error_information.CopyFrom(cast(ErrorInformation, value).to_protobuf()) + elif name in _METADATA_UPDATE_FIELDS: + getattr(target, name).CopyFrom(cast(Any, value).to_protobuf()) + elif isinstance(value, UUID): + setattr(target, name, id_string(value)) + else: + setattr(target, name, value) + + return FieldMask(paths=list(fields)) + + +def resource_update_fields( + resource: object, + descriptor: Descriptor, + allowed: frozenset[str], + relationships: Mapping[str, str], +) -> dict[str, object]: + """Select every updatable field of a resource, in protobuf field order. + + Empty values are selected too, so the update clears them. Stage 2 fields are + selected only when they have a value. When a relationship has both an ID and + inline metadata, such as an expanded read, only the ID is sent. + """ + fields: dict[str, object] = {} + for field in descriptor.fields: + if field.name not in allowed: + continue + value = getattr(resource, field.name) + if field.name in _STAGE_2_UPDATE_FIELDS and not value: + continue + if field.name == "extension" and not value: + value = None + fields[field.name] = value + + for id_field, inline_field in relationships.items(): + if fields.get(id_field) and fields.get(inline_field) is not None: + fields[inline_field] = None + return fields diff --git a/tests/unit/data/test_v2_data_store_client.py b/tests/unit/data/test_v2_data_store_client.py new file mode 100644 index 0000000..72d7eed --- /dev/null +++ b/tests/unit/data/test_v2_data_store_client.py @@ -0,0 +1,1698 @@ +"""Tests for the V2 data store client.""" + +from __future__ import annotations + +import datetime as std_datetime +import uuid +from collections.abc import Callable +from typing import Any, cast +from unittest.mock import Mock + +import numpy as np +import pytest +from hightime import datetime, timedelta +from ni.measurements.data.v2 import data_store_pb2, data_store_service_pb2 +from ni.protobuf.types.precision_duration_conversion import hightime_timedelta_to_protobuf +from ni.protobuf.types.precision_timestamp_conversion import hightime_datetime_to_protobuf +from ni.protobuf.types.vector_conversion import vector_to_protobuf +from ni.protobuf.types.waveform_pb2 import DoubleAnalogWaveform +from nitypes.scalar import Scalar +from nitypes.vector import Vector +from nitypes.waveform import AnalogWaveform, Timing +from pytest_mock import MockerFixture + +from ni.datastore.data.v2 import ( + Condition, + DataStoreClient, + ErrorInformation, + JsonValue, + Measurement, + MeasurementField, + MeasurementUpdateBuilder, + MeasurementUpdateFields, + MeasurementValueField, + Outcome, + QueryResults, + Status, + Step, + StepField, + StepUpdateBuilder, + TestResult, + TestResultField, + TestResultUpdateBuilder, +) + +UTC = std_datetime.timezone.utc +TIMESTAMP = datetime(2026, 5, 6, 7, 8, 9, tzinfo=UTC) + + +@pytest.fixture +def service_client(mocker: MockerFixture) -> Any: + service_client = Mock() + # Default responses for tests that check only the request. Tests that check + # the returned model set their own response. + service_client.update_test_result.return_value = data_store_pb2.TestResult() + service_client.update_step.return_value = data_store_pb2.Step() + service_client.update_measurement.return_value = data_store_pb2.Measurement() + service_client.create_measurement.return_value = data_store_pb2.Measurement() + service_client.remove_test_result_metadata.return_value = ( + data_store_service_pb2.RemoveTestResultMetadataResponse() + ) + mocker.patch( + "ni.datastore.data.v2._data_store_client.DataStoreServiceClient", + return_value=service_client, + ) + return service_client + + +@pytest.fixture +def client(service_client: Any) -> DataStoreClient: + return DataStoreClient() + + +def _sent(service_method: Mock) -> Any: + return service_method.call_args.args[0] + + +def _read_response(service_client: Any, value: data_store_pb2.MeasurementValue) -> None: + service_client.read_measurement_values.return_value = ( + data_store_service_pb2.ReadMeasurementValuesResponse(value=value) + ) + + +# ---------------------------------------------------------------- create + + +def test___create_test_result___sends_model_and_returns_created_test_result( + client: DataStoreClient, service_client: Any +) -> None: + service_client.create_test_result.return_value = data_store_pb2.TestResult( + id="tr-id", name="Power test" + ) + test_result = TestResult( + "Power test", + uut_instance_id="uut-instance-id", + software_item_ids=["sw"], + start_date_time=TIMESTAMP, + duration=timedelta(seconds=2), + outcome=Outcome.PASSED, + keywords=["smoke"], + ) + + created = client.create_test_result(test_result) + + request = _sent(service_client.create_test_result) + assert isinstance(request, data_store_service_pb2.CreateTestResultRequest) + assert request.test_result == test_result.to_protobuf() + assert created == TestResult("Power test", id="tr-id") + + +def test___create_step___sends_model_with_conditions_and_returns_created_step( + client: DataStoreClient, service_client: Any +) -> None: + service_client.create_step.return_value = data_store_pb2.Step( + id="step-id", name="Measure voltage" + ) + step = Step( + "Measure voltage", + test_result_id="tr-id", + status=Status.RUNNING, + conditions=[Condition("temperature", 25.0)], + ) + + created = client.create_step(step) + + request = _sent(service_client.create_step) + assert isinstance(request, data_store_service_pb2.CreateStepRequest) + assert request.step == step.to_protobuf() + assert request.step.conditions[0].value.scalar.double_value == 25.0 + assert created == Step("Measure voltage", id="step-id") + + +def test___create_test_result___name_and_fields___sends_same_request_as_model( + client: DataStoreClient, service_client: Any +) -> None: + service_client.create_test_result.return_value = data_store_pb2.TestResult(id="tr-id") + + created = client.create_test_result( + "Power test", operator_id="operator-id", outcome=Outcome.PASSED, keywords=["smoke"] + ) + + expected = TestResult( + "Power test", operator_id="operator-id", outcome=Outcome.PASSED, keywords=["smoke"] + ) + assert _sent(service_client.create_test_result).test_result == expected.to_protobuf() + assert created.id == "tr-id" + + +def test___create_step___name_and_fields___sends_same_request_as_model( + client: DataStoreClient, service_client: Any +) -> None: + service_client.create_step.return_value = data_store_pb2.Step(id="step-id") + + created = client.create_step( + "Measure voltage", test_result_id="tr-id", conditions=[Condition("temperature", 25.0)] + ) + + expected = Step( + "Measure voltage", test_result_id="tr-id", conditions=[Condition("temperature", 25.0)] + ) + assert _sent(service_client.create_step).step == expected.to_protobuf() + assert created.id == "step-id" + + +def test___create_measurement___model___sends_same_request_as_fields( + client: DataStoreClient, service_client: Any +) -> None: + service_client.create_measurement.return_value = data_store_pb2.Measurement(id="m-id") + measurement = Measurement( + name="voltage", value=Scalar(5.0, "V"), step_id="step-id", outcome=Outcome.PASSED + ) + + created = client.create_measurement(measurement) + + from_fields = client.create_measurement( + "voltage", Scalar(5.0, "V"), "step-id", outcome=Outcome.PASSED + ) + first, second = [call.args[0] for call in service_client.create_measurement.call_args_list] + assert first.measurement == measurement.to_protobuf() + assert first == second + assert created.id == from_fields.id == "m-id" + + +@pytest.mark.parametrize( + "call", + [ + pytest.param( + lambda client: client.create_test_result(TestResult("t"), link="x"), + id="create_test_result", + ), + pytest.param(lambda client: client.create_step(Step("s"), notes="x"), id="create_step"), + pytest.param( + lambda client: client.create_measurement(Measurement(name="v"), 1.0), + id="create_measurement_positional", + ), + pytest.param( + lambda client: client.create_measurement(Measurement(name="v"), units="V"), + id="create_measurement_keyword", + ), + ], +) +def test___create___model_with_fields___raises_type_error_before_rpc( + client: DataStoreClient, service_client: Any, call: Callable[[DataStoreClient], object] +) -> None: + with pytest.raises(TypeError, match="not both"): + call(client) + + assert service_client.method_calls == [] + + +def test___add_condition___sends_condition_and_returns_step( + client: DataStoreClient, service_client: Any +) -> None: + service_client.add_condition.return_value = data_store_service_pb2.AddConditionResponse( + step=data_store_pb2.Step(id="step-id", name="Measure voltage") + ) + condition = Condition("supply-voltage", Scalar(5.0, "V"), condition_type="Nominal") + + step = client.add_condition("step-id", condition) + + request = _sent(service_client.add_condition) + assert request.step_id == "step-id" + assert request.condition == condition.to_protobuf() + assert request.condition.units == "V" + assert step == Step("Measure voltage", id="step-id") + + +def test___add_condition___rejects_unsupported_value_before_rpc( + client: DataStoreClient, service_client: Any +) -> None: + with pytest.raises(TypeError): + client.add_condition("step-id", Condition("config", [1, 2])) + + service_client.add_condition.assert_not_called() + + +def test___create_measurement___converts_value_and_metadata( + client: DataStoreClient, service_client: Any +) -> None: + response = data_store_pb2.Measurement(id="m-id", name="voltage", step_id="step-id", units="V") + response.value.scalar.double_value = 5.2 + service_client.create_measurement.return_value = response + + created = client.create_measurement( + "voltage", + 5.2, + "step-id", + units="V", + start_date_time=TIMESTAMP, + duration=timedelta(milliseconds=5), + outcome=Outcome.PASSED, + error_information=ErrorInformation(error_code=1, message="m", source="s"), + software_item_ids=["sw"], + ) + + request = _sent(service_client.create_measurement) + assert isinstance(request, data_store_service_pb2.CreateMeasurementRequest) + assert created == Measurement( + id="m-id", name="voltage", step_id="step-id", units="V", value=Scalar(5.2) + ) + assert (request.measurement.name, request.measurement.step_id) == ("voltage", "step-id") + assert request.measurement.units == "V" + assert request.measurement.value.scalar.double_value == 5.2 + assert request.measurement.start_date_time == hightime_datetime_to_protobuf(TIMESTAMP) + assert request.measurement.duration == hightime_timedelta_to_protobuf(timedelta(milliseconds=5)) + assert request.measurement.outcome == data_store_pb2.OUTCOME_PASSED + assert request.measurement.error_information.message == "m" + assert list(request.measurement.software_item_ids) == ["sw"] + + +def test___create_measurement___uses_scalar_units_when_units_not_given( + client: DataStoreClient, service_client: Any +) -> None: + service_client.create_measurement.return_value = data_store_pb2.Measurement(id="m-id") + + client.create_measurement("voltage", Scalar(5.2, "V"), "step-id") + + assert _sent(service_client.create_measurement).measurement.units == "V" + + +def test___create_measurement___keeps_given_units_for_scalar_value( + client: DataStoreClient, service_client: Any +) -> None: + service_client.create_measurement.return_value = data_store_pb2.Measurement(id="m-id") + + client.create_measurement("voltage", Scalar(5.2, "V"), "step-id", units="mV") + + assert _sent(service_client.create_measurement).measurement.units == "mV" + + +def test___create_measurement___accepts_inline_metadata_objects( + client: DataStoreClient, service_client: Any +) -> None: + from ni.datastore.metadata import HardwareItem, SoftwareItem, TestAdapter + + software = SoftwareItem(product="p", version="1") + hardware = HardwareItem(manufacturer="m", model="x", serial_number="1") + adapter = TestAdapter(name="adapter") + + client.create_measurement( + "voltage", + 5.0, + "step-id", + software_items=[software], + hardware_items=[hardware], + test_adapters=[adapter], + ) + + measurement = _sent(service_client.create_measurement).measurement + assert list(measurement.software_items) == [software.to_protobuf()] + assert list(measurement.hardware_items) == [hardware.to_protobuf()] + assert list(measurement.test_adapters) == [adapter.to_protobuf()] + + +def test___create_measurement___converts_mapping_to_json_value( + client: DataStoreClient, service_client: Any +) -> None: + client.create_measurement("configuration", {"enabled": True}, "step-id") + + value = _sent(service_client.create_measurement).measurement.value + assert value.WhichOneof("value") == "json_value" + assert value.json_value.value.fields["enabled"].bool_value is True + + +def test___create_measurement___converts_analog_waveform( + client: DataStoreClient, service_client: Any +) -> None: + waveform = AnalogWaveform( + sample_count=2, + raw_data=np.array([1.0, 2.0], dtype=np.float64), + timing=Timing.create_with_regular_interval(timedelta(seconds=1), TIMESTAMP), + ) + + client.create_measurement("signal", waveform, "step-id") + + value = _sent(service_client.create_measurement).measurement.value + assert value.WhichOneof("value") == "double_analog_waveform" + assert list(value.double_analog_waveform.y_data) == [1.0, 2.0] + assert value.double_analog_waveform.dt == 1.0 + + +def test___create_measurement___rejects_unsupported_value_before_rpc( + client: DataStoreClient, service_client: Any +) -> None: + with pytest.raises(TypeError, match="Unsupported measurement value type"): + client.create_measurement("measurement", object(), "step-id") # type: ignore[call-overload] + + service_client.create_measurement.assert_not_called() + + +# ---------------------------------------------------------------- read value + + +@pytest.mark.parametrize( + "read_source", + [ + pytest.param(Measurement(id="m-id"), id="measurement"), + pytest.param("m-id", id="measurement_id"), + ], +) +def test___read_measurement_value___requests_value_of_measurement_id( + client: DataStoreClient, service_client: Any, read_source: Measurement | str +) -> None: + value = data_store_pb2.MeasurementValue() + value.scalar.bool_value = True + _read_response(service_client, value) + + read_value = client.read_measurement_value(read_source) + + request = _sent(service_client.read_measurement_values) + assert request.measurement_id == "m-id" + assert list(request.read_mask.paths) == ["value"] + assert read_value == Scalar(True) + + +def test___read_measurement_value___returns_scalar_without_units( + client: DataStoreClient, service_client: Any +) -> None: + value = data_store_pb2.MeasurementValue() + value.scalar.double_value = 5.2 + _read_response(service_client, value) + + read_value = client.read_measurement_value(Measurement(id="m-id"), Scalar) + + assert (read_value.value, read_value.units) == (5.2, "") + + +def test___read_measurement_value___preserves_scalar_units( + client: DataStoreClient, service_client: Any +) -> None: + value = data_store_pb2.MeasurementValue() + value.scalar.double_value = 5.2 + value.scalar.attributes["NI_UnitDescription"].string_value = "V" + _read_response(service_client, value) + + read_value = client.read_measurement_value(Measurement(id="m-id"), Scalar) + + assert (read_value.value, read_value.units) == (5.2, "V") + + +def test___read_measurement_value___converts_vector( + client: DataStoreClient, service_client: Any +) -> None: + value = data_store_pb2.MeasurementValue() + value.vector.CopyFrom(vector_to_protobuf(Vector([1.0, 2.0]))) + _read_response(service_client, value) + + read_value = client.read_measurement_value(Measurement(id="m-id"), Vector) + + assert list(read_value) == [1.0, 2.0] + + +def test___read_measurement_value___converts_json( + client: DataStoreClient, service_client: Any +) -> None: + value = data_store_pb2.MeasurementValue() + value.json_value.value.update({"enabled": True}) + _read_response(service_client, value) + + read_value = client.read_measurement_value(Measurement(id="m-id")) + + assert read_value == {"enabled": True} + + +def test___read_measurement_value___converts_analog_waveform( + client: DataStoreClient, service_client: Any +) -> None: + _read_response( + service_client, + data_store_pb2.MeasurementValue(double_analog_waveform=DoubleAnalogWaveform(y_data=[1.0])), + ) + + read_value = client.read_measurement_value(Measurement(id="m-id"), AnalogWaveform) + + assert list(read_value.scaled_data) == [1.0] + + +def test___read_measurement_value___wrong_expected_type___raises( + client: DataStoreClient, service_client: Any +) -> None: + value = data_store_pb2.MeasurementValue() + value.scalar.double_value = 1.0 + _read_response(service_client, value) + + with pytest.raises(TypeError, match="Expected type"): + client.read_measurement_value(Measurement(id="m-id"), Vector) + + +# ---------------------------------------------------------------- get / query + + +def test___get_test_result___returns_model_with_python_values( + client: DataStoreClient, service_client: Any +) -> None: + proto = data_store_pb2.TestResult( + id="tr-id", + name="Power test", + start_date_time=hightime_datetime_to_protobuf(TIMESTAMP), + outcome=data_store_pb2.OUTCOME_PASSED, + ) + proto.extension.update({"lot": "A1"}) + service_client.get_test_result.return_value = proto + + test_result = client.get_test_result("tr-id", read_fields=["name", "extension"]) + + request = _sent(service_client.get_test_result) + assert request.test_result_id == "tr-id" + assert list(request.read_mask.paths) == ["name", "extension"] + assert isinstance(test_result, TestResult) + assert test_result.start_date_time == TIMESTAMP + assert test_result.outcome == Outcome.PASSED + assert test_result.extension == {"lot": "A1"} + + +def test___get_step___omits_empty_read_mask(client: DataStoreClient, service_client: Any) -> None: + service_client.get_step.return_value = data_store_pb2.Step(id="step-id", name="s") + + step = client.get_step("step-id") + + assert not _sent(service_client.get_step).HasField("read_mask") + assert step == Step("s", id="step-id") + + +def test___get_measurement___returns_model(client: DataStoreClient, service_client: Any) -> None: + service_client.get_measurement.return_value = data_store_pb2.Measurement( + id="m-id", name="voltage", units="V" + ) + + measurement = client.get_measurement("m-id") + + assert _sent(service_client.get_measurement).measurement_id == "m-id" + assert measurement == Measurement(id="m-id", name="voltage", units="V") + + +def test___get_measurement___propagates_transport_error( + client: DataStoreClient, service_client: Any +) -> None: + transport_error = RuntimeError("service unavailable") + service_client.get_measurement.side_effect = transport_error + + with pytest.raises(RuntimeError) as error: + client.get_measurement("m-id") + + assert error.value is transport_error + + +def test___query_test_results___returns_models( + client: DataStoreClient, service_client: Any +) -> None: + service_client.query_test_results.return_value = ( + data_store_service_pb2.QueryTestResultsResponse( + test_results=[data_store_pb2.TestResult(id="tr-id", name="Power test")] + ) + ) + odata_query = "$filter=Name eq 'Power test'" + + results = client.query_test_results(odata_query, read_fields=["name"]) + + request = _sent(service_client.query_test_results) + assert request.odata_query == odata_query + assert list(request.read_mask.paths) == ["name"] + assert results == [TestResult("Power test", id="tr-id")] + + +def test___query_steps___returns_models(client: DataStoreClient, service_client: Any) -> None: + service_client.query_steps.return_value = data_store_service_pb2.QueryStepsResponse( + steps=[data_store_pb2.Step(id="step-id", name="s")] + ) + odata_query = "$filter=Name eq 's'" + + steps = client.query_steps(odata_query) + + assert _sent(service_client.query_steps).odata_query == odata_query + assert steps == [Step("s", id="step-id")] + + +def test___query_measurements___returns_models( + client: DataStoreClient, service_client: Any +) -> None: + service_client.query_measurements.return_value = ( + data_store_service_pb2.QueryMeasurementsResponse( + measurements=[data_store_pb2.Measurement(id="m-id", name="voltage")] + ) + ) + odata_query = "$filter=Name eq 'voltage'" + + measurements = client.query_measurements(odata_query) + + assert _sent(service_client.query_measurements).odata_query == odata_query + assert measurements == [Measurement(id="m-id", name="voltage")] + + +@pytest.mark.parametrize( + ("method", "response_type"), + [ + ("query_test_results", "QueryTestResultsResponse"), + ("query_steps", "QueryStepsResponse"), + ("query_measurements", "QueryMeasurementsResponse"), + ], +) +def test___query___returns_next_link( + client: DataStoreClient, + service_client: Any, + method: str, + response_type: str, +) -> None: + getattr(service_client, method).return_value = getattr(data_store_service_pb2, response_type)( + next_link="https://next" + ) + + results = getattr(client, method)() + + assert isinstance(results, QueryResults) + assert (list(results), results.next_link) == ([], "https://next") + + +def test___query___next_link_is_empty_on_last_page( + client: DataStoreClient, service_client: Any +) -> None: + service_client.query_steps.return_value = data_store_service_pb2.QueryStepsResponse() + + results = client.query_steps() + + assert results.next_link == "" + + +def test___json_value_measurement___sends_and_reads_attributes( + client: DataStoreClient, service_client: Any +) -> None: + service_client.create_measurement.return_value = data_store_pb2.Measurement(id="m-id") + value = JsonValue({"serial": "SN-1"}, attributes={"source": "dmm", "rev": 2}) + + client.create_measurement("config", value, "step-id") + + sent = _sent(service_client.create_measurement).measurement.value + _read_response(service_client, sent) + read_value = client.read_measurement_value(Measurement(id="m-id")) + + assert read_value == value + + +# ---------------------------------------------------------------- update + + +def test___update_test_result___builds_mask_and_returns_model( + client: DataStoreClient, service_client: Any +) -> None: + service_client.update_test_result.return_value = data_store_pb2.TestResult( + id="tr-id", name="t", link="https://example.com", outcome=data_store_pb2.OUTCOME_FAILED + ) + + updated = client.update_test_result( + "tr-id", link="https://example.com", outcome=Outcome.FAILED, uut_id="uut-id" + ) + + request = _sent(service_client.update_test_result) + assert request.test_result.id == "tr-id" + assert request.test_result.link == "https://example.com" + assert request.test_result.outcome == data_store_pb2.OUTCOME_FAILED + assert request.test_result.uut_id == "uut-id" + assert list(request.update_mask.paths) == ["link", "outcome", "uut_id"] + assert updated.outcome == Outcome.FAILED + + +def test___update_step___builds_mask_and_returns_model( + client: DataStoreClient, service_client: Any +) -> None: + service_client.update_step.return_value = data_store_pb2.Step(id="step-id", notes="done") + + updated = client.update_step("step-id", notes="done", status=Status.DONE) + + request = _sent(service_client.update_step) + assert request.step.notes == "done" + assert request.step.status == data_store_pb2.STATUS_DONE + assert list(request.update_mask.paths) == ["notes", "status"] + assert updated.notes == "done" + + +def test___update_measurement___converts_error_information_and_extension( + client: DataStoreClient, service_client: Any +) -> None: + client.update_measurement( + "m-id", + error_information=ErrorInformation(error_code=2, message="boom"), + extension={"retest": True}, + ) + + request = _sent(service_client.update_measurement) + assert request.measurement.error_information.message == "boom" + assert request.measurement.extension["retest"] is True + assert list(request.update_mask.paths) == ["error_information", "extension"] + + +@pytest.mark.parametrize("field", ["start_date_time", "end_date_time"]) +def test___update_measurement___converts_timestamp( + client: DataStoreClient, service_client: Any, field: str +) -> None: + client.update_measurement("m-id", **cast(MeasurementUpdateFields, {field: TIMESTAMP})) + + request = _sent(service_client.update_measurement) + assert getattr(request.measurement, field) == hightime_datetime_to_protobuf(TIMESTAMP) + assert list(request.update_mask.paths) == [field] + + +@pytest.mark.parametrize( + ("method", "resource_field"), + [("update_test_result", "test_result"), ("update_step", "step")], +) +def test___update___replaces_keywords( + client: DataStoreClient, service_client: Any, method: str, resource_field: str +) -> None: + getattr(client, method)("resource-id", keywords=["b", "c"]) + + request = _sent(getattr(service_client, method)) + assert list(getattr(request, resource_field).keywords) == ["b", "c"] + assert list(request.update_mask.paths) == ["keywords"] + + +@pytest.mark.parametrize( + ("method", "resource_field"), + [ + ("update_test_result", "test_result"), + ("update_step", "step"), + ("update_measurement", "measurement"), + ], +) +def test___update___converts_duration( + client: DataStoreClient, service_client: Any, method: str, resource_field: str +) -> None: + duration = timedelta(seconds=3) + + getattr(client, method)("resource-id", duration=duration) + + request = _sent(getattr(service_client, method)) + assert getattr(request, resource_field).duration == hightime_timedelta_to_protobuf(duration) + assert list(request.update_mask.paths) == ["duration"] + + +def test___update___none_sends_field_in_mask_with_empty_value( + client: DataStoreClient, service_client: Any +) -> None: + client.update_test_result("tr-id", link=None) + + # The service clears a field that is in the update mask but has an empty value. + request = _sent(service_client.update_test_result) + assert request.test_result.link == "" + assert list(request.update_mask.paths) == ["link"] + + +def test___update___rejects_empty_update_before_rpc( + client: DataStoreClient, service_client: Any +) -> None: + with pytest.raises(ValueError, match="At least one field"): + client.update_measurement("m-id") + + service_client.update_measurement.assert_not_called() + + +@pytest.mark.parametrize( + ("method", "field"), + [ + ("update_test_result", "name"), + ("update_step", "step_type"), + ("update_measurement", "units"), + ], +) +def test___update___rejects_immutable_field_before_rpc( + client: DataStoreClient, service_client: Any, method: str, field: str +) -> None: + with pytest.raises(ValueError, match=f"Fields cannot be updated: {field}"): + getattr(client, method)("resource-id", **{field: "x"}) + + getattr(service_client, method).assert_not_called() + + +def test___update_step___builder___sends_builder_request( + client: DataStoreClient, service_client: Any +) -> None: + service_client.update_step.return_value = data_store_pb2.Step(id="step-id", notes="ok") + + updated = client.update_step(StepUpdateBuilder("step-id").with_notes("ok")) + + request = _sent(service_client.update_step) + assert request.step.id == "step-id" + assert request.step.notes == "ok" + assert list(request.update_mask.paths) == ["notes"] + assert updated.notes == "ok" + + +@pytest.mark.parametrize( + ("method", "builder"), + [ + ("update_test_result", TestResultUpdateBuilder("tr-id").with_link("x")), + ("update_step", StepUpdateBuilder("step-id").with_notes("x")), + ("update_measurement", MeasurementUpdateBuilder("m-id").with_notes("x")), + ], +) +def test___update___builder_with_keyword_fields___raises_type_error( + client: DataStoreClient, service_client: Any, method: str, builder: object +) -> None: + with pytest.raises(TypeError, match="builder"): + getattr(client, method)(builder, outcome=Outcome.PASSED) + + getattr(service_client, method).assert_not_called() + + +def test___update_step___resource_with_keyword_fields___updates_only_given_fields( + client: DataStoreClient, service_client: Any +) -> None: + step = Step(name="s", id="step-id", notes="old", link="https://example.com") + + client.update_step(step, outcome=Outcome.PASSED) + + request = _sent(service_client.update_step) + assert request.step.id == "step-id" + assert request.step.notes == "" + assert list(request.update_mask.paths) == ["outcome"] + + +def test___update_test_result___resource_only___selects_every_supported_field( + client: DataStoreClient, service_client: Any +) -> None: + test_result = TestResult( + name="ignored", + id="tr-id", + operator_id="operator-id", + link="https://example.com", + outcome=Outcome.PASSED, + schema_id="ignored-schema", + software_item_ids=["sw-id"], + ) + + client.update_test_result(test_result) + + request = _sent(service_client.update_test_result) + assert list(request.update_mask.paths) == [ + "uut_instance_id", + "operator_id", + "test_station_id", + "test_description_id", + "start_date_time", + "end_date_time", + "outcome", + "status", + "error_information", + "link", + "extension", + "uut_instance", + "operator", + "test_station", + "test_description", + ] + sent = request.test_result + assert sent.id == "tr-id" + assert sent.operator_id == "operator-id" + assert sent.link == "https://example.com" + assert sent.outcome == data_store_pb2.OUTCOME_PASSED + assert not sent.HasField("extension") + # Immutable and dedicated-RPC fields are not sent. + assert sent.name == "" + assert sent.schema_id == "" + assert list(sent.software_item_ids) == [] + + +def test___update_test_result___resource_only_with_stage_2_values___selects_them( + client: DataStoreClient, service_client: Any +) -> None: + test_result = TestResult( + name="t", id="tr-id", uut_id="uut-id", keywords=["smoke"], duration=timedelta(seconds=1) + ) + + client.update_test_result(test_result) + + paths = list(_sent(service_client.update_test_result).update_mask.paths) + assert {"uut_id", "keywords", "duration"} <= set(paths) + assert "uut" not in paths + + +def test___update_step___resource_only_with_id_and_inline___sends_id_only( + client: DataStoreClient, service_client: Any +) -> None: + from ni.datastore.metadata import Test + + step = Step(name="s", id="step-id", test_id="test-id", test=Test(name="expanded")) + + client.update_step(step) + + request = _sent(service_client.update_step) + assert request.step.test_id == "test-id" + assert not request.step.HasField("test") + assert {"test_id", "test"} <= set(request.update_mask.paths) + + +def test___update_measurement___resource_only___does_not_send_value( + client: DataStoreClient, service_client: Any +) -> None: + measurement = Measurement( + id="m-id", name="v", value=Vector([1.0, 2.0]), notes="checked", outcome=Outcome.PASSED + ) + + client.update_measurement(measurement) + + request = _sent(service_client.update_measurement) + assert list(request.update_mask.paths) == [ + "notes", + "start_date_time", + "end_date_time", + "outcome", + "error_information", + "extension", + ] + assert request.measurement.notes == "checked" + assert not request.measurement.HasField("value") + + +def test___batch_update_steps___builders_and_resources___sends_each_request( + client: DataStoreClient, service_client: Any +) -> None: + service_client.batch_update_steps.return_value = ( + data_store_service_pb2.BatchUpdateStepsResponse() + ) + + client.batch_update_steps( + [StepUpdateBuilder("a").with_notes("n"), Step(name="b", id="b", notes="m")] + ) + + first, second = _sent(service_client.batch_update_steps).requests + assert first.step.id == "a" + assert list(first.update_mask.paths) == ["notes"] + assert second.step.id == "b" + assert second.step.notes == "m" + assert "notes" in second.update_mask.paths + + +@pytest.mark.parametrize( + ("method", "updates"), + [ + ("batch_update_test_results", [TestResultUpdateBuilder("a").with_link("x")]), + ("batch_update_measurements", [MeasurementUpdateBuilder("a").with_notes("x")]), + ], +) +def test___batch_update___builders___sends_builder_requests( + client: DataStoreClient, service_client: Any, method: str, updates: list[Any] +) -> None: + client_method = getattr(service_client, method) + client_method.return_value = Mock(test_results=[], measurements=[]) + + getattr(client, method)(updates) + + [request] = _sent(client_method).requests + assert len(request.update_mask.paths) == 1 + + +def test___batch_update___empty_iterable___raises_value_error( + client: DataStoreClient, service_client: Any +) -> None: + with pytest.raises(ValueError, match="At least one update"): + client.batch_update_steps([]) + + service_client.batch_update_steps.assert_not_called() + + +# ---------------------------------------------------------------- delete + + +@pytest.mark.parametrize("force", [False, True]) +def test___delete_test_result___sends_id_and_force( + client: DataStoreClient, service_client: Any, force: bool +) -> None: + client.delete_test_result("tr-id", force=force) + + assert _sent(service_client.delete_test_result) == ( + data_store_service_pb2.DeleteTestResultRequest(test_result_id="tr-id", force=force) + ) + + +@pytest.mark.parametrize("force", [False, True]) +def test___delete_step___sends_id_and_force( + client: DataStoreClient, service_client: Any, force: bool +) -> None: + client.delete_step("step-id", force=force) + + assert _sent(service_client.delete_step) == data_store_service_pb2.DeleteStepRequest( + step_id="step-id", force=force + ) + + +def test___delete_measurement___sends_id(client: DataStoreClient, service_client: Any) -> None: + client.delete_measurement("m-id") + + assert _sent(service_client.delete_measurement) == ( + data_store_service_pb2.DeleteMeasurementRequest(measurement_id="m-id") + ) + + +def test___delete___defaults_to_no_force(client: DataStoreClient, service_client: Any) -> None: + client.delete_test_result("tr-id") + client.delete_step("step-id") + + assert _sent(service_client.delete_test_result).force is False + assert _sent(service_client.delete_step).force is False + + +def test___delete___resources___sends_their_ids( + client: DataStoreClient, service_client: Any +) -> None: + client.delete_test_result(TestResult("t", id="tr-id"), force=True) + client.delete_step(Step("s", id="step-id")) + client.delete_measurement(Measurement(id="m-id")) + + assert _sent(service_client.delete_test_result).test_result_id == "tr-id" + assert _sent(service_client.delete_test_result).force is True + assert _sent(service_client.delete_step).step_id == "step-id" + assert _sent(service_client.delete_measurement).measurement_id == "m-id" + + +def test___batch_delete___resources_and_ids___sends_ids_in_order( + client: DataStoreClient, service_client: Any +) -> None: + client.batch_delete_test_results([TestResult("t", id="a"), "b"], force=[True, False]) + client.batch_delete_steps([Step("s", id="c"), "d"]) + client.batch_delete_measurements(["e", Measurement(id="f")]) + + test_results = _sent(service_client.batch_delete_test_results).requests + assert [(r.test_result_id, r.force) for r in test_results] == [("a", True), ("b", False)] + assert [r.step_id for r in _sent(service_client.batch_delete_steps).requests] == ["c", "d"] + measurements = _sent(service_client.batch_delete_measurements).requests + assert [r.measurement_id for r in measurements] == ["e", "f"] + + +# ---------------------------------------------------------------- conditions and metadata links + + +def test___remove_test_result_metadata___sends_request_without_checking_measurements( + client: DataStoreClient, service_client: Any +) -> None: + client.remove_test_result_metadata( + "tr-id", software_item_ids=["sw"], hardware_item_ids=["hw"], test_adapter_ids=["ta"] + ) + + service_client.query_measurements.assert_not_called() + assert _sent(service_client.remove_test_result_metadata) == ( + data_store_service_pb2.RemoveTestResultMetadataRequest( + test_result_id="tr-id", + software_item_ids=["sw"], + hardware_item_ids=["hw"], + test_adapter_ids=["ta"], + ) + ) + + +def test___remove_condition___sends_name_and_returns_step( + client: DataStoreClient, service_client: Any +) -> None: + service_client.remove_condition.return_value = data_store_service_pb2.RemoveConditionResponse( + step=data_store_pb2.Step(id="step-id") + ) + + step = client.remove_condition("step-id", "temperature") + + assert _sent(service_client.remove_condition) == data_store_service_pb2.RemoveConditionRequest( + step_id="step-id", condition_name="temperature" + ) + assert step.id == "step-id" + + +@pytest.mark.parametrize( + ("method", "id_field", "response_type", "response_field"), + [ + ( + "add_test_result_metadata", + "test_result_id", + "AddTestResultMetadataResponse", + "test_result", + ), + ( + "remove_test_result_metadata", + "test_result_id", + "RemoveTestResultMetadataResponse", + "test_result", + ), + ( + "add_measurement_metadata", + "measurement_id", + "AddMeasurementMetadataResponse", + "measurement", + ), + ( + "remove_measurement_metadata", + "measurement_id", + "RemoveMeasurementMetadataResponse", + "measurement", + ), + ], +) +def test___metadata_link_methods___send_ids_and_return_model( + client: DataStoreClient, + service_client: Any, + method: str, + id_field: str, + response_type: str, + response_field: str, +) -> None: + response = getattr(data_store_service_pb2, response_type)() + getattr(response, response_field).id = "resource-id" + getattr(service_client, method).return_value = response + + result = getattr(client, method)( + "resource-id", software_item_ids=["sw"], hardware_item_ids=["hw"], test_adapter_ids=["ta"] + ) + + request = _sent(getattr(service_client, method)) + assert getattr(request, id_field) == "resource-id" + assert list(request.software_item_ids) == ["sw"] + assert list(request.hardware_item_ids) == ["hw"] + assert list(request.test_adapter_ids) == ["ta"] + assert result.id == "resource-id" + + +# ---------------------------------------------------------------- limits and value reads + + +def test___create_measurement___sends_limits_and_extra_fields( + client: DataStoreClient, service_client: Any +) -> None: + service_client.create_measurement.return_value = data_store_pb2.Measurement(id="m-id") + + client.create_measurement( + "voltage", + 5.0, + "step-id", + upper_limit=5.5, + lower_limit=Vector([4.5, 4.6]), + comparison_type="GELE", + extension={"lot": "A1"}, + schema_id="schema", + ) + + sent = _sent(service_client.create_measurement).measurement + assert sent.value.scalar.double_value == 5.0 + assert sent.upper_limit.scalar.double_value == 5.5 + assert sent.lower_limit.vector == vector_to_protobuf(Vector([4.5, 4.6])) + assert (sent.comparison_type, sent.schema_id) == ("GELE", "schema") + assert sent.extension["lot"] == "A1" + + +def test___create_measurement___omits_limits_when_not_given( + client: DataStoreClient, service_client: Any +) -> None: + service_client.create_measurement.return_value = data_store_pb2.Measurement(id="m-id") + + client.create_measurement("voltage", 5.0, "step-id") + + sent = _sent(service_client.create_measurement).measurement + assert not sent.HasField("upper_limit") + assert not sent.HasField("lower_limit") + + +@pytest.mark.parametrize( + "read_source", + [ + pytest.param(Measurement(id="m-id"), id="measurement"), + pytest.param("m-id", id="measurement_id"), + ], +) +def test___read_measurement_values___returns_value_and_limits( + client: DataStoreClient, service_client: Any, read_source: Measurement | str +) -> None: + response = data_store_service_pb2.ReadMeasurementValuesResponse() + response.value.scalar.double_value = 5.0 + response.upper_limit.scalar.double_value = 5.5 + service_client.read_measurement_values.return_value = response + + result = client.read_measurement_values(read_source) + + assert _sent(service_client.read_measurement_values) == ( + data_store_service_pb2.ReadMeasurementValuesRequest(measurement_id="m-id") + ) + assert (result.id, result.value, result.upper_limit, result.lower_limit) == ( + "m-id", + Scalar(5.0), + Scalar(5.5), + None, + ) + + +def test___read_measurement_values___sends_read_fields( + client: DataStoreClient, service_client: Any +) -> None: + service_client.read_measurement_values.return_value = ( + data_store_service_pb2.ReadMeasurementValuesResponse() + ) + + client.read_measurement_values( + Measurement(id="m-id"), read_fields=[MeasurementValueField.UPPER_LIMIT] + ) + + assert list(_sent(service_client.read_measurement_values).read_mask.paths) == ["upper_limit"] + + +def test___get_test_result___sends_read_field_enums_as_field_names( + client: DataStoreClient, service_client: Any +) -> None: + service_client.get_test_result.return_value = data_store_pb2.TestResult(id="tr-id") + + client.get_test_result("tr-id", read_fields=[TestResultField.NAME, TestResultField.OUTCOME]) + + assert list(_sent(service_client.get_test_result).read_mask.paths) == ["name", "outcome"] + + +def test___get_step___sends_read_field_enums_as_field_names( + client: DataStoreClient, service_client: Any +) -> None: + service_client.get_step.return_value = data_store_pb2.Step(id="step-id") + + client.get_step("step-id", read_fields=[StepField.NAME, StepField.CONDITIONS]) + + assert list(_sent(service_client.get_step).read_mask.paths) == ["name", "conditions"] + + +def test___query_measurements___accepts_read_field_enums_and_plain_names( + client: DataStoreClient, service_client: Any +) -> None: + service_client.query_measurements.return_value = ( + data_store_service_pb2.QueryMeasurementsResponse() + ) + + client.query_measurements(read_fields=[MeasurementField.NAME, "conditions"]) + + assert list(_sent(service_client.query_measurements).read_mask.paths) == [ + "name", + "conditions", + ] + + +# ---------------------------------------------------------------- batch create + + +def test___batch_create_test_results___sends_models_and_returns_created_test_results( + client: DataStoreClient, service_client: Any +) -> None: + service_client.batch_create_test_results.return_value = ( + data_store_service_pb2.BatchCreateTestResultsResponse( + test_results=[ + data_store_pb2.TestResult(id="a", name="tr-1"), + data_store_pb2.TestResult(id="b", name="tr-2"), + ] + ) + ) + nested = TestResult("tr-1", steps=[Step("s", measurements=[Measurement(name="m", value=1.0)])]) + + created = client.batch_create_test_results([nested, TestResult("tr-2")]) + + requests = _sent(service_client.batch_create_test_results).requests + assert [r.test_result.name for r in requests] == ["tr-1", "tr-2"] + assert requests[0].test_result.steps[0].measurements[0].value.scalar.double_value == 1.0 + assert created == [TestResult("tr-1", id="a"), TestResult("tr-2", id="b")] + + +def test___batch_create_steps___sends_models_and_returns_created_steps( + client: DataStoreClient, service_client: Any +) -> None: + service_client.batch_create_steps.return_value = ( + data_store_service_pb2.BatchCreateStepsResponse( + steps=[data_store_pb2.Step(id="a", name="parent")] + ) + ) + + created = client.batch_create_steps([Step("parent", children=[Step("child")])]) + + [request] = _sent(service_client.batch_create_steps).requests + assert request.step.children[0].name == "child" + assert created == [Step("parent", id="a")] + + +def test___batch_create_measurements___sends_values_and_returns_created_measurements( + client: DataStoreClient, service_client: Any +) -> None: + service_client.batch_create_measurements.return_value = ( + data_store_service_pb2.BatchCreateMeasurementsResponse( + measurements=[ + data_store_pb2.Measurement(id="a", name="m1"), + data_store_pb2.Measurement(id="b", name="m2"), + ] + ) + ) + + created = client.batch_create_measurements( + [ + Measurement(name="m1", step_id="s1", value=1.0, upper_limit=2.0), + Measurement(name="m2", step_id="s2", value="PASS"), + ] + ) + + requests = _sent(service_client.batch_create_measurements).requests + assert requests[0].measurement.upper_limit.scalar.double_value == 2.0 + assert requests[1].measurement.value.scalar.string_value == "PASS" + assert created == [Measurement(id="a", name="m1"), Measurement(id="b", name="m2")] + + +# ---------------------------------------------------------------- batch update + + +@pytest.mark.parametrize( + ("method", "response_type", "response_field", "resource_field", "proto_type"), + [ + ( + "batch_update_test_results", + "BatchUpdateTestResultsResponse", + "test_results", + "test_result", + data_store_pb2.TestResult, + ), + ("batch_update_steps", "BatchUpdateStepsResponse", "steps", "step", data_store_pb2.Step), + ( + "batch_update_measurements", + "BatchUpdateMeasurementsResponse", + "measurements", + "measurement", + data_store_pb2.Measurement, + ), + ], +) +def test___batch_update___sends_per_item_masks_and_returns_models( + client: DataStoreClient, + service_client: Any, + method: str, + response_type: str, + response_field: str, + resource_field: str, + proto_type: Any, +) -> None: + response = getattr(data_store_service_pb2, response_type)( + **{response_field: [proto_type(id="a"), proto_type(id="b")]} + ) + getattr(service_client, method).return_value = response + + results = getattr(client, method)( + {"a": {"outcome": Outcome.PASSED}, "b": {"extension": {"x": 1}, "outcome": None}} + ) + + first, second = _sent(getattr(service_client, method)).requests + assert getattr(first, resource_field).id == "a" + assert list(first.update_mask.paths) == ["outcome"] + assert getattr(first, resource_field).outcome == data_store_pb2.OUTCOME_PASSED + assert getattr(second, resource_field).id == "b" + assert list(second.update_mask.paths) == ["extension", "outcome"] + assert [r.id for r in results] == ["a", "b"] + + +@pytest.mark.parametrize( + "method", ["batch_update_test_results", "batch_update_steps", "batch_update_measurements"] +) +def test___batch_update___rejects_empty_or_invalid_before_rpc( + client: DataStoreClient, service_client: Any, method: str +) -> None: + with pytest.raises(ValueError, match="At least one update"): + getattr(client, method)({}) + with pytest.raises(ValueError, match="At least one field"): + getattr(client, method)({"a": {}}) + with pytest.raises(ValueError, match="Fields cannot be updated: name"): + getattr(client, method)({"a": {"name": "x"}}) + + getattr(service_client, method).assert_not_called() + + +def test___update_test_result___accepts_metadata_models( + client: DataStoreClient, service_client: Any +) -> None: + from ni.datastore.metadata import Operator, Uut + + client.update_test_result( + "tr-id", operator=Operator(name="Ada", role="Engineer"), uut=Uut(model_name="model") + ) + + sent = _sent(service_client.update_test_result) + assert sent.test_result.operator.name == "Ada" + assert sent.test_result.uut.model_name == "model" + assert list(sent.update_mask.paths) == ["operator", "uut"] + + +def test___batch_update_test_results___accepts_inline_uut( + client: DataStoreClient, service_client: Any +) -> None: + from ni.datastore.metadata import Uut + + service_client.batch_update_test_results.return_value = ( + data_store_service_pb2.BatchUpdateTestResultsResponse() + ) + + client.batch_update_test_results({"tr-id": {"uut": Uut(model_name="model")}}) + + request = _sent(service_client.batch_update_test_results).requests[0] + assert request.test_result.uut.model_name == "model" + assert list(request.update_mask.paths) == ["uut"] + + +# ---------------------------------------------------------------- batch delete + + +def test___batch_delete_test_results___applies_single_force_to_all( + client: DataStoreClient, service_client: Any +) -> None: + client.batch_delete_test_results(["a", "b"], force=True) + + requests = _sent(service_client.batch_delete_test_results).requests + assert [(r.test_result_id, r.force) for r in requests] == [("a", True), ("b", True)] + + +def test___batch_delete_steps___accepts_per_item_force( + client: DataStoreClient, service_client: Any +) -> None: + client.batch_delete_steps(["a", "b"], force=[True, False]) + + requests = _sent(service_client.batch_delete_steps).requests + assert [(r.step_id, r.force) for r in requests] == [("a", True), ("b", False)] + + +def test___batch_delete___rejects_mismatched_force_count( + client: DataStoreClient, service_client: Any +) -> None: + with pytest.raises(ValueError, match="Expected 2 force flags"): + client.batch_delete_steps(["a", "b"], force=[True]) + + service_client.batch_delete_steps.assert_not_called() + + +def test___batch_delete_measurements___sends_ids( + client: DataStoreClient, service_client: Any +) -> None: + client.batch_delete_measurements(["a", "b"]) + + requests = _sent(service_client.batch_delete_measurements).requests + assert [r.measurement_id for r in requests] == ["a", "b"] + + +# ---------------------------------------------------------------- string list arguments + + +@pytest.mark.parametrize( + ("argument", "call"), + [ + pytest.param( + "keywords", + lambda client: client.update_test_result("tr-id", keywords="smoke"), + id="update_test_result", + ), + pytest.param( + "keywords", + lambda client: client.update_step("step-id", keywords="smoke"), + id="update_step", + ), + pytest.param( + "keywords", + lambda client: client.batch_update_test_results({"tr-id": {"keywords": "smoke"}}), + id="batch_update_test_results", + ), + pytest.param( + "keywords", + lambda client: client.batch_update_steps({"step-id": {"keywords": "smoke"}}), + id="batch_update_steps", + ), + pytest.param( + "software_item_ids", + lambda client: client.create_measurement("v", 1.0, "step-id", software_item_ids="sw"), + id="create_measurement", + ), + pytest.param( + "software_item_ids", + lambda client: client.add_test_result_metadata("tr-id", software_item_ids="sw"), + id="add_test_result_metadata", + ), + pytest.param( + "hardware_item_ids", + lambda client: client.remove_test_result_metadata("tr-id", hardware_item_ids="hw"), + id="remove_test_result_metadata", + ), + pytest.param( + "test_adapter_ids", + lambda client: client.add_measurement_metadata("m-id", test_adapter_ids="ta"), + id="add_measurement_metadata", + ), + pytest.param( + "software_item_ids", + lambda client: client.remove_measurement_metadata("m-id", software_item_ids="sw"), + id="remove_measurement_metadata", + ), + pytest.param( + "read_fields", + lambda client: client.get_test_result("tr-id", read_fields="name"), + id="get_test_result", + ), + pytest.param( + "read_fields", + lambda client: client.get_step("step-id", read_fields="name"), + id="get_step", + ), + pytest.param( + "read_fields", + lambda client: client.get_measurement("m-id", read_fields="name"), + id="get_measurement", + ), + pytest.param( + "read_fields", + lambda client: client.query_test_results(read_fields="name"), + id="query_test_results", + ), + pytest.param( + "read_fields", + lambda client: client.query_steps(read_fields="name"), + id="query_steps", + ), + pytest.param( + "read_fields", + lambda client: client.query_measurements(read_fields="name"), + id="query_measurements", + ), + pytest.param( + "read_fields", + lambda client: client.read_measurement_values( + Measurement(id="m-id"), read_fields=MeasurementValueField.VALUE + ), + id="read_measurement_values", + ), + pytest.param( + "test_results", + lambda client: client.batch_delete_test_results("tr-id"), + id="batch_delete_test_results", + ), + pytest.param( + "steps", + lambda client: client.batch_delete_steps("step-id"), + id="batch_delete_steps", + ), + pytest.param( + "measurements", + lambda client: client.batch_delete_measurements("m-id"), + id="batch_delete_measurements", + ), + ], +) +def test___string_list_argument___single_string___raises_before_rpc( + client: DataStoreClient, + service_client: Any, + argument: str, + call: Callable[[DataStoreClient], object], +) -> None: + with pytest.raises(TypeError, match=f"^{argument} must be"): + call(client) + + assert service_client.method_calls == [] + + +# ---------------------------------------------------------------- UUID IDs + +ID = uuid.UUID("3f2b8c1e-5a7d-4e2b-9c1f-2d4e6a8b0c3d") +ID_TEXT = str(ID) + + +def test___get_methods___uuid___send_string_ids( + client: DataStoreClient, service_client: Any +) -> None: + service_client.get_test_result.return_value = data_store_pb2.TestResult() + service_client.get_step.return_value = data_store_pb2.Step() + service_client.get_measurement.return_value = data_store_pb2.Measurement() + + client.get_test_result(ID) + client.get_step(ID) + client.get_measurement(ID) + + assert _sent(service_client.get_test_result).test_result_id == ID_TEXT + assert _sent(service_client.get_step).step_id == ID_TEXT + assert _sent(service_client.get_measurement).measurement_id == ID_TEXT + + +def test___read_measurement_value___uuid___sends_string_id( + client: DataStoreClient, service_client: Any +) -> None: + client.create_measurement("v", 1.0, "step-id") + _read_response(service_client, _sent(service_client.create_measurement).measurement.value) + + client.read_measurement_value(ID) + assert _sent(service_client.read_measurement_values).measurement_id == ID_TEXT + client.read_measurement_values(uuid.UUID(int=1)) + assert _sent(service_client.read_measurement_values).measurement_id == str(uuid.UUID(int=1)) + + +def test___create_measurement___uuid_ids___sends_string_ids( + client: DataStoreClient, service_client: Any +) -> None: + client.create_measurement("v", 1.0, ID, software_item_ids=[ID]) + + sent = _sent(service_client.create_measurement).measurement + assert sent.step_id == ID_TEXT + assert list(sent.software_item_ids) == [ID_TEXT] + + +def test___create_step___uuid_fields___sends_string_ids( + client: DataStoreClient, service_client: Any +) -> None: + service_client.create_step.return_value = data_store_pb2.Step() + + client.create_step("s", test_result_id=ID, test_id=ID) + + sent = _sent(service_client.create_step).step + assert (sent.test_result_id, sent.test_id) == (ID_TEXT, ID_TEXT) + + +def test___conditions___uuid_step_id___sends_string_id( + client: DataStoreClient, service_client: Any +) -> None: + service_client.add_condition.return_value = data_store_service_pb2.AddConditionResponse() + service_client.remove_condition.return_value = data_store_service_pb2.RemoveConditionResponse() + + client.add_condition(ID, Condition("c", 1.0)) + client.remove_condition(ID, "c") + + assert _sent(service_client.add_condition).step_id == ID_TEXT + assert _sent(service_client.remove_condition).step_id == ID_TEXT + + +def test___update___uuid_target_and_id_fields___sends_string_ids( + client: DataStoreClient, service_client: Any +) -> None: + client.update_test_result(ID, operator_id=ID) + client.update_step(StepUpdateBuilder(ID).with_test_id(ID)) + + test_result = _sent(service_client.update_test_result).test_result + assert (test_result.id, test_result.operator_id) == (ID_TEXT, ID_TEXT) + step = _sent(service_client.update_step).step + assert (step.id, step.test_id) == (ID_TEXT, ID_TEXT) + + +def test___batch_update___uuid_keys___sends_string_ids( + client: DataStoreClient, service_client: Any +) -> None: + service_client.batch_update_measurements.return_value = ( + data_store_service_pb2.BatchUpdateMeasurementsResponse() + ) + + client.batch_update_measurements({ID: {"notes": "n"}}) + + assert _sent(service_client.batch_update_measurements).requests[0].measurement.id == ID_TEXT + + +def test___metadata_links___uuid_ids___sends_string_ids( + client: DataStoreClient, service_client: Any +) -> None: + service_client.add_test_result_metadata.return_value = ( + data_store_service_pb2.AddTestResultMetadataResponse() + ) + service_client.add_measurement_metadata.return_value = ( + data_store_service_pb2.AddMeasurementMetadataResponse() + ) + + client.add_test_result_metadata(ID, software_item_ids=[ID], test_adapter_ids=["alias"]) + client.add_measurement_metadata(ID, hardware_item_ids=[ID]) + + test_result_request = _sent(service_client.add_test_result_metadata) + assert test_result_request.test_result_id == ID_TEXT + assert list(test_result_request.software_item_ids) == [ID_TEXT] + assert list(test_result_request.test_adapter_ids) == ["alias"] + measurement_request = _sent(service_client.add_measurement_metadata) + assert measurement_request.measurement_id == ID_TEXT + assert list(measurement_request.hardware_item_ids) == [ID_TEXT] + + +def test___deletes___uuid_ids___send_string_ids( + client: DataStoreClient, service_client: Any +) -> None: + client.delete_test_result(ID) + client.delete_step(ID) + client.delete_measurement(ID) + client.batch_delete_steps([ID, "b"]) + + assert _sent(service_client.delete_test_result).test_result_id == ID_TEXT + assert _sent(service_client.delete_step).step_id == ID_TEXT + assert _sent(service_client.delete_measurement).measurement_id == ID_TEXT + assert [r.step_id for r in _sent(service_client.batch_delete_steps).requests] == [ + ID_TEXT, + "b", + ] + + +def test___id_list___single_uuid___raises_type_error_before_rpc( + client: DataStoreClient, service_client: Any +) -> None: + with pytest.raises(TypeError, match="^steps must be"): + client.batch_delete_steps(ID) # type: ignore[arg-type] + with pytest.raises(TypeError, match="^software_item_ids must be"): + client.add_test_result_metadata("tr-id", software_item_ids=ID) # type: ignore[arg-type] + + assert service_client.method_calls == [] + + +# ---------------------------------------------------------------- lifecycle + + +def test___init___forwards_connection_options(mocker: MockerFixture) -> None: + service_client_class = mocker.patch( + "ni.datastore.data.v2._data_store_client.DataStoreServiceClient" + ) + discovery_client, grpc_channel, grpc_channel_pool = Mock(), Mock(), Mock() + + DataStoreClient( + discovery_client=discovery_client, + grpc_channel=grpc_channel, + grpc_channel_pool=grpc_channel_pool, + ) + + service_client_class.assert_called_once_with( + discovery_client=discovery_client, + grpc_channel=grpc_channel, + grpc_channel_pool=grpc_channel_pool, + ) + + +def test___close___closes_underlying_client(client: DataStoreClient, service_client: Any) -> None: + client.close() + + service_client.close.assert_called_once_with() + + +def test___context_manager___closes_underlying_client(service_client: Any) -> None: + with DataStoreClient(): + pass + + service_client.close.assert_called_once_with() + + +def test___closed_client___raises_without_calling_service( + client: DataStoreClient, service_client: Any +) -> None: + client.close() + + with pytest.raises(RuntimeError, match="closed"): + client.get_measurement("m-id") + + service_client.get_measurement.assert_not_called() diff --git a/tests/unit/data/test_v2_grpc_conversion.py b/tests/unit/data/test_v2_grpc_conversion.py new file mode 100644 index 0000000..c289b05 --- /dev/null +++ b/tests/unit/data/test_v2_grpc_conversion.py @@ -0,0 +1,117 @@ +"""Tests for V2 measurement value conversion.""" + +from __future__ import annotations + +from typing import Any + +import numpy as np +import pytest +from ni.measurements.data.v2 import data_store_pb2 +from nitypes.complex import ComplexInt32DType +from nitypes.waveform import AnalogWaveform, ComplexWaveform, DigitalWaveform, Spectrum +from nitypes.xy_data import XYData +from pytest_mock import MockerFixture + +from ni.datastore.data.v2 import DataStoreClient +from ni.datastore.data.v2._grpc_conversion import ( + convert_measurement_value_from_protobuf, + measurement_value_to_protobuf, +) + + +def _data(value: Any) -> object: + if isinstance(value, (AnalogWaveform, ComplexWaveform)): + return value.raw_data.tolist() + if isinstance(value, Spectrum): + return value.data.tolist() + if isinstance(value, DigitalWaveform): + return value.data.tolist() + return [value.x_data.tolist(), value.y_data.tolist()] + + +@pytest.mark.parametrize( + ("value", "oneof_field"), + [ + ( + AnalogWaveform.from_array_1d(np.array([1.0, 2.0], np.float64), dtype=np.float64), + "double_analog_waveform", + ), + ( + AnalogWaveform.from_array_1d(np.array([1.0, 2.0], np.float32), dtype=np.float32), + "float_analog_waveform", + ), + ( + AnalogWaveform.from_array_1d(np.array([1, 2], np.int16), dtype=np.int16), + "i16_analog_waveform", + ), + ( + ComplexWaveform.from_array_1d(np.array([1 + 2j], np.complex128), dtype=np.complex128), + "double_complex_waveform", + ), + ( + ComplexWaveform.from_array_1d(np.array([1 + 2j], np.complex64), dtype=np.complex64), + "float_complex_waveform", + ), + ( + ComplexWaveform.from_array_1d( + np.array([(1, 2), (3, 4)], ComplexInt32DType), dtype=ComplexInt32DType + ), + "i16_complex_waveform", + ), + ( + Spectrum.from_array_1d(np.array([1.0, 2.0], np.float64), dtype=np.float64), + "double_spectrum", + ), + ( + Spectrum.from_array_1d(np.array([1.0, 2.0], np.float32), dtype=np.float32), + "float_spectrum", + ), + (DigitalWaveform.from_lines([[0, 1], [1, 0]], signal_count=2), "digital_waveform"), + (XYData.from_arrays_1d([1.0, 2.0], [3.0, 4.0], np.float64), "x_y_data"), + ], +) +def test___waveform_family_value___round_trips_with_matching_type_and_dtype( + value: Any, oneof_field: str +) -> None: + proto = measurement_value_to_protobuf(value) + + assert proto is not None + assert proto.WhichOneof("value") == oneof_field + read_back: Any = convert_measurement_value_from_protobuf(proto) + assert type(read_back) is type(value) + assert read_back.dtype == value.dtype + assert _data(read_back) == _data(value) + + +@pytest.mark.parametrize( + "value", + [ + AnalogWaveform.from_array_1d(np.array([1, 2], np.int32), dtype=np.int32), + Spectrum.from_array_1d(np.array([1, 2], np.int32), dtype=np.int32), + XYData.from_arrays_1d([1.0, 2.0], [3.0, 4.0], np.float32), + ], +) +def test___unsupported_dtype___raises_type_error(value: Any) -> None: + with pytest.raises(TypeError, match="Unsupported"): + measurement_value_to_protobuf(value) + + +def test___create_measurement_with_unsupported_dtype___raises_before_rpc( + mocker: MockerFixture, +) -> None: + service_client = mocker.Mock() + mocker.patch( + "ni.datastore.data.v2._data_store_client.DataStoreServiceClient", + return_value=service_client, + ) + value = AnalogWaveform.from_array_1d(np.array([1, 2], np.int32), dtype=np.int32) + + with pytest.raises(TypeError): + DataStoreClient().create_measurement("voltage", value, "step-id") + + service_client.create_measurement.assert_not_called() + + +def test___missing_value___raises_type_error() -> None: + with pytest.raises(TypeError): + convert_measurement_value_from_protobuf(data_store_pb2.MeasurementValue()) diff --git a/tests/unit/data/test_v2_types.py b/tests/unit/data/test_v2_types.py new file mode 100644 index 0000000..28577f3 --- /dev/null +++ b/tests/unit/data/test_v2_types.py @@ -0,0 +1,618 @@ +"""Tests for the V2 data store Python types.""" + +from __future__ import annotations + +import datetime as std_datetime +import inspect +import uuid +from collections.abc import Callable +from enum import Enum +from typing import Any + +import pytest +from google.protobuf import json_format +from hightime import datetime, timedelta +from ni.measurements.data.v2 import data_store_pb2, data_store_service_pb2 +from ni.protobuf.types.precision_duration_conversion import hightime_timedelta_to_protobuf +from ni.protobuf.types.precision_timestamp_conversion import hightime_datetime_to_protobuf +from nitypes.scalar import Scalar +from nitypes.vector import Vector + +from ni.datastore.data.v2 import ( + Condition, + ErrorInformation, + JsonValue, + Measurement, + MeasurementField, + MeasurementValueField, + Outcome, + Status, + Step, + StepCreateFields, + StepField, + StepUpdateFields, + TestResult, + TestResultCreateFields, + TestResultField, + TestResultUpdateFields, +) +from ni.datastore.data.v2._update_conversion import ( + STEP_UPDATE_FIELDS, + TEST_RESULT_UPDATE_FIELDS, +) +from ni.datastore.metadata import ( + HardwareItem, + Operator, + SoftwareItem, + Test, + TestAdapter, + TestDescription, + TestStation, + Uut, + UutInstance, +) + +UTC = std_datetime.timezone.utc +START = datetime(2026, 1, 2, 3, 4, 5, tzinfo=UTC) +END = datetime(2026, 1, 2, 3, 4, 15, tzinfo=UTC) +DURATION = timedelta(seconds=10) + + +def test___test_result___round_trips_through_protobuf() -> None: + test_result = TestResult( + "Power test", + id="test-result-id", + uut_instance_id="uut-instance-id", + uut_id="uut-id", + operator_id="operator-id", + test_station_id="station-id", + test_description_id="description-id", + software_item_ids=["sw"], + hardware_item_ids=["hw"], + test_adapter_ids=["ta"], + start_date_time=START, + end_date_time=END, + duration=DURATION, + outcome=Outcome.PASSED, + status=Status.DONE, + link="https://example.com/run", + keywords=["smoke"], + extension={"lot": "A1", "nested": {"ok": True}}, + error_information=ErrorInformation(error_code=-1, message="boom", source="e2e"), + ) + + proto = test_result.to_protobuf() + + assert proto.name == "Power test" + assert proto.uut_id == "uut-id" + assert list(proto.software_item_ids) == ["sw"] + assert proto.start_date_time == hightime_datetime_to_protobuf(START) + assert proto.duration == hightime_timedelta_to_protobuf(DURATION) + assert proto.outcome == data_store_pb2.OUTCOME_PASSED + assert proto.status == data_store_pb2.STATUS_DONE + assert list(proto.keywords) == ["smoke"] + assert json_format.MessageToDict(proto.extension) == {"lot": "A1", "nested": {"ok": True}} + assert proto.error_information.message == "boom" + assert TestResult.from_protobuf(proto) == test_result + + +def test___test_result___unset_optional_fields_read_as_default() -> None: + test_result = TestResult.from_protobuf(data_store_pb2.TestResult(id="id", name="name")) + + assert test_result.start_date_time is None + assert test_result.end_date_time is None + assert test_result.duration is None + assert test_result.error_information is None + assert test_result.extension == {} + assert test_result.outcome == Outcome.UNSPECIFIED + + +def test___step___round_trips_through_protobuf_including_conditions() -> None: + step = Step( + "Measure voltage", + id="step-id", + parent_step_id="parent-id", + test_result_id="test-result-id", + test_id="test-id", + step_type="Measurement", + notes="first", + start_date_time=START, + end_date_time=END, + duration=DURATION, + outcome=Outcome.FAILED, + status=Status.DONE, + link="https://example.com/step", + keywords=["k"], + extension={"inputs": {"v": 1}}, + conditions=[Condition("temperature", 25.0, condition_type="Environment", units="C")], + ) + + proto = step.to_protobuf() + + assert proto.parent_step_id == "parent-id" + assert proto.step_type == "Measurement" + assert proto.conditions[0].value.scalar.double_value == 25.0 + assert Step.from_protobuf(proto) == step + + +def test___measurement___reads_metadata_and_conditions_from_protobuf() -> None: + proto = data_store_pb2.Measurement( + id="measurement-id", + test_result_id="test-result-id", + step_id="step-id", + name="voltage", + value_type="Scalar", + units="V", + notes="note", + outcome=data_store_pb2.OUTCOME_PASSED, + software_item_ids=["sw"], + comparison_type="GELE", + start_date_time=hightime_datetime_to_protobuf(START), + conditions=[ + data_store_pb2.Condition(name="temperature", value=data_store_pb2.ConditionValue()) + ], + ) + proto.conditions[0].value.scalar.double_value = 25.0 + proto.value.scalar.double_value = 5.0 + + measurement = Measurement.from_protobuf(proto) + + assert (measurement.id, measurement.name, measurement.units) == ( + "measurement-id", + "voltage", + "V", + ) + assert (measurement.value_type, measurement.comparison_type) == ("Scalar", "GELE") + assert measurement.outcome == Outcome.PASSED + assert measurement.start_date_time == START + assert list(measurement.software_item_ids) == ["sw"] + assert measurement.conditions == (Condition("temperature", 25.0),) + + +def test___measurement___conditions_are_read_only() -> None: + condition = Condition("temperature", 25.0) + measurement = Measurement(conditions=[condition]) + + assert isinstance(measurement.conditions, tuple) + with pytest.raises(AttributeError): + getattr(measurement.conditions, "append")(Condition("humidity", 50.0)) + with pytest.raises(AttributeError): + getattr(measurement.conditions, "pop")(0) + + +def test___measurement___accepts_but_does_not_send_output_fields() -> None: + condition = Condition("temperature", 25.0) + measurement = Measurement( + id="measurement-id", + name="voltage", + step_id="step-id", + value_type="x", + conditions=[condition], + ) + + proto = measurement.to_protobuf() + + assert proto.id == "measurement-id" + assert measurement.value_type == "x" + assert measurement.conditions == (condition,) + assert proto.value_type == "" + assert len(proto.conditions) == 0 + + +def test___measurement___to_protobuf_omits_unset_value() -> None: + measurement = Measurement(name="voltage", step_id="step-id") + + proto = measurement.to_protobuf() + + assert not proto.HasField("value") + assert not proto.HasField("upper_limit") + assert not proto.HasField("lower_limit") + + +@pytest.mark.parametrize( + ("value", "field", "expected"), + [ + (5.5, "double_value", 5.5), + (7, "sint32_value", 7), + (True, "bool_value", True), + ("high", "string_value", "high"), + ], +) +def test___condition___converts_scalar_values(value: object, field: str, expected: object) -> None: + proto = Condition("cond", value).to_protobuf() + + assert getattr(proto.value.scalar, field) == expected + assert Condition.from_protobuf(proto).value == expected + + +def test___condition___uses_scalar_units_when_units_not_given() -> None: + proto = Condition("voltage", Scalar(3.3, "V")).to_protobuf() + + assert proto.units == "V" + assert proto.value.scalar.double_value == 3.3 + + +@pytest.mark.parametrize("model", ["condition", "measurement"]) +def test___scalar_value_on_construction___sets_units(model: str) -> None: + value = Scalar(3.4, "V") + item = Condition("Vcc", value) if model == "condition" else Measurement(name="Vcc", value=value) + + assert item.units == "V" + + +@pytest.mark.parametrize("model", ["condition", "measurement"]) +def test___scalar_value_with_given_units___keeps_given_units(model: str) -> None: + value = Scalar(3.4, "V") + item = ( + Condition("Vcc", value, units="mV") + if model == "condition" + else Measurement(name="Vcc", value=value, units="mV") + ) + + assert item.units == "mV" + + +@pytest.mark.parametrize("model", ["condition", "measurement"]) +def test___scalar_value_set_later___sets_empty_units(model: str) -> None: + item = Condition("Vcc", 3.4) if model == "condition" else Measurement(name="Vcc", value=3.4) + + item.value = Scalar(3.4, "V") + + assert item.units == "V" + + +@pytest.mark.parametrize("model", ["condition", "measurement"]) +def test___scalar_value_set_later___keeps_existing_units(model: str) -> None: + item = ( + Condition("Vcc", 3.4, units="mV") + if model == "condition" + else Measurement(name="Vcc", value=3.4, units="mV") + ) + + item.value = Scalar(3.4, "V") + + assert item.units == "mV" + + +def test___condition___scalar_with_only_units_reads_as_plain_value() -> None: + proto = Condition("voltage", Scalar(3.3, "V")).to_protobuf() + + condition = Condition.from_protobuf(proto) + + assert condition.value == 3.3 + assert not isinstance(condition.value, Scalar) + assert condition.units == "V" + + +def test___condition___scalar_extended_properties_round_trip() -> None: + proto = Condition( + "supply", Scalar(1.25, "V", extended_properties={"custom": "source"}) + ).to_protobuf() + + condition = Condition.from_protobuf(proto) + + assert isinstance(condition.value, Scalar) + assert condition.value.value == 1.25 + assert condition.value.extended_properties["custom"] == "source" + assert condition.units == "V" + + +def test___condition___converts_json_mapping() -> None: + proto = Condition("config", {"mode": "fast"}).to_protobuf() + + assert proto.value.WhichOneof("value") == "json_value" + assert Condition.from_protobuf(proto).value == {"mode": "fast"} + + +def test___condition___rejects_unsupported_value() -> None: + with pytest.raises(TypeError): + Condition("bad", [1, 2]).to_protobuf() + + +def test___error_information___round_trips_through_protobuf() -> None: + error = ErrorInformation(error_code=3, message="m", source="s") + + assert ErrorInformation.from_protobuf(error.to_protobuf()) == error + + +@pytest.mark.parametrize("enum_type", [Outcome, Status]) +def test___enums___reject_unknown_protobuf_values(enum_type: type[Outcome] | type[Status]) -> None: + with pytest.raises(ValueError): + enum_type.from_protobuf(999) # type: ignore[arg-type] + + +@pytest.mark.parametrize( + ("field_type", "message_type"), + [ + (TestResultField, data_store_pb2.TestResult), + (StepField, data_store_pb2.Step), + (MeasurementField, data_store_pb2.Measurement), + (MeasurementValueField, data_store_service_pb2.ReadMeasurementValuesResponse), + ], +) +def test___read_field_enums___match_protobuf_fields( + field_type: type[Enum], message_type: Any +) -> None: + field_names = sorted(field.value for field in field_type) + + assert field_names == sorted(field.name for field in message_type.DESCRIPTOR.fields) + + +@pytest.mark.parametrize( + ("argument", "create_model"), + [ + pytest.param( + "keywords", lambda: TestResult("t", keywords="smoke"), id="test_result_keywords" + ), + pytest.param( + "software_item_ids", + lambda: TestResult("t", software_item_ids="sw"), + id="test_result_software_item_ids", + ), + pytest.param( + "hardware_item_ids", + lambda: TestResult("t", hardware_item_ids="hw"), + id="test_result_hardware_item_ids", + ), + pytest.param( + "test_adapter_ids", + lambda: TestResult("t", test_adapter_ids="ta"), + id="test_result_test_adapter_ids", + ), + pytest.param("keywords", lambda: Step("s", keywords="smoke"), id="step_keywords"), + pytest.param("path", lambda: Step("s", path="root"), id="step_path"), + pytest.param("path_ids", lambda: Step("s", path_ids="root-id"), id="step_path_ids"), + pytest.param( + "software_item_ids", + lambda: Measurement(software_item_ids="sw"), + id="measurement_software_item_ids", + ), + pytest.param( + "hardware_item_ids", + lambda: Measurement(hardware_item_ids="hw"), + id="measurement_hardware_item_ids", + ), + pytest.param( + "test_adapter_ids", + lambda: Measurement(test_adapter_ids="ta"), + id="measurement_test_adapter_ids", + ), + ], +) +def test___models___single_string_for_string_list___raises_type_error( + argument: str, create_model: Callable[[], object] +) -> None: + with pytest.raises(TypeError, match=f"^{argument} must be"): + create_model() + + +def test___measurement___round_trips_value_limits_and_metadata() -> None: + measurement = Measurement( + name="voltage", + step_id="step-id", + value=Scalar(5.0, "V"), + upper_limit=5.5, + lower_limit=Vector([4.5, 4.6]), + schema_id="schema", + software_items=[SoftwareItem(product="p", version="1")], + hardware_items=[HardwareItem(manufacturer="m", model="x", serial_number="1")], + test_adapters=[TestAdapter(name="adapter")], + ) + + proto = measurement.to_protobuf() + + assert proto.value.scalar.double_value == 5.0 + assert proto.upper_limit.scalar.double_value == 5.5 + assert proto.units == "V" + assert proto.schema_id == "schema" + assert proto.software_items[0].product == "p" + round_tripped = Measurement.from_protobuf(proto) + assert round_tripped.units == "V" + assert round_tripped.value == Scalar(5.0, "V") + assert round_tripped.upper_limit == Scalar(5.5) + assert round_tripped.lower_limit == Vector([4.5, 4.6]) + assert round_tripped.software_items == measurement.software_items + + +def test___measurement___moniker_or_missing_value_reads_as_none() -> None: + proto = data_store_pb2.Measurement(id="m") + proto.value.data_moniker.data_instance = 1 + + measurement = Measurement.from_protobuf(proto) + + assert (measurement.value, measurement.upper_limit, measurement.lower_limit) == ( + None, + None, + None, + ) + + +def test___test_result___round_trips_metadata_objects_and_nested_steps() -> None: + test_result = TestResult( + "Power test", + uut_instance=UutInstance(uut_id="uut", serial_number="sn"), + uut=Uut(model_name="model"), + operator=Operator(name="Ada", role="Engineer"), + test_station=TestStation(name="station"), + test_description=TestDescription(uut_id="uut", name="desc"), + software_items=[SoftwareItem(product="p", version="1")], + schema_id="schema", + steps=[ + Step( + "parent", + test=Test(name="t"), + schema_id="step-schema", + measurements=[Measurement(name="m", value=1.0)], + children=[Step("child")], + ) + ], + ) + + proto = test_result.to_protobuf() + + assert proto.operator.name == "Ada" + assert proto.steps[0].children[0].name == "child" + assert proto.steps[0].measurements[0].value.scalar.double_value == 1.0 + assert proto.steps[0].test.name == "t" + assert TestResult.from_protobuf(proto) == test_result + + +def test___measurement___plain_value_equals_scalar() -> None: + assert Measurement(value=1.0, upper_limit=2) == Measurement( + value=Scalar(1.0), upper_limit=Scalar(2) + ) + assert Measurement(value=1.0) != Measurement(value=Scalar(1.0, "V")) + + +def test___test_result___unset_metadata_objects_read_as_none() -> None: + test_result = TestResult.from_protobuf(data_store_pb2.TestResult(name="n")) + + assert (test_result.operator, test_result.uut_instance, test_result.uut) == (None, None, None) + assert test_result.steps == [] + + +def test___json_value___round_trips_attributes_and_schema() -> None: + value = JsonValue( + {"a": 1, "nested": {"ok": True}}, + attributes={"flag": True, "count": 3, "ratio": 0.5, "name": "x"}, + schema_id="schema", + ) + + proto = value.to_protobuf() + + assert proto.attributes["count"].integer_value == 3 + assert proto.attributes["flag"].bool_value is True + assert proto.schema_id == "schema" + assert JsonValue.from_protobuf(proto) == value + + +def test___json_value___rejects_unsupported_attribute() -> None: + with pytest.raises(TypeError): + JsonValue({}, attributes={"bad": [1]}).to_protobuf() # type: ignore[dict-item] + + +def test___condition___json_value_with_attributes_round_trips() -> None: + condition = Condition("config", JsonValue({"mode": "fast"}, attributes={"source": "fixture"})) + + assert Condition.from_protobuf(condition.to_protobuf()) == condition + + +def test___condition___plain_json_reads_as_dict() -> None: + proto = Condition("config", {"mode": "fast"}).to_protobuf() + + assert Condition.from_protobuf(proto).value == {"mode": "fast"} + + +@pytest.mark.parametrize( + ("proto_type", "model_type"), + [ + (data_store_pb2.TestResult, TestResult), + (data_store_pb2.Step, Step), + (data_store_pb2.Measurement, Measurement), + (data_store_pb2.Condition, Condition), + (data_store_pb2.ErrorInformation, ErrorInformation), + (data_store_pb2.JsonValue, JsonValue), + ], +) +def test___models___expose_every_protobuf_field(proto_type: Any, model_type: Any) -> None: + model_fields = {slot.lstrip("_") for slot in model_type.__slots__} + + missing = [f.name for f in proto_type.DESCRIPTOR.fields if f.name not in model_fields] + + assert missing == [] + + +def test___client___wraps_every_low_level_method() -> None: + from ni.measurements.data.v2.client import DataStoreClient as LowLevelClient + + from ni.datastore.data.v2 import DataStoreClient + + low_level_methods = { + name + for name in dir(LowLevelClient) + if not name.startswith("_") and callable(getattr(LowLevelClient, name)) + } + + assert sorted(low_level_methods - set(dir(DataStoreClient))) == [] + + +_OUTPUT_ONLY_INIT_PARAMETERS = {"id", "created_at", "updated_at", "created_by", "updated_by"} + + +@pytest.mark.parametrize( + ("model", "create_fields", "excluded"), + [ + (TestResult, TestResultCreateFields, {"name"}), + (Step, StepCreateFields, {"name", "path", "path_ids"}), + ], +) +def test___create_fields___match_model_constructor( + model: type, create_fields: type, excluded: set[str] +) -> None: + parameters = set(inspect.signature(model).parameters) + + expected = parameters - excluded - _OUTPUT_ONLY_INIT_PARAMETERS + assert set(create_fields.__annotations__) == expected + + +@pytest.mark.parametrize( + ("update_fields", "allowed"), + [ + (TestResultUpdateFields, TEST_RESULT_UPDATE_FIELDS), + (StepUpdateFields, STEP_UPDATE_FIELDS), + ], +) +def test___update_fields___match_updatable_fields( + update_fields: type, allowed: frozenset[str] +) -> None: + assert set(update_fields.__annotations__) == allowed + + +_UUID = uuid.UUID("3f2b8c1e-5a7d-4e2b-9c1f-2d4e6a8b0c3d") + + +def test___test_result___uuid_ids___stored_as_strings() -> None: + test_result = TestResult( + "t", + id=_UUID, + uut_instance_id=_UUID, + uut_id=_UUID, + operator_id=_UUID, + test_station_id=_UUID, + test_description_id=_UUID, + software_item_ids=[_UUID, "alias"], + hardware_item_ids=[_UUID], + test_adapter_ids=[_UUID], + ) + + proto = test_result.to_protobuf() + assert test_result.id == str(_UUID) + assert test_result.operator_id == str(_UUID) + assert list(test_result.software_item_ids) == [str(_UUID), "alias"] + assert proto.uut_instance_id == proto.test_description_id == str(_UUID) + assert list(proto.hardware_item_ids) == list(proto.test_adapter_ids) == [str(_UUID)] + + +def test___step___uuid_ids___stored_as_strings() -> None: + step = Step("s", id=_UUID, parent_step_id=_UUID, test_result_id=_UUID, test_id=_UUID) + + assert (step.id, step.parent_step_id, step.test_result_id, step.test_id) == (str(_UUID),) * 4 + + +def test___measurement___uuid_ids___stored_as_strings() -> None: + measurement = Measurement( + id=_UUID, + test_result_id=_UUID, + step_id=_UUID, + software_item_ids=[_UUID], + hardware_item_ids=[_UUID], + test_adapter_ids=[_UUID], + ) + + assert (measurement.id, measurement.test_result_id, measurement.step_id) == (str(_UUID),) * 3 + assert list(measurement.to_protobuf().software_item_ids) == [str(_UUID)] + + +def test___model___single_uuid_for_id_list___raises_type_error() -> None: + with pytest.raises(TypeError, match="^software_item_ids must be"): + TestResult("t", software_item_ids=_UUID) # type: ignore[arg-type] diff --git a/tests/unit/data/test_v2_update_builders.py b/tests/unit/data/test_v2_update_builders.py new file mode 100644 index 0000000..b4b7690 --- /dev/null +++ b/tests/unit/data/test_v2_update_builders.py @@ -0,0 +1,201 @@ +"""Tests for the V2 update builders.""" + +from __future__ import annotations + +import datetime as std_datetime + +import pytest +from hightime import datetime, timedelta +from ni.measurements.data.v2 import data_store_pb2 +from ni.protobuf.types.precision_duration_conversion import hightime_timedelta_to_protobuf +from ni.protobuf.types.precision_timestamp_conversion import hightime_datetime_to_protobuf + +from ni.datastore.data.v2 import ( + ErrorInformation, + Measurement, + MeasurementUpdateBuilder, + Outcome, + Status, + Step, + StepUpdateBuilder, + TestResult, + TestResultUpdateBuilder, +) +from ni.datastore.metadata import Operator, Test, Uut + +TIMESTAMP = datetime(2026, 5, 6, 7, 8, 9, tzinfo=std_datetime.timezone.utc) + + +def test___step_builder___build_update___sets_values_and_selects_fields_in_call_order() -> None: + request = ( + StepUpdateBuilder("step-id") + .with_outcome(Outcome.PASSED) + .with_notes("Verified") + .with_status(Status.DONE) + .build_update() + ) + + assert request.step.id == "step-id" + assert request.step.outcome == data_store_pb2.OUTCOME_PASSED + assert request.step.notes == "Verified" + assert request.step.status == data_store_pb2.STATUS_DONE + assert list(request.update_mask.paths) == ["outcome", "notes", "status"] + + +def test___test_result_builder___all_fields___converts_values() -> None: + request = ( + TestResultUpdateBuilder("tr-id") + .with_start_date_time(TIMESTAMP) + .with_end_date_time(TIMESTAMP) + .with_duration(timedelta(seconds=3)) + .with_status(Status.DONE) + .with_outcome(Outcome.FAILED) + .with_error_information(ErrorInformation(error_code=2, message="boom")) + .with_extension({"retest": True}) + .with_link("https://example.com") + .with_keywords(["smoke"]) + .with_uut_instance_id("uut-instance-id") + .with_uut_id("uut-id") + .with_operator_id("operator-id") + .with_test_station_id("station-id") + .with_test_description_id("description-id") + .build_update() + ) + + test_result = request.test_result + assert test_result.start_date_time == hightime_datetime_to_protobuf(TIMESTAMP) + assert test_result.end_date_time == hightime_datetime_to_protobuf(TIMESTAMP) + assert test_result.duration == hightime_timedelta_to_protobuf(timedelta(seconds=3)) + assert test_result.status == data_store_pb2.STATUS_DONE + assert test_result.outcome == data_store_pb2.OUTCOME_FAILED + assert test_result.error_information.message == "boom" + assert test_result.extension["retest"] is True + assert test_result.link == "https://example.com" + assert list(test_result.keywords) == ["smoke"] + assert test_result.uut_instance_id == "uut-instance-id" + assert test_result.uut_id == "uut-id" + assert test_result.operator_id == "operator-id" + assert test_result.test_station_id == "station-id" + assert test_result.test_description_id == "description-id" + assert len(request.update_mask.paths) == 14 + + +def test___measurement_builder___build_update___sets_values_and_selects_fields() -> None: + request = ( + MeasurementUpdateBuilder("m-id") + .with_notes("Rechecked") + .with_outcome(Outcome.PASSED) + .with_extension({"probe": "10x"}) + .build_update() + ) + + assert request.measurement.id == "m-id" + assert request.measurement.notes == "Rechecked" + assert request.measurement.outcome == data_store_pb2.OUTCOME_PASSED + assert request.measurement.extension["probe"] == "10x" + assert list(request.update_mask.paths) == ["notes", "outcome", "extension"] + + +def test___builder___none___selects_field_with_empty_value() -> None: + request = StepUpdateBuilder("step-id").with_link(None).with_notes(None).build_update() + + assert request.step.link == "" + assert request.step.notes == "" + assert list(request.update_mask.paths) == ["link", "notes"] + + +def test___builder___same_field_twice___last_value_wins() -> None: + request = StepUpdateBuilder("step-id").with_notes("first").with_notes("second").build_update() + + assert request.step.notes == "second" + assert list(request.update_mask.paths) == ["notes"] + + +def test___test_result_builder___id_after_inline___discards_inline() -> None: + request = ( + TestResultUpdateBuilder("tr-id") + .with_operator(Operator(name="Ada")) + .with_operator_id("operator-id") + .build_update() + ) + + assert request.test_result.operator_id == "operator-id" + assert not request.test_result.HasField("operator") + assert list(request.update_mask.paths) == ["operator_id"] + + +def test___test_result_builder___inline_after_id___discards_id() -> None: + request = ( + TestResultUpdateBuilder("tr-id") + .with_uut_id("uut-id") + .with_uut(Uut(model_name="model")) + .build_update() + ) + + assert request.test_result.uut_id == "" + assert request.test_result.uut.model_name == "model" + assert list(request.update_mask.paths) == ["uut"] + + +def test___step_builder___inline_test___sends_test_and_discards_id() -> None: + request = ( + StepUpdateBuilder("step-id") + .with_test_id("test-id") + .with_test(Test(name="t")) + .build_update() + ) + + assert request.step.test_id == "" + assert request.step.test.name == "t" + assert list(request.update_mask.paths) == ["test"] + + +def test___test_result_builder___from_resource___uses_resource_id() -> None: + request = ( + TestResultUpdateBuilder(TestResult(name="t", id="tr-id")) + .with_outcome(Outcome.PASSED) + .build_update() + ) + + assert request.test_result.id == "tr-id" + + +def test___step_builder___from_resource___uses_resource_id() -> None: + request = StepUpdateBuilder(Step(name="s", id="step-id")).with_notes("n").build_update() + + assert request.step.id == "step-id" + + +def test___measurement_builder___from_resource___uses_resource_id() -> None: + request = MeasurementUpdateBuilder(Measurement(id="m-id")).with_notes("n").build_update() + + assert request.measurement.id == "m-id" + + +@pytest.mark.parametrize("target", ["", " ", Step(name="unsaved")]) +def test___builder___blank_id___raises_value_error(target: Step | str) -> None: + with pytest.raises(ValueError, match="ID must not be empty"): + StepUpdateBuilder(target) + + +def test___builder___no_fields_selected___build_update_raises_value_error() -> None: + with pytest.raises(ValueError, match="At least one field"): + MeasurementUpdateBuilder("m-id").build_update() + + +def test___builder___single_string_keywords___raises_type_error() -> None: + with pytest.raises(TypeError, match="keywords must be a list"): + StepUpdateBuilder("step-id").with_keywords("smoke") + + +def test___builder___input_changed_after_with___request_uses_original_value() -> None: + extension = {"probe": "10x"} + keywords = ["smoke"] + builder = StepUpdateBuilder("step-id").with_extension(extension).with_keywords(keywords) + + extension["probe"] = "1x" + keywords.append("regression") + request = builder.build_update() + + assert request.step.extension["probe"] == "10x" + assert list(request.step.keywords) == ["smoke"]