diff --git a/docs/collections/reference_attributes.md b/docs/collections/reference_attributes.md new file mode 100644 index 00000000..1c58eaf5 --- /dev/null +++ b/docs/collections/reference_attributes.md @@ -0,0 +1 @@ +::: albert.collections.reference_attributes.ReferenceAttributeCollection diff --git a/docs/resources/reference_attributes.md b/docs/resources/reference_attributes.md new file mode 100644 index 00000000..81a4b7ec --- /dev/null +++ b/docs/resources/reference_attributes.md @@ -0,0 +1 @@ +::: albert.resources.reference_attributes diff --git a/mkdocs.yml b/mkdocs.yml index 2764570f..c4c052f0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -161,6 +161,7 @@ nav: - Product Design: collections/product_design.md - Projects: collections/projects.md - Property Data: collections/property_data.md + - Reference Attributes: collections/reference_attributes.md - Reports: collections/reports.md - Roles: collections/roles.md - Storage Classes: collections/storage_classes.md @@ -204,6 +205,7 @@ nav: - Product Design: resources/product_design.md - Projects: resources/projects.md - Property Data: resources/property_data.md + - Reference Attributes: resources/reference_attributes.md - Reports: resources/reports.md - Roles: resources/roles.md - Storage Classes: resources/storage_classes.md diff --git a/src/albert/__init__.py b/src/albert/__init__.py index a426ca95..1fcbdcd0 100644 --- a/src/albert/__init__.py +++ b/src/albert/__init__.py @@ -4,4 +4,4 @@ __all__ = ["Albert", "AlbertClientCredentials", "AlbertSSOClient"] -__version__ = "1.9.6" +__version__ = "2.0.0b1" diff --git a/src/albert/client.py b/src/albert/client.py index 45a6d5dc..e1643a15 100644 --- a/src/albert/client.py +++ b/src/albert/client.py @@ -32,6 +32,7 @@ from albert.collections.product_design import ProductDesignCollection from albert.collections.projects import ProjectCollection from albert.collections.property_data import PropertyDataCollection +from albert.collections.reference_attributes import ReferenceAttributeCollection from albert.collections.report_templates import ReportTemplateCollection from albert.collections.reports import ReportCollection from albert.collections.roles import RoleCollection @@ -272,6 +273,10 @@ def parameters(self) -> ParameterCollection: def property_data(self) -> PropertyDataCollection: return PropertyDataCollection(session=self.session) + @property + def reference_attributes(self) -> ReferenceAttributeCollection: + return ReferenceAttributeCollection(session=self.session) + @property def product_design(self) -> ProductDesignCollection: return ProductDesignCollection(session=self.session) diff --git a/src/albert/collections/inventory.py b/src/albert/collections/inventory.py index bb36a094..e29f5189 100644 --- a/src/albert/collections/inventory.py +++ b/src/albert/collections/inventory.py @@ -13,17 +13,19 @@ from albert.core.shared.identifiers import ( InventoryId, ProjectId, + ReferenceAttributeId, SearchProjectId, WorksheetId, ) from albert.resources.facet import FacetItem from albert.resources.inventory import ( ALL_MERGE_MODULES, + InventoryAttribute, + InventoryAttributeList, + InventoryAttributeUpdate, InventoryCategory, InventoryItem, InventorySearchItem, - InventorySpec, - InventorySpecList, MergeInventory, ) from albert.resources.locations import Location @@ -249,22 +251,22 @@ def get_by_ids(self, *, ids: list[InventoryId]) -> list[InventoryItem]: return inventory @validate_call - def get_specs(self, *, ids: list[InventoryId]) -> list[InventorySpecList]: - """Get the specs for a list of inventory items. + def get_attributes(self, *, ids: list[InventoryId]) -> list[InventoryAttributeList]: + """Get inventory attributes for a list of inventory items. Parameters ---------- ids : list[InventoryId] - List of Inventory IDs to get the specs for. + List of Inventory IDs to get the attributes for. Returns ------- - list[InventorySpecList] - A list of InventorySpecList entities, each containing the specs for an inventory item. + list[InventoryAttributeList] + A list of InventoryAttributeList entities, each containing the attributes for an inventory item. """ - url = f"{self.base_path}/specs" + url = f"{self.base_path}/attributes" batches = [ids[i : i + 250] for i in range(0, len(ids), 250)] - ta = TypeAdapter(InventorySpecList) + ta = TypeAdapter(InventoryAttributeList) return [ ta.validate_python(item) for batch in batches @@ -272,38 +274,130 @@ def get_specs(self, *, ids: list[InventoryId]) -> list[InventorySpecList]: ] @validate_call - def add_specs( + def add_attributes( self, *, inventory_id: InventoryId, - specs: InventorySpec | list[InventorySpec], - ) -> InventorySpecList: - """Add inventory specs to the inventory item. - - An `InventorySpec` is a property that was not directly measured via a task, - but is a generic property of that inentory item. + attributes: InventoryAttribute | list[InventoryAttribute], + ) -> InventoryAttributeList: + """Add inventory attributes to the inventory item. Parameters ---------- inventory_id : InventoryId - The Albert ID of the inventory item to add the specs to - specs : list[InventorySpec] - List of InventorySpec entities to add to the inventory item, - which described the value and, optionally, - the conditions associated with the value (via workflow). + The Albert ID of the inventory item to add attributes for. + attributes : list[InventoryAttribute] + List of InventoryAttribute entities to add for the inventory item. Returns ------- - InventorySpecList - The list of InventorySpecs attached to the InventoryItem. + InventoryAttributeList + The list of InventoryAttributes attached to the InventoryItem. """ - if isinstance(specs, InventorySpec): - specs = [specs] + if isinstance(attributes, InventoryAttribute): + attributes = [attributes] response = self.session.put( - url=f"{self.base_path}/{inventory_id}/specs", - json=[x.model_dump(exclude_unset=True, by_alias=True, mode="json") for x in specs], + url=f"{self.base_path}/{inventory_id}/attributes", + json=[ + x.model_dump(exclude_unset=True, by_alias=True, mode="json") for x in attributes + ], ) - return InventorySpecList(**response.json()) + return InventoryAttributeList(**response.json()) + + @validate_call + def update_attributes( + self, + *, + inventory_id: InventoryId, + updates: InventoryAttributeUpdate | list[InventoryAttributeUpdate], + ) -> InventoryAttributeList: + """Update inventory attribute values. + + Parameters + ---------- + inventory_id : InventoryId + The Albert ID of the inventory item to update attributes for. + updates : list[InventoryAttributeUpdate] + The attribute updates to apply. + + Returns + ------- + InventoryAttributeList + The updated list of InventoryAttributes for the inventory item. + """ + if isinstance(updates, InventoryAttributeUpdate): + updates = [updates] + if not updates: + return self.get_attributes(ids=[inventory_id])[0] + + payload: list[dict[str, object]] = [] + for update in updates: + if update.reference_value is not None: + payload.append( + { + "operation": "update", + "attribute": "referenceValue", + "attributeId": update.attribute_id, + "newValue": update.reference_value, + } + ) + if update.clear_reference_value: + payload.append( + { + "operation": "delete", + "attribute": "referenceValue", + "attributeId": update.attribute_id, + } + ) + if update.range is not None: + payload.append( + { + "operation": "update", + "attribute": "range", + "attributeId": update.attribute_id, + "newValue": update.range.model_dump( + exclude_none=True, + by_alias=True, + mode="json", + ), + } + ) + if update.clear_range: + payload.append( + { + "operation": "delete", + "attribute": "range", + "attributeId": update.attribute_id, + } + ) + + if not payload: + return self.get_attributes(ids=[inventory_id])[0] + + self.session.patch( + url=f"{self.base_path}/{inventory_id}/attributes", + json=payload, + ) + return self.get_attributes(ids=[inventory_id])[0] + + @validate_call + def delete_attribute( + self, *, inventory_id: InventoryId, attribute_id: ReferenceAttributeId + ) -> None: + """Delete an inventory attribute row by ID. + + Parameters + ---------- + inventory_id : InventoryId + The Albert ID of the inventory item. + attribute_id : ReferenceAttributeId + The attribute ID to delete. + + Returns + ------- + None + """ + self.session.delete(f"{self.base_path}/{inventory_id}/attributes/{attribute_id}") @validate_call def delete(self, *, id: InventoryId) -> None: diff --git a/src/albert/collections/reference_attributes.py b/src/albert/collections/reference_attributes.py new file mode 100644 index 00000000..194e66be --- /dev/null +++ b/src/albert/collections/reference_attributes.py @@ -0,0 +1,167 @@ +from collections.abc import Iterator + +from pydantic import validate_call + +from albert.collections.base import BaseCollection +from albert.core.pagination import AlbertPaginator +from albert.core.session import AlbertSession +from albert.core.shared.enums import PaginationMode +from albert.core.shared.identifiers import ReferenceAttributeId +from albert.resources.reference_attributes import ReferenceAttribute + + +class ReferenceAttributeCollection(BaseCollection): + """ + ReferenceAttributeCollection is a collection class for managing reference attributes in Albert. + """ + + _api_version = "v3" + + def __init__(self, *, session: AlbertSession): + """ + Initializes the ReferenceAttributeCollection with the provided session. + + Parameters + ---------- + session : AlbertSession + The Albert session instance. + """ + super().__init__(session=session) + self.base_path = f"/api/{ReferenceAttributeCollection._api_version}/referenceattributes" + + def get_all( + self, + *, + start_key: str | None = None, + max_items: int | None = None, + ) -> Iterator[ReferenceAttribute]: + """ + Get all reference attributes with pagination support. + + Parameters + ---------- + start_key : str | None, optional + The pagination key to start from. + max_items : int | None, optional + Maximum number of items to return in total. If None, fetches all available items. + + Returns + ------- + Iterator[ReferenceAttribute] + An iterator of reference attributes. + """ + params = {"startKey": start_key} + params = {k: v for k, v in params.items() if v is not None} + + return AlbertPaginator( + mode=PaginationMode.KEY, + path=self.base_path, + session=self.session, + params=params, + max_items=max_items, + deserialize=lambda items: [ReferenceAttribute(**item) for item in items], + ) + + def create(self, *, reference_attribute: ReferenceAttribute) -> ReferenceAttribute: + """ + Create a reference attribute. + + Parameters + ---------- + reference_attribute : ReferenceAttribute + The reference attribute to create. + + Returns + ------- + ReferenceAttribute + The created reference attribute. + """ + payload = reference_attribute.model_dump( + by_alias=True, + exclude_none=True, + mode="json", + ) + if reference_attribute.data_column is not None and payload.get("datacolumnId") is None: + payload["datacolumnId"] = reference_attribute.data_column.id + payload.pop("datacolumn", None) + + if reference_attribute.unit is not None and payload.get("unitId") is None: + payload["unitId"] = reference_attribute.unit.id + payload.pop("unit", None) + + if reference_attribute.parameters is not None: + parameter_payloads = [] + for parameter in reference_attribute.parameters: + serialized = parameter.model_dump(by_alias=True, exclude_none=True, mode="json") + if parameter.unit is not None and serialized.get("unitId") is None: + serialized["unitId"] = parameter.unit.id + serialized.pop("unit", None) + parameter_payloads.append(serialized) + payload["parameters"] = parameter_payloads + + response = self.session.post(self.base_path, json=payload) + return ReferenceAttribute(**response.json()) + + @validate_call + def get_by_id(self, *, id: ReferenceAttributeId) -> ReferenceAttribute: + """ + Get a reference attribute by its ID. + + Parameters + ---------- + id : ReferenceAttributeId + The reference attribute ID. + + Returns + ------- + ReferenceAttribute + The reference attribute. + """ + response = self.session.get(f"{self.base_path}/{id}") + return ReferenceAttribute(**response.json()) + + @validate_call + def get_by_ids(self, *, ids: list[ReferenceAttributeId]) -> list[ReferenceAttribute]: + """ + Get reference attributes by their IDs. + + Parameters + ---------- + ids : list[ReferenceAttributeId] + The reference attribute IDs. + + Returns + ------- + list[ReferenceAttribute] + The reference attributes. + """ + if not ids: + return [] + url = f"{self.base_path}/ids" + batches = [ids[i : i + 100] for i in range(0, len(ids), 100)] + items: list[ReferenceAttribute] = [] + for batch in batches: + response = self.session.get(url, params={"id": batch}) + data = response.json() + if isinstance(data, list): + batch_items = data + else: + batch_items = data.get("items") or data.get("Items") or [] + items.extend(ReferenceAttribute(**item) for item in batch_items) + return items + + @validate_call + def delete(self, *, id: ReferenceAttributeId) -> None: + """ + Delete a reference attribute by its ID. + + Parameters + ---------- + id : ReferenceAttributeId + The reference attribute ID. + + Returns + ------- + None + """ + self.session.delete(f"{self.base_path}/{id}") diff --git a/src/albert/core/pagination.py b/src/albert/core/pagination.py index 9dbb714f..3246b557 100644 --- a/src/albert/core/pagination.py +++ b/src/albert/core/pagination.py @@ -67,7 +67,9 @@ def _create_iterator(self) -> Iterator[ItemType]: while True: response = self.session.get(self.path, params=self.params) data = response.json() - items = data.get("Items", []) + items = data.get("Items") + if items is None: + items = data.get("items", []) item_count = len(items) if not items and self.mode == PaginationMode.OFFSET: diff --git a/src/albert/core/shared/identifiers.py b/src/albert/core/shared/identifiers.py index 54ac9316..c30c385a 100644 --- a/src/albert/core/shared/identifiers.py +++ b/src/albert/core/shared/identifiers.py @@ -24,6 +24,7 @@ "ParameterId": "PRM", "ProjectId": "PRO", "PropertyDataId": "PTD", + "ReferenceAttributeId": "ATR", "ReportId": "REP", "RowId": "ROW", "RuleId": "RUL", @@ -242,6 +243,13 @@ def ensure_propertydata_id(id: str) -> str: PropertyDataId = Annotated[str, AfterValidator(ensure_propertydata_id)] +def ensure_reference_attribute_id(id: str) -> str: + return _ensure_albert_id(id, "ReferenceAttributeId") + + +ReferenceAttributeId = Annotated[str, AfterValidator(ensure_reference_attribute_id)] + + def ensure_task_id(id: str) -> str: return _ensure_albert_id(id, "TaskId") diff --git a/src/albert/resources/data_columns.py b/src/albert/resources/data_columns.py index cba67d42..fedabbd9 100644 --- a/src/albert/resources/data_columns.py +++ b/src/albert/resources/data_columns.py @@ -6,7 +6,7 @@ class DataColumn(BaseResource): name: str - defalt: bool = False + default: bool = False metadata: dict[str, MetadataItem] | None = Field(alias="Metadata", default=None) id: str = Field(default=None, alias="albertId") diff --git a/src/albert/resources/data_templates.py b/src/albert/resources/data_templates.py index 024dca5c..927931ce 100644 --- a/src/albert/resources/data_templates.py +++ b/src/albert/resources/data_templates.py @@ -25,6 +25,8 @@ class CSVMapping(BaseAlbertModel): class DataColumnValue(BaseAlbertModel): data_column: DataColumn = Field(exclude=True, default=None) data_column_id: str = Field(alias="id", default=None) + name: str | None = Field(default=None, exclude=True) + original_name: str | None = Field(default=None, alias="originalName", exclude=True) value: str | None = None hidden: bool = False unit: SerializeAsEntityLink[Unit] | None = Field(default=None, alias="Unit") diff --git a/src/albert/resources/inventory.py b/src/albert/resources/inventory.py index 3905c412..9e347975 100644 --- a/src/albert/resources/inventory.py +++ b/src/albert/resources/inventory.py @@ -5,7 +5,7 @@ from albert.core.base import BaseAlbertModel from albert.core.shared.enums import SecurityClass -from albert.core.shared.identifiers import InventoryId +from albert.core.shared.identifiers import InventoryId, ReferenceAttributeId from albert.core.shared.models.base import AuditFields from albert.core.shared.types import MetadataItem, SerializeAsEntityLink from albert.resources._mixins import HydrationMixin @@ -253,31 +253,64 @@ def validate_formula_fields(self) -> "InventoryItem": return self -class InventorySpecValue(BaseAlbertModel): +class InventoryAttributeRange(BaseAlbertModel): min: str | None = Field(default=None) max: str | None = Field(default=None) - reference: str | None = Field(default=None) comparison_operator: str | None = Field(default=None, alias="comparisonOperator") -class InventorySpec(BaseAlbertModel): +class InventoryAttributeValue(BaseAlbertModel): + min: str | None = Field(default=None) + max: str | None = Field(default=None) + reference: str | float | int | None = Field(default=None) + comparison_operator: str | None = Field(default=None, alias="comparisonOperator") + + +class InventoryAttribute(BaseAlbertModel): + reference_attribute_id: ReferenceAttributeId | None = Field( + default=None, alias="referenceAttributeId" + ) + reference_value: str | float | int | None = Field(default=None, alias="referenceValue") + range: InventoryAttributeRange | None = None + id: str | None = Field(default=None, alias="albertId") - name: str - data_column_id: str = Field(..., alias="datacolumnId") + name: str | None = None + data_column_id: str | None = Field(default=None, alias="datacolumnId") data_column_name: str | None = Field(default=None, alias="datacolumnName") - data_template_id: str | None = Field(default=None, alias="datatemplateId") - data_template_name: str | None = Field(default=None, alias="datatemplateName") + workflow_id: str | None = Field(default=None, alias="workflowId") unit_id: str | None = Field(default=None, alias="unitId") unit_name: str | None = Field(default=None, alias="unitName") - workflow_id: str | None = Field(default=None, alias="workflowId") - workflow_name: str | None = Field(default=None, alias="workflowName") - spec_config: str | None = Field(default=None, alias="specConfig") - value: InventorySpecValue | None = Field(default=None, alias="Value") + prm_count: int | None = Field(default=None, alias="prmCount") + validation: list[dict[str, Any]] | None = None + value: InventoryAttributeValue | None = Field(default=None, alias="Value") + + +class InventoryAttributeUpdate(BaseAlbertModel): + attribute_id: str = Field(alias="attributeId") + reference_value: str | float | int | None = Field(default=None, alias="referenceValue") + range: InventoryAttributeRange | None = None + clear_reference_value: bool = False + clear_range: bool = False + + @model_validator(mode="after") + def validate_update(self) -> "InventoryAttributeUpdate": + if self.reference_value is not None and self.clear_reference_value: + raise ValueError("Set either reference_value or clear_reference_value, not both.") + if self.range is not None and self.clear_range: + raise ValueError("Set either range or clear_range, not both.") + if ( + self.reference_value is None + and self.range is None + and not self.clear_reference_value + and not self.clear_range + ): + raise ValueError("At least one update or clear flag must be set.") + return self -class InventorySpecList(BaseAlbertModel): +class InventoryAttributeList(BaseAlbertModel): parent_id: str = Field(..., alias="parentId") - specs: list[InventorySpec] = Field(..., alias="Specs") + attributes: list[InventoryAttribute] = Field(default_factory=list, alias="attributes") # TODO: Find other pictogram items across the platform diff --git a/src/albert/resources/parameter_groups.py b/src/albert/resources/parameter_groups.py index 6d7ae15a..a92b39e6 100644 --- a/src/albert/resources/parameter_groups.py +++ b/src/albert/resources/parameter_groups.py @@ -39,6 +39,7 @@ class Operator(str, Enum): GREATER_THAN_OR_EQUAL = "gte" GREATER_THAN = "gt" EQUALS = "eq" + NOT_EQUALS = "neq" class EnumValidationValue(BaseAlbertModel): @@ -64,9 +65,9 @@ class EnumValidationValue(BaseAlbertModel): class ValueValidation(BaseAlbertModel): # We may want to abstract this out if we end up reusing on Data Templates datatype: DataType = Field(...) - value: str | list[EnumValidationValue] | None = Field(default=None) - min: str | None = Field(default=None) - max: str | None = Field(default=None) + value: float | int | str | list[EnumValidationValue] | None = Field(default=None) + min: float | int | str | None = Field(default=None) + max: float | int | str | None = Field(default=None) operator: Operator | None = Field(default=None) diff --git a/src/albert/resources/reference_attributes.py b/src/albert/resources/reference_attributes.py new file mode 100644 index 00000000..018fb96f --- /dev/null +++ b/src/albert/resources/reference_attributes.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +from pydantic import Field, model_validator + +from albert.core.base import BaseAlbertModel +from albert.core.shared.identifiers import ( + DataColumnId, + ParameterId, + ReferenceAttributeId, + UnitId, +) +from albert.core.shared.models.base import AuditFields, BaseResource +from albert.core.shared.types import SerializeAsEntityLink +from albert.resources.data_columns import DataColumn +from albert.resources.parameter_groups import ValueValidation +from albert.resources.parameters import ParameterCategory +from albert.resources.units import Unit +from albert.resources.workflows import Workflow + + +class ReferenceAttributeParameter(BaseAlbertModel): + """ + Parameter value attached to a reference attribute. + + Attributes + ---------- + id : ParameterId + The parameter identifier. + category : ParameterCategory | None + The parameter category. + value : str | float | int | None + The parameter value. + unit_id : UnitId | None + The unit identifier for the parameter value. + unit : SerializeAsEntityLink[Unit] | None + The unit information for the parameter value. + name : str | None + The parameter name. + """ + + id: ParameterId + category: ParameterCategory | None = None + value: str | float | int | None = None + unit_id: UnitId | None = Field(default=None, alias="unitId") + unit: SerializeAsEntityLink[Unit] | None = Field( + default=None, + alias="unit", + ) + name: str | None = None + + +class ReferenceAttribute(BaseResource): + """ + Represents a reference attribute. + + Attributes + ---------- + id : ReferenceAttributeId | None + The Albert identifier for the reference attribute. + reference_name : str | None + The reference attribute name. + full_name : str | None + The full name (data column name + reference attribute name). + name_override : bool | None + Whether the name was overridden. + data_column : SerializeAsEntityLink[DataColumn] | None + The linked data column. + data_column_id : DataColumnId | None + The linked data column identifier. + unit : SerializeAsEntityLink[Unit] | None + The linked unit. + unit_id : UnitId | None + The linked unit identifier. + workflow : SerializeAsEntityLink[Workflow] | None + The linked workflow. + validation : list[ValueValidation] | None + Validation rules for the reference attribute. + parameters : list[ReferenceAttributeParameter] | None + Parameter values for the reference attribute. + status : Status | None + The reference attribute status. + created : AuditFields | None + Audit fields for creation. + updated : AuditFields | None + Audit fields for last update. + """ + + id: ReferenceAttributeId | None = Field(default=None, alias="albertId") + reference_name: str | None = Field(default=None, alias="referenceName") + full_name: str | None = Field(default=None, alias="fullName") + name_override: bool | None = Field(default=None, alias="nameOverride") + data_column: SerializeAsEntityLink[DataColumn] | None = Field(default=None, alias="datacolumn") + data_column_id: DataColumnId | None = Field(default=None, alias="datacolumnId") + unit: SerializeAsEntityLink[Unit] | None = Field(default=None, alias="unit") + unit_id: UnitId | None = Field(default=None, alias="unitId") + workflow: SerializeAsEntityLink[Workflow] | None = Field(default=None, alias="workflow") + validation: list[ValueValidation] | None = None + parameters: list[ReferenceAttributeParameter] | None = None + + created: AuditFields | None = Field(default=None, alias="created", frozen=True) + updated: AuditFields | None = Field(default=None, alias="updated", frozen=True) + + @model_validator(mode="before") + @classmethod + def _normalize_parameters(cls, data: dict) -> dict: + if not isinstance(data, dict): + return data + parameters = data.get("parameters") or data.get("Parameters") + if isinstance(parameters, dict): + data["parameters"] = parameters.get("values", []) + return data + + @model_validator(mode="after") + def _normalize_linked_ids(self) -> ReferenceAttribute: + if self.data_column_id is None and self.data_column is not None: + object.__setattr__(self, "data_column_id", self.data_column.id) + if self.unit_id is None and self.unit is not None: + object.__setattr__(self, "unit_id", self.unit.id) + return self diff --git a/tests/collections/test_inventory.py b/tests/collections/test_inventory.py index fb7cefe3..529ed440 100644 --- a/tests/collections/test_inventory.py +++ b/tests/collections/test_inventory.py @@ -9,18 +9,16 @@ from albert.exceptions import BadRequestError from albert.resources.cas import Cas from albert.resources.companies import Company -from albert.resources.data_columns import DataColumn from albert.resources.facet import FacetItem, FacetValue from albert.resources.inventory import ( CasAmount, + InventoryAttribute, + InventoryAttributeUpdate, InventoryItem, - InventorySpec, - InventorySpecValue, InventoryUnitCategory, ) +from albert.resources.reference_attributes import ReferenceAttribute from albert.resources.tags import Tag -from albert.resources.units import Unit -from albert.resources.workflows import Workflow def assert_valid_inventory_items(returned_list: list[InventoryItem]): @@ -190,29 +188,6 @@ def test_blocks_dupes(caplog, client: Albert, seeded_inventory: list[InventoryIt ) -def test_add_property_to_inv_spec( - seed_prefix: str, - client: Albert, - seeded_inventory: list[InventoryItem], - seeded_data_columns: list[DataColumn], - seeded_units: list[Unit], - seeded_workflows: list[Workflow], -): - specs = [] - for dc in seeded_data_columns: - spec_to_add = InventorySpec( - name=f"{seed_prefix} -- {dc.name}", - data_column_id=dc.id, - unit_id=seeded_units[0].id, - value=InventorySpecValue(reference="42"), - workflow_id=seeded_workflows[0].id, - ) - specs.append(spec_to_add) - added_specs = client.inventory.add_specs(inventory_id=seeded_inventory[0].id, specs=specs) - assert len(added_specs.specs) == len(seeded_data_columns) - assert all([isinstance(x, InventorySpec) for x in added_specs.specs]) - - def test_update_inventory_item_standard_attributes( client: Albert, seeded_inventory: list[InventoryItem] ): @@ -398,3 +373,54 @@ def test_inventory_search_with_tags( tags = [x.tag for x in m.tags] assert any(t in tags for t in tags_to_check) + + +def test_inventory_attributes_add_update_delete( + client: Albert, + seeded_inventory: list[InventoryItem], + seeded_reference_attributes: list[ReferenceAttribute], +): + inventory_item = seeded_inventory[0] + reference_attribute = seeded_reference_attributes[0] + + added = client.inventory.add_attributes( + inventory_id=inventory_item.id, + attributes=InventoryAttribute( + reference_attribute_id=reference_attribute.id, + reference_value="5", + ), + ) + assert added.parent_id == inventory_item.id + added_attribute = next(attr for attr in added.attributes if attr.id == reference_attribute.id) + assert added_attribute.value is not None + assert str(added_attribute.value.reference) == "5" + + updated = client.inventory.update_attributes( + inventory_id=inventory_item.id, + updates=InventoryAttributeUpdate( + attribute_id=reference_attribute.id, + reference_value="10", + ), + ) + updated_attribute = next( + attr for attr in updated.attributes if attr.id == reference_attribute.id + ) + assert updated_attribute.value is not None + assert str(updated_attribute.value.reference) == "10" + + cleared = client.inventory.update_attributes( + inventory_id=inventory_item.id, + updates=InventoryAttributeUpdate( + attribute_id=reference_attribute.id, + clear_reference_value=True, + ), + ) + cleared_attribute = next( + attr for attr in cleared.attributes if attr.id == reference_attribute.id + ) + assert cleared_attribute.value is None or cleared_attribute.value.reference is None + + client.inventory.delete_attribute( + inventory_id=inventory_item.id, + attribute_id=reference_attribute.id, + ) diff --git a/tests/collections/test_reference_attributes.py b/tests/collections/test_reference_attributes.py new file mode 100644 index 00000000..edc0b263 --- /dev/null +++ b/tests/collections/test_reference_attributes.py @@ -0,0 +1,38 @@ +from albert.client import Albert +from albert.resources.reference_attributes import ReferenceAttribute + + +def assert_reference_attribute_items(returned_list: list[ReferenceAttribute]): + """Assert basic ReferenceAttribute structure and types.""" + assert returned_list, "Expected at least one ReferenceAttribute" + for item in returned_list[:10]: + assert isinstance(item, ReferenceAttribute) + assert isinstance(item.id, str) + + +def test_reference_attributes_get_all( + client: Albert, seeded_reference_attributes: list[ReferenceAttribute] +): + """Test retrieving reference attributes with pagination.""" + results = list(client.reference_attributes.get_all(max_items=5)) + assert_reference_attribute_items(results) + + +def test_reference_attributes_get_by_id( + client: Albert, seeded_reference_attributes: list[ReferenceAttribute] +): + """Test retrieving a reference attribute by id.""" + reference_attribute = seeded_reference_attributes[0] + fetched = client.reference_attributes.get_by_id(id=reference_attribute.id) + assert fetched.id == reference_attribute.id + assert fetched.reference_name == reference_attribute.reference_name + + +def test_reference_attributes_get_by_ids( + client: Albert, seeded_reference_attributes: list[ReferenceAttribute] +): + """Test retrieving reference attributes by bulk ids.""" + ids = [x.id for x in seeded_reference_attributes] + results = client.reference_attributes.get_by_ids(ids=ids) + assert len(results) == len(ids) + assert {x.id for x in results} == set(ids) diff --git a/tests/conftest.py b/tests/conftest.py index a825a4a6..480fbbcb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,6 +25,7 @@ from albert.resources.parameter_groups import ParameterGroup from albert.resources.parameters import Parameter from albert.resources.projects import Project +from albert.resources.reference_attributes import ReferenceAttribute from albert.resources.reports import FullAnalyticalReport from albert.resources.roles import Role from albert.resources.sheets import Component, Sheet @@ -59,6 +60,7 @@ generate_parameter_seeds, generate_pricing_seeds, generate_project_seeds, + generate_reference_attribute_seeds, generate_report_seeds, generate_storage_location_seeds, generate_tag_seeds, @@ -354,6 +356,29 @@ def seeded_data_columns( client.data_columns.delete(id=data_column.id) +@pytest.fixture(scope="session") +def seeded_reference_attributes( + client: Albert, + seed_prefix: str, + seeded_data_columns: list[DataColumn], +) -> Iterator[list[ReferenceAttribute]]: + seeded = [] + for reference_attribute in generate_reference_attribute_seeds( + seed_prefix=seed_prefix, + seeded_data_columns=seeded_data_columns, + ): + created_reference_attribute = client.reference_attributes.create( + reference_attribute=reference_attribute + ) + seeded.append(created_reference_attribute) + + yield seeded + + for reference_attribute in seeded: + with suppress(NotFoundError, BadRequestError): + client.reference_attributes.delete(id=reference_attribute.id) + + @pytest.fixture(scope="session") def seeded_data_templates( client: Albert, diff --git a/tests/seeding.py b/tests/seeding.py index 50c43eeb..e40e4ace 100644 --- a/tests/seeding.py +++ b/tests/seeding.py @@ -71,6 +71,7 @@ Project, ProjectClass, ) +from albert.resources.reference_attributes import ReferenceAttribute from albert.resources.reports import FullAnalyticalReport from albert.resources.storage_locations import StorageLocation from albert.resources.tags import Tag @@ -633,6 +634,32 @@ def generate_data_column_seeds(seed_prefix: str, seeded_units: list[Unit]) -> li ] +def generate_reference_attribute_seeds( + seed_prefix: str, + seeded_data_columns: list[DataColumn], +) -> list[ReferenceAttribute]: + """ + Generates a list of ReferenceAttribute seed objects for testing without IDs. + + Returns + ------- + list[ReferenceAttribute] + A list of ReferenceAttribute objects with different permutations. + """ + return [ + ReferenceAttribute( + reference_name=f"{seed_prefix} - Reference Attribute A", + data_column_id=seeded_data_columns[0].id, + validation=[ValueValidation(datatype=DataType.STRING)], + ), + ReferenceAttribute( + reference_name=f"{seed_prefix} - Reference Attribute B", + data_column_id=seeded_data_columns[1].id, + validation=[ValueValidation(datatype=DataType.STRING)], + ), + ] + + def generate_data_template_seeds( seed_prefix: str, user: User,