Skip to content
Merged
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
54 changes: 47 additions & 7 deletions src/albert/collections/property_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ def update_property_on_inventory(
"""
existing_properties = self.get_properties_on_inventory(inventory_id=inventory_id)
existing_value = None
existing_id = None
for p in existing_properties.custom_property_data:
if p.data_column.data_column_id == property_data.data_column_id:
existing_value = (
Expand All @@ -296,6 +297,11 @@ def update_property_on_inventory(
)
existing_id = p.data_column.property_data.id
break
if existing_id is None:
# No property-data record exists for this column, so there is nothing
# to patch; add the value instead.
self.add_properties_to_inventory(inventory_id=inventory_id, properties=[property_data])
return self.get_properties_on_inventory(inventory_id=inventory_id)
if existing_value is not None:
payload = [
PropertyDataPatchDatum(
Expand Down Expand Up @@ -887,12 +893,20 @@ def void_interval_data(
lot_id : LotId, optional
A specific lot of the inventory item. Defaults to None.
data_template_id : DataTemplateId, optional
Limit voiding to a specific data template. Defaults to None (all).
Deprecated and ignored. Voiding always applies to every data
template in the interval.

Returns
-------
None
"""
if data_template_id is not None:
warnings.warn(
"data_template_id is deprecated and ignored; voiding applies to "
"every data template in the interval.",
DeprecationWarning,
stacklevel=2,
)
payload = {
"operation": "void",
"by": "intervalCombination",
Expand All @@ -901,7 +915,6 @@ def void_interval_data(
"inventoryId": inventory_id,
"blockId": block_id,
"lotId": lot_id,
"dataTemplateId": data_template_id,
}
payload = {k: v for k, v in payload.items() if v is not None}
self.session.patch(
Expand Down Expand Up @@ -948,12 +961,20 @@ def unvoid_interval_data(
lot_id : LotId, optional
A specific lot of the inventory item. Defaults to None.
data_template_id : DataTemplateId, optional
Limit unvoiding to a specific data template. Defaults to None (all).
Deprecated and ignored. Unvoiding always applies to every data
template in the interval.

Returns
-------
None
"""
if data_template_id is not None:
warnings.warn(
"data_template_id is deprecated and ignored; unvoiding applies to "
"every data template in the interval.",
DeprecationWarning,
stacklevel=2,
)
payload = {
"operation": "unvoid",
"by": "intervalCombination",
Expand All @@ -962,7 +983,6 @@ def unvoid_interval_data(
"inventoryId": inventory_id,
"blockId": block_id,
"lotId": lot_id,
"dataTemplateId": data_template_id,
}
payload = {k: v for k, v in payload.items() if v is not None}
self.session.patch(
Expand Down Expand Up @@ -1172,7 +1192,6 @@ def add_properties_to_task(
"inventoryId": inventory_id,
"lotId": lot_id,
"autoCalculate": "true",
"history": "true",
}
params = {k: v for k, v in params.items() if v is not None}
payload = (
Expand All @@ -1194,10 +1213,31 @@ def add_properties_to_task(
params=params,
)
response_json = response.json()
# The POST response is not in request order (existing trials are returned
# before new trials, and rejected values are dropped), so pair each
# response item back to its request property by identity, not position.
properties_by_key = {
(
prop.interval_combination,
prop.data_column.data_column_id,
prop.data_column.column_sequence,
prop.visible_trial_number,
): prop
for prop in properties
}
registered_properties: list[TaskPropertyCreate] = []
for prop, item in zip(properties, response_json, strict=False):
for item in response_json:
item_data = dict(item)
if "DataTemplate" not in item_data and prop.data_template:
item_column = item_data.get("DataColumns") or {}
prop = properties_by_key.get(
(
item_data.get("intervalCombination"),
item_column.get("id"),
item_column.get("columnId"),
item_data.get("visibleTrialNo"),
)
)
if prop is not None and "DataTemplate" not in item_data and prop.data_template:
item_data["DataTemplate"] = prop.data_template
registered_properties.append(TaskPropertyCreate(**item_data))
existing_data_rows = self.get_task_block_properties(
Expand Down
8 changes: 7 additions & 1 deletion src/albert/resources/property_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ class PropertyData(BaseAlbertModel):
value: str | None = Field(default=None)
"""The stored result value. All values are stored as strings in Albert."""

value_numeric: float | None = Field(default=None, alias="valueNumeric")
"""The numeric form of the value, when the column is numeric. Serialized as ``valueNumeric``."""

value_string: str | None = Field(default=None, alias="valueString")
"""The string form of the value, when the column is string-typed. Serialized as ``valueString``."""

value_type: str | None = Field(default=None, alias="valueType")
"""The type of the value (e.g. numeric, string, image, curve). Serialized as ``valueType``."""

Expand Down Expand Up @@ -837,7 +843,7 @@ def set_visible_trial_number(self) -> TaskPropertyCreate:
if self.trial_number is not None:
self.visible_trial_number = self.trial_number
else:
self.visible_trial_number = "1"
self.visible_trial_number = 1
return self


Expand Down
26 changes: 26 additions & 0 deletions tests/integration/collections/test_property_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,32 @@ def test_add_and_update_property_data_on_inventory(
assert r[0].data_columns[0].value == "55.5"


def test_update_property_on_inventory_adds_then_updates(
client: Albert,
seeded_inventory: list[BaseTask],
seeded_data_columns: list[BaseTask],
):
"""Test update adds the value when the item has none for the column, then updates it."""
inv = seeded_inventory[0]
column_id = seeded_data_columns[2].id

added = client.property_data.update_property_on_inventory(
inventory_id=inv.id,
property_data=InventoryDataColumn(data_column_id=column_id, value="11.1"),
)
match = [p for p in added.custom_property_data if p.data_column.data_column_id == column_id]
assert len(match) == 1
assert match[0].data_column.property_data.value == "11.1"

updated = client.property_data.update_property_on_inventory(
inventory_id=inv.id,
property_data=InventoryDataColumn(data_column_id=column_id, value="22.2"),
)
match = [p for p in updated.custom_property_data if p.data_column.data_column_id == column_id]
assert len(match) == 1
assert match[0].data_column.property_data.value == "22.2"


def test_task_property_calculation_evaluation(
client: Albert,
seed_prefix: str,
Expand Down
33 changes: 33 additions & 0 deletions tests/unit/resources/test_api_response_field_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
UnpackedProductDesign,
)
from albert.resources.projects import Project, ProjectClass, TaskConfig
from albert.resources.property_data import PropertyData, TaskDataColumn, TaskPropertyCreate
from albert.resources.substance import SubstanceInfo
from albert.resources.substance_v4 import SubstanceV4Info, SubstanceV4SearchItem

Expand Down Expand Up @@ -151,6 +152,38 @@ def test_project_old_api_params_uses_wire_name():
assert project.old_api_params == {"moNumber": "42"}


# Recorded from GET /api/v3/propertydata?entity=task (DataColumns[].PropertyData)
TASK_PROPERTY_DATA_PAYLOAD = {
"id": "PTD5495725",
"value": "12",
"valueNumeric": 12,
"valueString": "Text value for cell",
"valueType": "number",
}


def test_property_data_keeps_numeric_and_string_forms():
"""The typed value forms must survive validation, not be dropped as extras."""
property_data = PropertyData.model_validate(TASK_PROPERTY_DATA_PAYLOAD)

assert property_data.value == "12"
assert property_data.value_numeric == 12
assert property_data.value_string == "Text value for cell"


def test_task_property_create_sends_visible_trial_number_as_number():
"""``visibleTrialNo`` is a ``number`` in the API schema, not a string."""
prop = TaskPropertyCreate(
data_column=TaskDataColumn(data_column_id="DAC1", column_sequence="COL1"),
value="1.2",
)

payload = prop.model_dump(by_alias=True, exclude_none=True, mode="json")

assert isinstance(payload["visibleTrialNo"], int)
assert payload["visibleTrialNo"] == 1


def test_substance_info_tolerates_wider_v3_field_types():
"""``SubstanceInfo`` fields whose v3 payloads are wider than first modeled.

Expand Down
Loading