From 107ee317f381ac1b670aef7942107f0213d0e449 Mon Sep 17 00:00:00 2001 From: Prasad Date: Tue, 13 Jan 2026 15:06:39 +0530 Subject: [PATCH 1/4] feat: inventory function on cas --- src/albert/__init__.py | 2 +- src/albert/collections/inventory.py | 1 - src/albert/resources/inventory.py | 6 +++ src/albert/resources/lists.py | 3 +- src/albert/utils/inventory.py | 71 +++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 3 deletions(-) diff --git a/src/albert/__init__.py b/src/albert/__init__.py index 280296be..90c2d47f 100644 --- a/src/albert/__init__.py +++ b/src/albert/__init__.py @@ -4,4 +4,4 @@ __all__ = ["Albert", "AlbertClientCredentials", "AlbertSSOClient"] -__version__ = "1.12.0" +__version__ = "1.13.0" diff --git a/src/albert/collections/inventory.py b/src/albert/collections/inventory.py index b4a8bf1a..07c47324 100644 --- a/src/albert/collections/inventory.py +++ b/src/albert/collections/inventory.py @@ -841,7 +841,6 @@ def update(self, *, inventory_item: InventoryItem) -> InventoryItem: patch_payload = self._generate_inventory_patch_payload( existing=current_object, updated=inventory_item ) - # Complex patching does not work for some fields, so I'm going to do this in a loop :( # https://teams.microsoft.com/l/message/19:de4a48c366664ce1bafcdbea02298810@thread.tacv2/1724856117312?tenantId=98aab90e-764b-48f1-afaa-02e3c7300653&groupId=35a36a3d-fc25-4899-a1dd-ad9c7d77b5b3&parentMessageId=1724856117312&teamName=Product%20%2B%20Engineering&channelName=General%20-%20API&createdTime=1724856117312 url = f"{self.base_path}/{inventory_item.id}" diff --git a/src/albert/resources/inventory.py b/src/albert/resources/inventory.py index 33ea5003..5fa3290d 100644 --- a/src/albert/resources/inventory.py +++ b/src/albert/resources/inventory.py @@ -14,6 +14,7 @@ from albert.resources.acls import ACL from albert.resources.cas import Cas from albert.resources.companies import Company +from albert.resources.lists import ListItem from albert.resources.locations import Location from albert.resources.tagged_base import BaseTaggedResource from albert.resources.tags import Tag @@ -75,6 +76,8 @@ class CasAmount(BaseAlbertModel): The SMILES string of the CAS Number resource. Obtained from the Cas object when provided. number: str | None The CAS number. Obtained from the Cas object when provided. + inventory_function: list[ListItem | EntityLink | str] | None + Business-controlled functions associated with the CAS in this inventory context. !!! tip --- @@ -87,6 +90,9 @@ class CasAmount(BaseAlbertModel): target: float | None = Field(default=None, alias="inventoryValue") id: str | None = Field(default=None) cas_category: str | None = Field(default=None, alias="casCategory") + inventory_function: list[SerializeAsEntityLink[ListItem] | str] | None = Field( + default=None, alias="inventoryFunction" + ) type: str | None = Field(default=None) classification_type: str | None = Field(default=None, alias="classificationType") diff --git a/src/albert/resources/lists.py b/src/albert/resources/lists.py index d3ff6cf7..7d16dfd8 100644 --- a/src/albert/resources/lists.py +++ b/src/albert/resources/lists.py @@ -27,7 +27,8 @@ class ListItem(BaseResource): category : ListItemCategory | None The category of the list item. Allowed values are `businessDefined`, `userDefined`, `projects`, and `extensions`. list_type : str | None - The type of the list item. Allowed values are `projectState` for `projects` and `extensions` for `extensions`. + The type of the list item. Allowed values are `projectState` for `projects`, `extensions` for `extensions`, + and `casCategory` or `inventoryFunction` for `inventory`. """ name: str diff --git a/src/albert/utils/inventory.py b/src/albert/utils/inventory.py index 651c7d1e..af6e14ef 100644 --- a/src/albert/utils/inventory.py +++ b/src/albert/utils/inventory.py @@ -1,6 +1,7 @@ from collections.abc import Iterable from typing import Any +from albert.core.shared.models.base import BaseResource, EntityLink from albert.resources.inventory import CasAmount @@ -56,6 +57,61 @@ def _build_cas_delete_operation(identifier: str) -> dict[str, Any]: } +def _normalize_inventory_function_ids( + value: list[BaseResource | EntityLink | str] | None, +) -> list[str]: + if not value: + return [] + ids: list[str] = [] + for item in value: + if isinstance(item, str): + if item: + ids.append(item) + continue + if isinstance(item, BaseResource): + if item.id: + ids.append(item.id) + continue + if isinstance(item, EntityLink): + if item.id: + ids.append(item.id) + continue + return ids + + +def _build_inventory_function_operations( + *, + entity_id: str, + existing: list[BaseResource | EntityLink | str] | None, + updated: list[BaseResource | EntityLink | str] | None, +) -> list[dict[str, Any]]: + existing_ids = set(_normalize_inventory_function_ids(existing)) + updated_ids = set(_normalize_inventory_function_ids(updated)) + to_add = sorted(updated_ids - existing_ids) + to_delete = sorted(existing_ids - updated_ids) + + operations: list[dict[str, Any]] = [] + if to_add: + operations.append( + { + "attribute": "inventoryFunction", + "entityId": entity_id, + "operation": "add", + "newValue": to_add, + } + ) + if to_delete: + operations.append( + { + "attribute": "inventoryFunction", + "entityId": entity_id, + "operation": "delete", + "oldValue": to_delete, + } + ) + return operations + + def _build_cas_scalar_operation( *, attribute: str, @@ -117,6 +173,14 @@ def _build_cas_update_operations(existing: CasAmount, updated: CasAmount) -> lis if operation is not None: operations.append(operation) + operations.extend( + _build_inventory_function_operations( + entity_id=identifier, + existing=existing.inventory_function, + updated=updated.inventory_function, + ) + ) + return operations @@ -159,6 +223,13 @@ def _build_cas_patch_operations( ) if target_operation is not None: operations.append(target_operation) + operations.extend( + _build_inventory_function_operations( + entity_id=identifier, + existing=None, + updated=cas_amount.inventory_function, + ) + ) removals = [existing_lookup[key] for key in existing_lookup.keys() - updated_lookup.keys()] for cas_amount in removals: From 9a841191cd58023b8d4a4046a184e37b32a3ff47 Mon Sep 17 00:00:00 2001 From: Prasad Date: Tue, 13 Jan 2026 18:05:22 +0530 Subject: [PATCH 2/4] fix: tests --- src/albert/collections/inventory.py | 1 + tests/resources/test_inventory.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/albert/collections/inventory.py b/src/albert/collections/inventory.py index 07c47324..b4a8bf1a 100644 --- a/src/albert/collections/inventory.py +++ b/src/albert/collections/inventory.py @@ -841,6 +841,7 @@ def update(self, *, inventory_item: InventoryItem) -> InventoryItem: patch_payload = self._generate_inventory_patch_payload( existing=current_object, updated=inventory_item ) + # Complex patching does not work for some fields, so I'm going to do this in a loop :( # https://teams.microsoft.com/l/message/19:de4a48c366664ce1bafcdbea02298810@thread.tacv2/1724856117312?tenantId=98aab90e-764b-48f1-afaa-02e3c7300653&groupId=35a36a3d-fc25-4899-a1dd-ad9c7d77b5b3&parentMessageId=1724856117312&teamName=Product%20%2B%20Engineering&channelName=General%20-%20API&createdTime=1724856117312 url = f"{self.base_path}/{inventory_item.id}" diff --git a/tests/resources/test_inventory.py b/tests/resources/test_inventory.py index a43b237b..03a4be09 100644 --- a/tests/resources/test_inventory.py +++ b/tests/resources/test_inventory.py @@ -29,6 +29,7 @@ def test_cas_amount_attributes(): "target", "id", "cas_category", + "inventory_function", "created", "updated", "classification_type", From 5eb3a9c0448f1f8b11cf8c00689443d7bd970bcc Mon Sep 17 00:00:00 2001 From: Prasad Date: Mon, 19 Jan 2026 16:34:37 +0530 Subject: [PATCH 3/4] fix: validate list type for inventory --- src/albert/resources/lists.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/albert/resources/lists.py b/src/albert/resources/lists.py index 7d16dfd8..27c1054b 100644 --- a/src/albert/resources/lists.py +++ b/src/albert/resources/lists.py @@ -25,7 +25,8 @@ class ListItem(BaseResource): id : str | None The Albert ID of the list item. Set when the list item is retrieved from Albert. category : ListItemCategory | None - The category of the list item. Allowed values are `businessDefined`, `userDefined`, `projects`, and `extensions`. + The category of the list item. Allowed values are `businessDefined`, `userDefined`, `projects`, `extensions`, + and `inventory`. list_type : str | None The type of the list item. Allowed values are `projectState` for `projects`, `extensions` for `extensions`, and `casCategory` or `inventoryFunction` for `inventory`. @@ -38,14 +39,15 @@ class ListItem(BaseResource): @model_validator(mode="after") def validate_list_type(self) -> ListItem: + allowed_by_category = { + ListItemCategory.PROJECTS: {"projectState"}, + ListItemCategory.EXTENSIONS: {"extensions"}, + ListItemCategory.INVENTORY: {"casCategory", "inventoryFunction"}, + } if ( - self.category == ListItemCategory.PROJECTS - and self.list_type is not None - and self.list_type != "projectState" - ) or ( - self.category == ListItemCategory.EXTENSIONS - and self.list_type is not None - and self.list_type != "extensions" + self.list_type is not None + and self.category in allowed_by_category + and self.list_type not in allowed_by_category[self.category] ): raise ValueError( f"List type {self.list_type} is not allowed for category {self.category}" From 0211f59e31952bfbb241d90eab806cd25747505b Mon Sep 17 00:00:00 2001 From: Prasad Date: Tue, 20 Jan 2026 12:42:39 +0530 Subject: [PATCH 4/4] docs: add example for inventory function on cas --- docs/examples/inventory.md | 37 +++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 38 insertions(+) create mode 100644 docs/examples/inventory.md diff --git a/docs/examples/inventory.md b/docs/examples/inventory.md new file mode 100644 index 00000000..af17fffb --- /dev/null +++ b/docs/examples/inventory.md @@ -0,0 +1,37 @@ +# Inventory + +Albert Inventory serves as a digital manifestation of your physical inventory. It enables you to sort, filter, trace, and manage all types of inventory. + +## Inventory function on CAS + +Inventory function is a business-controlled, multi-select list on the Inventory ↔ CAS relationship. +Use it by updating an existing inventory item. + +!!! example "Add inventory function values to a CAS entry" + ```python + from albert import Albert + from albert.resources.lists import ListItem, ListItemCategory + + client = Albert.from_client_credentials() + + inventory_id = "INV123" + cas_id = "CAS123" + + # Optional: create a new inventoryFunction list item first. + list_item = ListItem( + name="Primary Function", + category=ListItemCategory.INVENTORY, + list_type="inventoryFunction", + ) + list_item = client.lists.create(list_item=list_item) + + inventory_item = client.inventory.get_by_id(id=inventory_id) + if inventory_item.cas: + for cas_amount in inventory_item.cas: + if cas_amount.id == cas_id: + cas_amount.inventory_function = [list_item] + break + + updated_item = client.inventory.update(inventory_item=inventory_item) + print(updated_item.id) + ``` diff --git a/mkdocs.yml b/mkdocs.yml index 78ce5e02..fab0923e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -221,6 +221,7 @@ nav: - Worksheets: resources/worksheets.md - Sheets: resources/sheets.md - Examples: + - Inventory: examples/inventory.md - Property Data: examples/property_data.md - Tasks: examples/tasks.md - Notebooks: examples/notebook.md