Skip to content

Cas

albert.collections.cas.CasCollection

CasCollection(*, session: AlbertSession)

Bases: BaseCollection

Manage CAS entries in the Albert platform.

A CAS entry (Cas) records a chemical substance identified by its CAS Registry Number (e.g. "7727-37-9" for nitrogen). CAS entries are the shared chemical dictionary that raw-material Inventory Items point to: a raw material lists the CAS numbers of its constituents, each paired with an amount (see CasAmount).

CAS entries are referenced by their CAS ID (format CAS..., e.g. "CAS1"). Most workflows either look a substance up by its registry number (get_by_number) or ensure one exists before linking it (get_or_create).

This collection is accessed as client.cas.

Example

from albert import Albert
client = Albert()
cas = client.cas.get_or_create(cas="7727-37-9")
print(cas.id, cas.number)

Parameters:

Name Type Description Default
session AlbertSession

The authenticated Albert session used for API calls.

required

Attributes:

Name Type Description
base_path str

The base API route for CAS requests.

Methods:

Name Description
create

Create a new CAS entry from a registry number or Cas object.

get_or_create

Return the existing entry for a registry number, or create it.

get_by_id

Get a single CAS entry by its ID.

get_by_number

Get a CAS entry by its registry number.

get_all

Iterate over CAS entries, optionally filtered by number(s) or ID.

exists

Check whether a CAS entry with the given number exists.

update

Update an existing CAS entry.

delete

Delete a CAS entry by its ID.

Parameters:

Name Type Description Default
session AlbertSession

The authenticated Albert session used for API calls.

required
Source code in src/albert/collections/cas.py
def __init__(self, *, session: AlbertSession):
    """Initialize a CasCollection.

    Parameters
    ----------
    session : AlbertSession
        The authenticated Albert session used for API calls.
    """
    warnings.warn(
        "CasCollection is deprecated and will be removed in SDK 2.0. "
        "Use client.substances_v4 instead. "
        "In SDK 2.0, all write operations target the Substance entity exclusively: "
        "CAS entities, reports, and SQL DWH tables backed by CAS will no longer be updated.",
        DeprecationWarning,
        stacklevel=2,
    )
    super().__init__(session=session)
    self.base_path = f"/api/{CasCollection._api_version}/cas"

base_path

base_path = f'/api/{CasCollection._api_version}/cas'

get_all

get_all(
    *,
    number: str | None = None,
    cas: list[str] | None = None,
    id: CasId | None = None,
    order_by: OrderBy = DESCENDING,
    start_key: str | int | None = None,
    max_items: int | None = None,
) -> Iterator[Cas]

Iterate over CAS entries, optionally filtered.

Use this to list CAS entries or to search by one or more registry numbers. Results are streamed page by page, so you can iterate large result sets without loading everything at once. To fetch a single entry when you already know its registry number or CAS ID, prefer get_by_number or get_by_id.

Example

# List the most recent CAS entries
for cas in client.cas.get_all(max_items=10):
    print(cas.id, cas.number)

# Look up specific registry numbers
matches = list(client.cas.get_all(cas=["7727-37-9", "64-17-5"]))

Parameters:

Name Type Description Default
number str

Filter by a single CAS registry number (substring/partial match).

None
cas list[str]

Filter by an exact list of CAS registry numbers.

None
id CasId

Return only the entry with this CAS ID (format CAS...). When set, other filters are ignored and at most one entry is yielded.

None
order_by OrderBy

Sort direction. Defaults to OrderBy.DESCENDING.

DESCENDING
start_key str or int

Pagination resume key. For unfiltered listing, pass the string key returned by a previous page. For filtered search (number or cas), pass an integer offset.

None
max_items int

Maximum number of entries to yield in total. If None, iterates over all matching entries.

None

Returns:

Type Description
Iterator[Cas]

Matching CAS entries. On the filtered and unfiltered paths, has_more / total remain available on the returned iterator.

Source code in src/albert/collections/cas.py
@validate_call
def get_all(
    self,
    *,
    number: str | None = None,
    cas: list[str] | None = None,
    id: CasId | None = None,
    order_by: OrderBy = OrderBy.DESCENDING,
    start_key: str | int | None = None,
    max_items: int | None = None,
) -> Iterator[Cas]:
    """Iterate over CAS entries, optionally filtered.

    Use this to list CAS entries or to search by one or more registry numbers.
    Results are streamed page by page, so you can iterate large result sets
    without loading everything at once. To fetch a single entry when you
    already know its registry number or CAS ID, prefer [`get_by_number`][albert.collections.cas.CasCollection.get_by_number]
    or [`get_by_id`][albert.collections.cas.CasCollection.get_by_id].

    !!! example
        ```python
        # List the most recent CAS entries
        for cas in client.cas.get_all(max_items=10):
            print(cas.id, cas.number)

        # Look up specific registry numbers
        matches = list(client.cas.get_all(cas=["7727-37-9", "64-17-5"]))
        ```

    Parameters
    ----------
    number : str, optional
        Filter by a single CAS registry number (substring/partial match).
    cas : list[str], optional
        Filter by an exact list of CAS registry numbers.
    id : CasId, optional
        Return only the entry with this CAS ID (format ``CAS...``). When set,
        other filters are ignored and at most one entry is yielded.
    order_by : OrderBy, optional
        Sort direction. Defaults to ``OrderBy.DESCENDING``.
    start_key : str or int, optional
        Pagination resume key. For unfiltered listing, pass the string key
        returned by a previous page. For filtered search (``number`` or
        ``cas``), pass an integer offset.
    max_items : int, optional
        Maximum number of entries to yield in total. If None, iterates over
        all matching entries.

    Returns
    -------
    Iterator[Cas]
        Matching CAS entries. On the filtered and unfiltered paths, ``has_more`` /
        ``total`` remain available on the returned iterator.
    """
    params: dict[str, Any] = {"orderBy": order_by}
    if id is not None:
        # Lazy single-item iterator: defer the lookup (and any NotFound) to first
        # iteration, matching the previous generator behaviour.
        def _single() -> Iterator[Cas]:
            yield self.get_by_id(id=id)

        return _single()

    if number is not None or cas:
        # Filtered search path: self-managed integer offset pagination.
        # The backend has a bug where it overwrites the numeric lastKey with a
        # string key, we must ignore lastKey and use integer offsets. (TAS-564)
        start_offset = 0
        if start_key is not None:
            try:
                start_offset = int(start_key)
            except (TypeError, ValueError) as exc:
                raise ValueError(
                    "start_key must be an integer for filtered CAS search."
                ) from exc
        params["startKey"] = start_offset
        if number is not None:
            params["number"] = number
        if cas:
            params["cas"] = cas
        return CasPaginator(
            path=self.base_path,
            session=self.session,
            params=params,
            max_items=max_items,
        )

    # Unfiltered listing path: string key pagination via lastKey in response.
    if start_key is not None:
        params["startKey"] = start_key
    return AlbertPaginator(
        mode=PaginationMode.KEY,
        path=self.base_path,
        session=self.session,
        params=params,
        max_items=max_items,
        deserialize=lambda items: [Cas(**item) for item in items],
    )

exists

exists(
    *,
    number: str,
    exact_match: bool = True,
    max_items: int | None = 50,
) -> bool

Check whether a CAS entry exists for the given registry number.

Useful before creating an entry to avoid duplicates. To retrieve the matching entry itself (rather than a boolean), use get_by_number; to fetch-or-create in one step, use get_or_create.

Example

client.cas.exists(number="7727-37-9")
# True

Parameters:

Name Type Description Default
number str

The CAS registry number to check.

required
exact_match bool

When True (default), require an exact registry-number match. When False, treat number as a partial match.

True
max_items int

Maximum number of results to search through when exact_match is False. Defaults to 50 (one page). Pass None for unbounded search.

50

Returns:

Type Description
bool

True if a matching CAS entry exists, False otherwise.

Source code in src/albert/collections/cas.py
@validate_call
def exists(self, *, number: str, exact_match: bool = True, max_items: int | None = 50) -> bool:
    """Check whether a CAS entry exists for the given registry number.

    Useful before creating an entry to avoid duplicates. To retrieve the
    matching entry itself (rather than a boolean), use [`get_by_number`][albert.collections.cas.CasCollection.get_by_number];
    to fetch-or-create in one step, use [`get_or_create`][albert.collections.cas.CasCollection.get_or_create].

    !!! example
        ```python
        client.cas.exists(number="7727-37-9")
        # True
        ```

    Parameters
    ----------
    number : str
        The CAS registry number to check.
    exact_match : bool, optional
        When True (default), require an exact registry-number match. When
        False, treat ``number`` as a partial match.
    max_items : int, optional
        Maximum number of results to search through when ``exact_match`` is
        False. Defaults to 50 (one page). Pass ``None`` for unbounded search.

    Returns
    -------
    bool
        True if a matching CAS entry exists, False otherwise.
    """
    return (
        self.get_by_number(number=number, exact_match=exact_match, max_items=max_items)
        is not None
    )

create

create(*, cas: str | Cas) -> Cas

Create a new CAS entry.

Use this to add a substance to Albert's CAS dictionary. If you are not sure whether the substance already exists, prefer get_or_create, which avoids creating a duplicate.

Example

cas = client.cas.create(cas="7727-37-9")
cas.id
# 'CAS1'

Parameters:

Name Type Description Default
cas str or Cas

The CAS registry number, or a fully built Cas object. A bare string is treated as the registry number.

required

Returns:

Type Description
Cas

The newly created entry, populated with its assigned CAS ID.

Source code in src/albert/collections/cas.py
def create(self, *, cas: str | Cas) -> Cas:
    """Create a new CAS entry.

    Use this to add a substance to Albert's CAS dictionary. If you are not
    sure whether the substance already exists, prefer [`get_or_create`][albert.collections.cas.CasCollection.get_or_create],
    which avoids creating a duplicate.

    !!! example
        ```python
        cas = client.cas.create(cas="7727-37-9")
        cas.id
        # 'CAS1'
        ```

    Parameters
    ----------
    cas : str or Cas
        The CAS registry number, or a fully built
        [`Cas`][albert.resources.cas.Cas] object. A bare string is treated as
        the registry number.

    Returns
    -------
    Cas
        The newly created entry, populated with its assigned CAS ID.
    """
    if isinstance(cas, str):
        cas = Cas(number=cas)

    payload = cas.model_dump(by_alias=True, exclude_unset=True, mode="json")
    # See class comment: CAS list metadata requires hydrated entity-link objects.
    if "metadata" in cas.model_fields_set and cas.metadata:
        payload["Metadata"] = {
            key: (
                [self._list_metadata_link_payload(link) for link in value]
                if isinstance(value, list)
                else self._list_metadata_link_payload(value)
                if isinstance(value, EntityLink)
                else value
            )
            for key, value in cas.metadata.items()
        }
    response = self.session.post(self.base_path, json=payload)
    cas = Cas(**response.json())
    return cas

get_or_create

get_or_create(*, cas: str | Cas) -> Cas

Return the CAS entry for a registry number, creating it if needed.

This is the safest way to obtain a CAS entry to link to a raw material: it looks up the registry number with an exact match and returns the existing entry if found, otherwise creates a new one via create.

Example

cas = client.cas.get_or_create(cas="7727-37-9")
cas.id
# 'CAS1'

Parameters:

Name Type Description Default
cas str or Cas

The CAS registry number, or a fully built Cas object. A bare string is treated as the registry number.

required

Returns:

Type Description
Cas

The existing or newly created entry.

Source code in src/albert/collections/cas.py
def get_or_create(self, *, cas: str | Cas) -> Cas:
    """Return the CAS entry for a registry number, creating it if needed.

    This is the safest way to obtain a CAS entry to link to a raw material:
    it looks up the registry number with an exact match and returns the
    existing entry if found, otherwise creates a new one via [`create`][albert.collections.cas.CasCollection.create].

    !!! example
        ```python
        cas = client.cas.get_or_create(cas="7727-37-9")
        cas.id
        # 'CAS1'
        ```

    Parameters
    ----------
    cas : str or Cas
        The CAS registry number, or a fully built
        [`Cas`][albert.resources.cas.Cas] object. A bare string is treated as
        the registry number.

    Returns
    -------
    Cas
        The existing or newly created entry.
    """
    if isinstance(cas, str):
        cas = Cas(number=cas)
    found = self.get_by_number(number=cas.number, exact_match=True)
    if found:
        return found
    else:
        return self.create(cas=cas)

get_by_id

get_by_id(*, id: CasId) -> Cas

Get a single CAS entry by its ID.

To look a substance up by its registry number instead, use get_by_number.

Example

cas = client.cas.get_by_id(id="CAS1")
cas.number
# '7727-37-9'

Parameters:

Name Type Description Default
id CasId

The CAS ID to retrieve (format CAS..., e.g. "CAS1").

required

Returns:

Type Description
Cas

The fully populated CAS entry.

Source code in src/albert/collections/cas.py
@validate_call
def get_by_id(self, *, id: CasId) -> Cas:
    """Get a single CAS entry by its ID.

    To look a substance up by its registry number instead, use
    [`get_by_number`][albert.collections.cas.CasCollection.get_by_number].

    !!! example
        ```python
        cas = client.cas.get_by_id(id="CAS1")
        cas.number
        # '7727-37-9'
        ```

    Parameters
    ----------
    id : CasId
        The CAS ID to retrieve (format ``CAS...``, e.g. ``"CAS1"``).

    Returns
    -------
    Cas
        The fully populated CAS entry.
    """
    url = f"{self.base_path}/{id}"
    response = self.session.get(url)
    cas = Cas(**response.json())
    return cas

get_by_number

get_by_number(
    *,
    number: str,
    exact_match: bool = True,
    max_items: int | None = 50,
) -> Cas | None

Get a CAS entry by its registry number.

The number is normalized before matching (extra spaces around the dashes are removed), mirroring how the Albert backend compares CAS numbers. To fetch-or-create in one step, use get_or_create.

Example

cas = client.cas.get_by_number(number="7727-37-9")
cas.id if cas else "not found"
# 'CAS1'

Parameters:

Name Type Description Default
number str

The CAS registry number to retrieve.

required
exact_match bool

When True (default), return the entry whose registry number matches exactly. When False, return the first entry whose number contains number as a substring.

True
max_items int

Maximum number of results to search through when exact_match is False. Defaults to 50 (one page). Pass None for unbounded search.

50

Returns:

Type Description
Cas or None

The matching CAS entry, or None if no match is found.

Source code in src/albert/collections/cas.py
@validate_call
def get_by_number(
    self, *, number: str, exact_match: bool = True, max_items: int | None = 50
) -> Cas | None:
    """Get a CAS entry by its registry number.

    The number is normalized before matching (extra spaces around the dashes
    are removed), mirroring how the Albert backend compares CAS numbers. To
    fetch-or-create in one step, use [`get_or_create`][albert.collections.cas.CasCollection.get_or_create].

    !!! example
        ```python
        cas = client.cas.get_by_number(number="7727-37-9")
        cas.id if cas else "not found"
        # 'CAS1'
        ```

    Parameters
    ----------
    number : str
        The CAS registry number to retrieve.
    exact_match : bool, optional
        When True (default), return the entry whose registry number matches
        exactly. When False, return the first entry whose number contains
        ``number`` as a substring.
    max_items : int, optional
        Maximum number of results to search through when ``exact_match`` is
        False. Defaults to 50 (one page). Pass ``None`` for unbounded search.

    Returns
    -------
    Cas or None
        The matching CAS entry, or None if no match is found.
    """
    cleaned_number = self._clean_cas_number(number)

    if exact_match:
        for candidate in self.get_all(cas=[cleaned_number], max_items=1):
            if self._clean_cas_number(candidate.number) == cleaned_number:
                return candidate
        return None

    for candidate in self.get_all(number=cleaned_number, max_items=max_items):
        if cleaned_number in self._clean_cas_number(candidate.number):
            return candidate
    return None

delete

delete(*, id: CasId) -> None

Delete a CAS entry by its CAS ID.

Example

client.cas.delete(id="CAS1")

Parameters:

Name Type Description Default
id CasId

The CAS ID to delete (format CAS...).

required

Returns:

Type Description
None
Source code in src/albert/collections/cas.py
@validate_call
def delete(self, *, id: CasId) -> None:
    """Delete a CAS entry by its CAS ID.

    !!! example
        ```python
        client.cas.delete(id="CAS1")
        ```

    Parameters
    ----------
    id : CasId
        The CAS ID to delete (format ``CAS...``).

    Returns
    -------
    None
    """
    url = f"{self.base_path}/{id}"
    self.session.delete(url)

update

update(*, updated_object: Cas) -> Cas

Update an existing CAS entry.

Fetch the entry (e.g. with get_by_id or get_by_number), modify the updatable fields on the returned object, then pass it here. The entry is matched by its id, so that field must be set.

Example

cas = client.cas.get_by_id(id="CAS1")
cas.notes = "Confirmed against supplier COA."
updated = client.cas.update(updated_object=cas)

Parameters:

Name Type Description Default
updated_object Cas

The modified CAS entry. Must carry the id of the entry to update.

required

Returns:

Type Description
Cas

The updated entry as it appears in Albert after the change.

Notes

Only the following fields are updatable: description, metadata, notes, smiles. Changes to other fields are ignored.

Source code in src/albert/collections/cas.py
def update(self, *, updated_object: Cas) -> Cas:
    """Update an existing CAS entry.

    Fetch the entry (e.g. with [`get_by_id`][albert.collections.cas.CasCollection.get_by_id] or [`get_by_number`][albert.collections.cas.CasCollection.get_by_number]),
    modify the updatable fields on the returned object, then pass it here. The
    entry is matched by its ``id``, so that field must be set.

    !!! example
        ```python
        cas = client.cas.get_by_id(id="CAS1")
        cas.notes = "Confirmed against supplier COA."
        updated = client.cas.update(updated_object=cas)
        ```

    Parameters
    ----------
    updated_object : Cas
        The modified CAS entry. Must carry the ``id`` of the entry to update.

    Returns
    -------
    Cas
        The updated entry as it appears in Albert after the change.

    Notes
    -----
    Only the following fields are updatable: ``description``, ``metadata``,
    ``notes``, ``smiles``. Changes to other fields are ignored.
    """
    # Fetch the current object state from the server or database
    existing_cas = self.get_by_id(id=updated_object.id)

    # Generate the PATCH payload
    patch_payload = self._generate_patch_payload(existing=existing_cas, updated=updated_object)
    if not patch_payload.data:
        return existing_cas
    self._batch_patch(url=f"{self.base_path}/{updated_object.id}", data=patch_payload.data)
    return self.get_by_id(id=updated_object.id)