Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ echo

# Check for changes in Python files
# Compare against the remote branch being pushed to
python_changed_files=($(git diff --name-only --diff-filter=ACM @{upstream}... | grep '^python/' || true))

python_changed_files=($(git diff --name-only --diff-filter=ACM @{upstream}... | grep -E '^(python/|pyproject.toml)' || true))
echo "→ Python files changed:"
if [[ -n "$python_changed_files" ]]; then
echo " ${python_changed_files[*]}"
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/python_ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ jobs:
run: |
ruff format --check

- name: Type Check
run: |
ty check --python $(which python)

- name: MyPy
run: |
mypy lib
Expand Down
4 changes: 2 additions & 2 deletions python/lib/sift_client/_internal/low_level_wrappers/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ async def update_asset(self, update: AssetUpdate) -> Asset:
updated_grpc_asset = cast("UpdateAssetResponse", response).asset
return Asset._from_proto(updated_grpc_asset)

async def archive_asset(self, asset_id: str, archive_runs: bool = False) -> list[str] | None:
async def archive_asset(self, asset_id: str, archive_runs: bool = False) -> list[str]:
request = ArchiveAssetRequest(asset_id=asset_id, archive_runs=archive_runs)
response = await self._grpc_client.get_stub(AssetServiceStub).ArchiveAsset(request)
response = cast("ArchiveAssetResponse", response)
return response.archived_runs
return list(response.archived_run_ids)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from typing import Any, cast

from sift.calculated_channels.v2.calculated_channels_pb2 import (
CalculatedChannelValidationResult,
CreateCalculatedChannelResponse,
GetCalculatedChannelRequest,
GetCalculatedChannelResponse,
Expand Down Expand Up @@ -185,12 +184,8 @@ async def update_calculated_channel(
response = cast("UpdateCalculatedChannelResponse", response)

updated_calculated_channel = CalculatedChannel._from_proto(response.calculated_channel)
inapplicable_assets = [
cast("CalculatedChannelValidationResult", asset)
for asset in response.inapplicable_assets
]

return updated_calculated_channel, inapplicable_assets
return updated_calculated_channel, list(response.inapplicable_assets)

async def list_calculated_channel_versions(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class MockClassAsync(ResourceBase):
"""Mock async class docstring."""

def __init__(self, client=None):
super().__init__(client)
super().__init__(client) # ty: ignore[invalid-argument-type]

async def async_method(self, param1: str, *, param2: int = 0) -> str:
"""Mock async method docstring.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class MockResourceAsync(ResourceBase):
"""Mock async resource class for testing the sync wrapper."""

def __init__(self, client=None, value: str = "default"):
super().__init__(client)
super().__init__(client) # ty: ignore[invalid-argument-type]
self._value = value
self._calls: dict[str, int] = {}

Expand Down
40 changes: 26 additions & 14 deletions python/lib/sift_client/_tests/resources/test_test_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def test_create_test_report(self, sift_client, nostromo_run):
"part_number": "1234567890",
"start_time": simulated_time,
"end_time": simulated_time,
"run_id": nostromo_run.id_,
"run_id": nostromo_run._id_or_error,
},
)
assert test_report.id_ is not None
Expand All @@ -64,12 +64,14 @@ def test_create_test_steps(self, sift_client):
test_report = self.test_reports.get("basic_test_report")
if not test_report:
pytest.skip("Need to create a test report first")
return

simulated_time = test_report.start_time

# Create multiple test steps using TestStepCreate
step1 = sift_client.test_results.create_step(
TestStepCreate(
test_report_id=test_report.id_,
test_report_id=test_report._id_or_error,
name="Step 1: Initialization",
description="Initialize the test environment",
step_type=TestStepType.ACTION,
Expand All @@ -83,8 +85,8 @@ def test_create_test_steps(self, sift_client):
# Create a step using a dict
step1_1 = sift_client.test_results.create_step(
{
"test_report_id": test_report.id_,
"parent_step_id": step1.id_,
"test_report_id": test_report._id_or_error,
"parent_step_id": step1._id_or_error,
"name": "Step 1.1: Substep 1",
"description": "Substep 1 of Step 1",
"step_type": TestStepType.ACTION,
Expand All @@ -98,7 +100,7 @@ def test_create_test_steps(self, sift_client):

step2 = sift_client.test_results.create_step(
TestStepCreate(
test_report_id=test_report.id_,
test_report_id=test_report._id_or_error,
name="Step 2: Data Collection",
description="Collect sensor data",
step_type=TestStepType.ACTION,
Expand All @@ -111,7 +113,7 @@ def test_create_test_steps(self, sift_client):
simulated_time = simulated_time + timedelta(seconds=10.1)
step3 = sift_client.test_results.create_step(
TestStepCreate(
test_report_id=test_report.id_,
test_report_id=test_report._id_or_error,
name="Step 3: Validation",
description="Validate collected data",
step_type=TestStepType.ACTION,
Expand All @@ -124,8 +126,8 @@ def test_create_test_steps(self, sift_client):

step3_1 = sift_client.test_results.create_step(
TestStepCreate(
test_report_id=test_report.id_,
parent_step_id=step3.id_,
test_report_id=test_report._id_or_error,
parent_step_id=step3._id_or_error,
name="Step 3.1: Substep 3.1",
description="Error demo",
step_type=TestStepType.ACTION,
Expand Down Expand Up @@ -155,6 +157,8 @@ def test_update_test_steps(self, sift_client):
step3_1 = self.test_steps.get("step3_1")
if not step3 or not step3_1:
pytest.skip("Need to create a step first")
return

step3 = sift_client.test_results.update_step(
step3,
{"status": TestStatus.PASSED},
Expand All @@ -173,11 +177,12 @@ def test_create_test_measurements(self, sift_client):
step1_1 = self.test_steps.get("step1_1")
if not step1 or not step2 or not step3 or not step1_1:
pytest.skip("Need to create steps first")
return

# Create measurements for each step using TestMeasurementCreate
measurement1 = sift_client.test_results.create_measurement(
TestMeasurementCreate(
test_step_id=step1.id_,
test_step_id=step1._id_or_error,
name="Temperature Reading",
measurement_type=TestMeasurementType.DOUBLE,
numeric_value=25.5,
Expand All @@ -195,7 +200,7 @@ def test_create_test_measurements(self, sift_client):
# Create a measurement using a dict
measurement2 = sift_client.test_results.create_measurement(
{
"test_step_id": step2.id_,
"test_step_id": step2._id_or_error,
"name": "FW Version",
"measurement_type": TestMeasurementType.STRING,
"string_value": "1.10.3",
Expand All @@ -208,7 +213,7 @@ def test_create_test_measurements(self, sift_client):

measurement3 = sift_client.test_results.create_measurement(
TestMeasurementCreate(
test_step_id=step3.id_,
test_step_id=step3._id_or_error,
name="Status Check",
measurement_type=TestMeasurementType.BOOLEAN,
boolean_value=True,
Expand All @@ -218,9 +223,10 @@ def test_create_test_measurements(self, sift_client):
update_step=True,
)

assert step1_1.start_time
measurement4 = sift_client.test_results.create_measurement(
TestMeasurementCreate(
test_step_id=step1_1.id_,
test_step_id=step1_1._id_or_error,
name="Substep 1.1: Substep 1.1.1",
measurement_type=TestMeasurementType.BOOLEAN,
boolean_value=True,
Expand All @@ -244,6 +250,7 @@ def test_update_test_measurements(self, sift_client):
measurement4 = self.test_measurements.get("measurement4")
if not measurement2 or not measurement4:
pytest.skip("Need to create measurements first")
return

measurement2 = sift_client.test_results.update_measurement(
measurement2,
Expand Down Expand Up @@ -284,6 +291,8 @@ def test_update_test_report(self, sift_client):
test_report = self.test_reports.get("basic_test_report")
if not test_report:
pytest.skip("Need to create a test report first")
return

new_end_time = test_report.start_time + timedelta(seconds=42)
# Update the report with metadata
updated_report = sift_client.test_results.update(
Expand Down Expand Up @@ -341,6 +350,8 @@ def test_list_test_steps(self, sift_client):
if step.parent_step_id is not None:
existing_step = step
break

assert existing_step is not None
assert len(steps)
steps = sift_client.test_results.list_steps(
test_reports=[existing_step.test_report_id],
Expand All @@ -367,14 +378,15 @@ def test_archive_and_delete_test_report(self, sift_client):
test_report = self.test_reports.get("basic_test_report")
if not test_report:
pytest.skip("Need to create a test report first")
return

# Archive the report
archived_report = sift_client.test_results.archive(test_report=test_report)
assert archived_report.is_archived

sift_client.test_results.delete(test_report=test_report)
try:
deleted_report = sift_client.test_results.get(test_report_id=test_report.id_)
deleted_report = sift_client.test_results.get(test_report_id=test_report._id_or_error)
assert deleted_report is None # Shouldn't reach here so error if we get something.
except aiogrpc.AioRpcError as e:
self.test_reports.pop("basic_test_report")
Expand All @@ -389,7 +401,7 @@ def test_import_test_report(self, sift_client):

# Excercise find_report, custom_filter, and filtering by commonon-proto fields such as created_date
found_report = sift_client.test_results.find(
filter_query=f"test_report_id == '{test_report.id_}' && created_date >= timestamp('{create_time}')"
filter_query=f"test_report_id == '{test_report._id_or_error}' && created_date >= timestamp('{create_time}')"
)
assert found_report is not None
assert found_report.id_ == test_report.id_
Expand Down
1 change: 1 addition & 0 deletions python/lib/sift_client/_tests/sift_types/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ def _from_proto(cls, proto, sift_client=None):

# Should not raise
model = TestModel(name="test", created_date=datetime.now(timezone.utc))
assert model.created_date
assert model.created_date.tzinfo is not None

def test_validate_timezones_with_naive_datetime(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ def test_expression_validator_accepts_both_set(self):
],
)
assert update.expression == "$1 + $2"
assert update.expression_channel_references
assert len(update.expression_channel_references) == 2


Expand Down
1 change: 1 addition & 0 deletions python/lib/sift_client/_tests/sift_types/test_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def test_bitfield_validator_accepts_bitfield_with_elements(self):
],
)
assert channel.data_type == ChannelDataType.BIT_FIELD
assert channel.bit_field_elements
assert len(channel.bit_field_elements) == 2

def test_other_data_types_dont_require_special_fields(self):
Expand Down
3 changes: 3 additions & 0 deletions python/lib/sift_client/_tests/util/test_test_results_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ def test_evaluate_measurement_bounds_with_numeric_bounds_object(self):
result = evaluate_measurement_bounds(measurement, 5.0, bounds)
assert result == True
assert measurement.passed == True
assert measurement.numeric_bounds
assert measurement.numeric_bounds.min == 0.0
assert measurement.numeric_bounds.max == 10.0

Expand Down Expand Up @@ -324,6 +325,7 @@ def test_evaluate_measurement_bounds_boolean_matching(self):
assert result6 == True
assert measurement.passed == True
assert measurement.boolean_value == True
assert measurement.string_expected_value
assert measurement.string_expected_value.lower() == "true"

def test_evaluate_measurement_bounds_boolean_not_matching(self):
Expand All @@ -343,6 +345,7 @@ def test_evaluate_measurement_bounds_boolean_not_matching(self):
assert result6 == False
assert measurement.passed == False
assert measurement.boolean_value == False
assert measurement.string_expected_value
assert measurement.string_expected_value.lower() == "true"

def test_evaluate_measurement_bounds_boolean_case_insensitive(self):
Expand Down
4 changes: 2 additions & 2 deletions python/lib/sift_client/util/test_results/bounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ def assign_value_to_measurement(
def evaluate_measurement_bounds(
measurement: TestMeasurement | TestMeasurementCreate | TestMeasurementUpdate,
value: float | str | bool,
bounds: dict[str, float] | NumericBounds | str | bool | None,
bounds: dict[str, float] | NumericBounds | str | float | bool | None,
) -> bool:
"""Update a measurement with the resolved bounds type and result of evaluating the given value against those bounds.

Args:
measurement: The measurement to update.
value: The value to evaluate the bounds of.
bounds: The bounds to evaluate the value against. Either a dictionary with "min" and "max" keys, a NumericBounds object, a string, a boolean, or None.
bounds: The bounds to evaluate the value against. Either a dictionary with "min" and "max" keys, a NumericBounds object, a string, a float, a boolean, or None.

Returns:
True if the value is within the bounds, False otherwise.
Expand Down
3 changes: 2 additions & 1 deletion python/lib/sift_client/util/test_results/context_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,8 @@ def __exit__(self, exc, exc_value, tb):
self.update_step_from_result(exc, exc_value, tb)

# Now that the step is updated. Let the report context handle removing it from the stack and updating the report context.
self.report_context.exit_step(self.current_step)
if self.current_step:
self.report_context.exit_step(self.current_step)

return True

Expand Down
10 changes: 9 additions & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ dev = [
'pytest==8.2.2',
'ruff~=0.12.10',
'tomlkit~=0.13.3',
'ty~=0.0.4',
]
dev-all = [
'build==1.2.1',
Expand All @@ -103,6 +104,7 @@ dev-all = [
'sift-stream-bindings>=0.2.0-rc4',
'tomlkit~=0.13.3',
'types-pyOpenSSL<24.0.0',
'ty~=0.0.4',
]
development = [
'grpcio-testing~=1.13',
Expand All @@ -115,6 +117,7 @@ development = [
'pytest==8.2.2',
'ruff~=0.12.10',
'tomlkit~=0.13.3',
'ty~=0.0.4',
]
docs = [
'griffe-pydantic',
Expand Down Expand Up @@ -156,6 +159,7 @@ docs-build = [
'sift-stream-bindings>=0.2.0-rc4',
'tomlkit~=0.13.3',
'types-pyOpenSSL<24.0.0',
'ty~=0.0.4',
]
file-imports = [
'h5py~=3.11',
Expand Down Expand Up @@ -199,7 +203,8 @@ development = [
"pytest-mock==3.14.0",
"pytest-dotenv==0.5.2",
"ruff~=0.12.10",
"tomlkit~=0.13.3"
"tomlkit~=0.13.3",
"ty~=0.0.4"
]
build = ["pdoc==14.5.0", "build==1.2.1"]
docs = ["mkdocs",
Expand Down Expand Up @@ -316,6 +321,9 @@ sift_grafana = ["py.typed"]
sift_py = ["py.typed"]
sift_client = ["py.typed", "resources/sync_stubs/*.pyi"]

[tool.ty.src]
include = ["lib/sift_client"]

[tool.ruff]
line-length = 100
indent-width = 4
Expand Down
Loading