Skip to content

Projects

albert.collections.projects.ProjectCollection

ProjectCollection(*, session: AlbertSession)

Bases: BaseCollection

Manage Projects in the Albert platform.

A Project is the top-level container for a piece of R&D work. It groups the formulations designed for that work, the Project's Worksheet (1:1 with the project), the Tasks run against it, and the inventory it references. Projects are the entry point most workflows start from: you create a project, then build formulas and run tasks inside it.

Every project is identified by a Project ID (format PRO..., e.g. "PRO123"). A project always has a description (which doubles as its display name) and a ProjectClass controlling its access level (private, shared, or confidential).

This collection is accessed as client.projects.

Example

from albert import Albert
from albert.resources.projects import Project
client = Albert()
project = client.projects.create(
    project=Project(description="Weatherproof Coatings 2026")
)
print(project.id)
# 'PRO123'

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 project requests.

Methods:

Name Description
create

Create a new project.

get_by_id

Get a single project by its ID.

update

Update an existing project.

delete

Delete a project by its ID.

search

Fast, lightweight search returning partial projects (best for lookups).

get_all

Same filters as search, but returns fully populated projects (slower).

document_search

Search documents (attachments) linked to a project.

Parameters:

Name Type Description Default
session AlbertSession

The authenticated Albert session used for API calls.

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

    Parameters
    ----------
    session : AlbertSession
        The authenticated Albert session used for API calls.
    """
    super().__init__(session=session)
    self.base_path = f"/api/{ProjectCollection._api_version}/projects"

base_path

base_path = (
    f"/api/{ProjectCollection._api_version}/projects"
)

create

create(*, project: Project) -> Project

Create a new project.

Use this to register a new R&D container. Only description is required; it doubles as the project's display name. Optionally set locations, project_class (defaults to private), metadata, and other fields on the Project first.

Example

from albert.resources.projects import Project
project = client.projects.create(
    project=Project(description="Weatherproof Coatings 2026")
)
project.id
# 'PRO123'

Parameters:

Name Type Description Default
project Project

The project to create.

required

Returns:

Type Description
Project

The newly created project, populated with its assigned Project ID.

Source code in src/albert/collections/projects.py
def create(self, *, project: Project) -> Project:
    """Create a new project.

    Use this to register a new R&D container. Only ``description`` is
    required; it doubles as the project's display name. Optionally set
    ``locations``, ``project_class`` (defaults to private), ``metadata``, and
    other fields on the [`Project`][albert.resources.projects.Project] first.

    !!! example
        ```python
        from albert.resources.projects import Project
        project = client.projects.create(
            project=Project(description="Weatherproof Coatings 2026")
        )
        project.id
        # 'PRO123'
        ```

    Parameters
    ----------
    project : Project
        The project to create.

    Returns
    -------
    Project
        The newly created project, populated with its assigned Project ID.
    """
    response = self.session.post(
        self.base_path, json=project.model_dump(by_alias=True, exclude_unset=True, mode="json")
    )
    return Project(**response.json(), session=self.session)

get_by_id

get_by_id(*, id: ProjectId) -> Project

Get a single project by its ID.

To find projects without knowing their IDs, use search or get_all.

Example

project = client.projects.get_by_id(id="PRO123")
project.description
# 'Weatherproof Coatings 2026'

Parameters:

Name Type Description Default
id ProjectId

The Project ID (format PRO..., e.g. "PRO123").

required

Returns:

Type Description
Project

The fully populated project.

Source code in src/albert/collections/projects.py
@validate_call
def get_by_id(self, *, id: ProjectId) -> Project:
    """Get a single project by its ID.

    To find projects without knowing their IDs, use [`search`][albert.collections.projects.ProjectCollection.search] or
    [`get_all`][albert.collections.projects.ProjectCollection.get_all].

    !!! example
        ```python
        project = client.projects.get_by_id(id="PRO123")
        project.description
        # 'Weatherproof Coatings 2026'
        ```

    Parameters
    ----------
    id : ProjectId
        The Project ID (format ``PRO...``, e.g. ``"PRO123"``).

    Returns
    -------
    Project
        The fully populated project.
    """
    url = f"{self.base_path}/{id}"
    response = self.session.get(url)

    return Project(**response.json(), session=self.session)

update

update(*, project: Project) -> Project

Update an existing project.

Retrieve the project (e.g. with get_by_id), modify the updatable fields, then pass it here. Only the fields listed in Notes are applied.

Example

project = client.projects.get_by_id(id="PRO123")
project.description = "Weatherproof Coatings 2026 (rev B)"
updated = client.projects.update(project=project)

Parameters:

Name Type Description Default
project Project

The project carrying the desired changes. Its id identifies which project to update.

required

Returns:

Type Description
Project

The updated project.

Notes

The following fields can be updated: description, grid, metadata, state, acl.

Source code in src/albert/collections/projects.py
def update(self, *, project: Project) -> Project:
    """Update an existing project.

    Retrieve the project (e.g. with
    [`get_by_id`][albert.collections.projects.ProjectCollection.get_by_id]), modify the updatable fields, then pass it
    here. Only the fields listed in Notes are applied.

    !!! example
        ```python
        project = client.projects.get_by_id(id="PRO123")
        project.description = "Weatherproof Coatings 2026 (rev B)"
        updated = client.projects.update(project=project)
        ```

    Parameters
    ----------
    project : Project
        The project carrying the desired changes. Its ``id`` identifies which
        project to update.

    Returns
    -------
    Project
        The updated project.

    Notes
    -----
    The following fields can be updated: ``description``, ``grid``,
    ``metadata``, ``state``, ``acl``.
    """
    existing_project = self.get_by_id(id=project.id)
    patch_data = self._generate_patch_payload(existing=existing_project, updated=project)
    url = f"{self.base_path}/{project.id}"
    patch_payload = patch_data.model_dump(mode="json", by_alias=True)

    acl_operations: list[dict[str, Any]] = []
    if "acl" in project.model_fields_set:
        acl_operations = self._generate_acl_patch_operations(
            existing=existing_project.acl,
            updated=project.acl,
        )

    if patch_payload["data"]:
        self.session.patch(url, json=patch_payload)

    if acl_operations:
        self.session.patch(f"{url}/acl", json={"data": acl_operations})

    if not patch_payload["data"] and not acl_operations:
        return existing_project

    return self.get_by_id(id=project.id)

delete

delete(*, id: ProjectId) -> None

Delete a project by its ID.

Example

client.projects.delete(id="PRO123")

Parameters:

Name Type Description Default
id ProjectId

The Project ID (format PRO..., e.g. "PRO123").

required

Returns:

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

    !!! example
        ```python
        client.projects.delete(id="PRO123")
        ```

    Parameters
    ----------
    id : ProjectId
        The Project ID (format ``PRO...``, e.g. ``"PRO123"``).

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

search

search(
    *,
    text: str | None = None,
    status: list[str] | None = None,
    market_segment: list[str] | None = None,
    application: list[str] | None = None,
    technology: list[str] | None = None,
    created_by: list[str] | None = None,
    location: list[str] | None = None,
    program: list[str] | None = None,
    technical_lead: list[str] | None = None,
    from_created_at: str | None = None,
    to_created_at: str | None = None,
    updated_by: str | list[str] | None = None,
    from_updated_at: str | None = None,
    to_updated_at: str | None = None,
    facet_field: str | None = None,
    facet_text: str | None = None,
    contains_field: list[str] | None = None,
    contains_text: list[str] | None = None,
    linked_to: str | None = None,
    my_project: bool | None = None,
    my_role: list[str] | None = None,
    metadata_filters: dict[str, Any] | None = None,
    additional_field: list[str] | None = None,
    custom_fields: dict[str, Any] | None = None,
    formula_access: list[str] | None = None,
    linked_to_grid: str | None = None,
    source_field: list[str] | None = None,
    order_by: OrderBy = DESCENDING,
    sort_by: str | None = None,
    offset: int | None = None,
    max_items: int | None = None,
) -> Iterator[ProjectSearchItem]

Search for projects matching the given filters.

This is the fast way to find projects: it returns lightweight, partial (unhydrated) ProjectSearchItem results and is best for lookups, counts, and pulling IDs. To retrieve fully detailed Project entities, use get_all instead (slower, one full fetch per result).

All filters are optional; with no arguments this iterates over all projects you can access.

Example

# Keep the paginator reference — do not wrap in list() if you need
# completeness signals after iteration.
hits = client.projects.search(text="coatings", max_items=25)
for hit in hits:
    print(hit.id, hit.description)
if hits.has_more:
    print(f"Stopped early; ~{hits.total} total matches")

Parameters:

Name Type Description Default
text str

Full-text search query.

None
status list[str]

Filter by project statuses.

None
market_segment list[str]

Filter by market segment.

None
application list[str]

Filter by application.

None
technology list[str]

Filter by technology tags.

None
created_by list[str]

Filter by creator. Accepts user display name(s) or UserId(s) (e.g. "USR4227" or "Jane Doe").

None
location list[str]

Filter by location(s).

None
program list[str]

Filter by project program (custom field).

None
technical_lead list[str]

Filter by technical lead (custom field).

None
from_created_at str

Only include projects created on or after this date, formatted as YYYY-MM-DD.

None
to_created_at str

Only include projects created on or before this date, formatted as YYYY-MM-DD.

None
updated_by str or list[str]

Filter by user(s) who last updated the project. Accepts UserId(s) only (e.g. "USR4227"), not display names.

None
from_updated_at str

Only include projects updated on or after this date (ISO 8601).

None
to_updated_at str

Only include projects updated on or before this date (ISO 8601).

None
facet_field str

Facet field to filter on.

None
facet_text str

Facet text to search for.

None
contains_field list[str]

Fields to search inside.

None
contains_text list[str]

Values to search for within the contains_field.

None
linked_to str

Entity ID the project is linked to.

None
my_project bool

If True, return only projects owned by current user.

None
my_role list[str]

User roles to filter by.

None
metadata_filters dict[str, Any]

Filter by custom field (metadata) values.

Warning

Do not use this for application, technology, program, technical lead, or market segment. Use their corresponding query parameters instead.

None
additional_field list[str]

Request additional columns from the search index.

None
custom_fields dict[str, Any]

Filter by custom field values.

None
formula_access list[str]

Filter by formula access level.

None
linked_to_grid str

Text for linked-to dropdown search in grid/report flows.

None
source_field list[str]

Restrict which fields are returned in the response.

None
order_by OrderBy

Sort order. Default is DESCENDING.

DESCENDING
sort_by str

Field to sort by.

None
max_items int

Maximum number of items to return in total. If None, fetches all available items.

None

Returns:

Type Description
Iterator[ProjectSearchItem]

An iterator of matching partial (unhydrated) project results.

Source code in src/albert/collections/projects.py
@validate_call
def search(
    self,
    *,
    text: str | None = None,
    status: list[str] | None = None,
    market_segment: list[str] | None = None,
    application: list[str] | None = None,
    technology: list[str] | None = None,
    created_by: list[str] | None = None,
    location: list[str] | None = None,
    program: list[str] | None = None,
    technical_lead: list[str] | None = None,
    from_created_at: str | None = None,
    to_created_at: str | None = None,
    updated_by: str | list[str] | None = None,
    from_updated_at: str | None = None,
    to_updated_at: str | None = None,
    facet_field: str | None = None,
    facet_text: str | None = None,
    contains_field: list[str] | None = None,
    contains_text: list[str] | None = None,
    linked_to: str | None = None,
    my_project: bool | None = None,
    my_role: list[str] | None = None,
    metadata_filters: dict[str, Any] | None = None,
    additional_field: list[str] | None = None,
    custom_fields: dict[str, Any] | None = None,
    formula_access: list[str] | None = None,
    linked_to_grid: str | None = None,
    source_field: list[str] | None = None,
    order_by: OrderBy = OrderBy.DESCENDING,
    sort_by: str | None = None,
    offset: int | None = None,
    max_items: int | None = None,
) -> Iterator[ProjectSearchItem]:
    """Search for projects matching the given filters.

    This is the fast way to find projects: it returns lightweight, partial
    (unhydrated) [`ProjectSearchItem`][albert.resources.projects.ProjectSearchItem] results
    and is best for lookups, counts, and pulling IDs. To retrieve fully
    detailed [`Project`][albert.resources.projects.Project] entities, use
    [`get_all`][albert.collections.projects.ProjectCollection.get_all] instead (slower, one full fetch per result).

    All filters are optional; with no arguments this iterates over all
    projects you can access.

    !!! example
        ```python
        # Keep the paginator reference — do not wrap in list() if you need
        # completeness signals after iteration.
        hits = client.projects.search(text="coatings", max_items=25)
        for hit in hits:
            print(hit.id, hit.description)
        if hits.has_more:
            print(f"Stopped early; ~{hits.total} total matches")
        ```

    Parameters
    ----------
    text : str, optional
        Full-text search query.
    status : list[str], optional
        Filter by project statuses.
    market_segment : list[str], optional
        Filter by market segment.
    application : list[str], optional
        Filter by application.
    technology : list[str], optional
        Filter by technology tags.
    created_by : list[str], optional
        Filter by creator. Accepts user display name(s) or UserId(s) (e.g.
        ``"USR4227"`` or ``"Jane Doe"``).
    location : list[str], optional
        Filter by location(s).
    program : list[str], optional
        Filter by project program (custom field).
    technical_lead : list[str], optional
        Filter by technical lead (custom field).
    from_created_at : str, optional
        Only include projects created on or after this date, formatted as
        ``YYYY-MM-DD``.
    to_created_at : str, optional
        Only include projects created on or before this date, formatted as
        ``YYYY-MM-DD``.
    updated_by : str or list[str], optional
        Filter by user(s) who last updated the project. Accepts UserId(s)
        only (e.g. ``"USR4227"``), not display names.
    from_updated_at : str, optional
        Only include projects updated on or after this date (ISO 8601).
    to_updated_at : str, optional
        Only include projects updated on or before this date (ISO 8601).
    facet_field : str, optional
        Facet field to filter on.
    facet_text : str, optional
        Facet text to search for.
    contains_field : list[str], optional
        Fields to search inside.
    contains_text : list[str], optional
        Values to search for within the `contains_field`.
    linked_to : str, optional
        Entity ID the project is linked to.
    my_project : bool, optional
        If True, return only projects owned by current user.
    my_role : list[str], optional
        User roles to filter by.
    metadata_filters : dict[str, Any], optional
        Filter by custom field (metadata) values.
        !!! warning
            Do not use this for application, technology, program, technical lead, or
            market segment. Use their corresponding query parameters instead.
    additional_field : list[str], optional
        Request additional columns from the search index.
    custom_fields : dict[str, Any], optional
        Filter by custom field values.
    formula_access : list[str], optional
        Filter by formula access level.
    linked_to_grid : str, optional
        Text for linked-to dropdown search in grid/report flows.
    source_field : list[str], optional
        Restrict which fields are returned in the response.
    order_by : OrderBy, optional
        Sort order. Default is DESCENDING.
    sort_by : str, optional
        Field to sort by.
    max_items : int, optional
        Maximum number of items to return in total. If None, fetches all available items.

    Returns
    -------
    Iterator[ProjectSearchItem]
        An iterator of matching partial (unhydrated) project results.
    """
    # Always POST — same path as the Albert UI (searchProjectsWithPost). GET search
    # sometimes ignores ``limit`` (defaults to 25) and returns empty for offset>0.
    payload: dict[str, Any] = {
        "order": order_by,
        "offset": offset,
        "text": text,
        "sortBy": sort_by,
        "status": status,
        "marketSegment": market_segment,
        "application": application,
        "technology": technology,
        "createdBy": created_by,
        "location": location,
        "program": program,
        "technicalLead": technical_lead,
        "fromCreatedAt": from_created_at,
        "toCreatedAt": to_created_at,
        "updatedBy": ensure_list(updated_by),
        "fromUpdatedAt": from_updated_at,
        "toUpdatedAt": to_updated_at,
        "facetField": facet_field,
        "facetText": facet_text,
        "containsField": contains_field,
        "containsText": contains_text,
        "linkedTo": linked_to,
        "myProject": my_project,
        "myRole": my_role,
        "additionalField": additional_field,
        "formulaAccess": formula_access,
        "linkedToGrid": linked_to_grid,
        "sourceField": source_field,
    }
    if metadata_filters is not None:
        payload["metadataFilters"] = {"metadata": metadata_filters}
    if custom_fields is not None:
        payload["customFields"] = {"metadata": custom_fields}

    return AlbertPaginator(
        mode=PaginationMode.OFFSET,
        path=f"{self.base_path}/search",
        session=self.session,
        max_items=max_items,
        deserialize=lambda items: [
            ProjectSearchItem(**item)._bind_collection(self) for item in items
        ],
        method="POST",
        json=payload,
    )
document_search(
    *,
    linked_to: SearchProjectId,
    text: str | None = None,
    source_field: list[str] | None = None,
    additional_field: list[str] | None = None,
    search_field: list[str] | None = None,
    order_by: OrderBy = DESCENDING,
    sort_by: str | None = None,
    offset: int | None = None,
    max_items: int | None = None,
) -> Iterator[DocumentSearchItem]

Search for documents (attachments) linked to a project.

Each result is a lightweight DocumentSearchItem describing an attachment (name, MIME type, size, uploader) rather than the file itself.

Example

for doc in client.projects.document_search(linked_to="PRO123"):
    print(doc.name, doc.mime_type)

Parameters:

Name Type Description Default
linked_to SearchProjectId

The project to filter documents by (format PRO..., e.g. "PRO123").

required
text str

Full-text search query for document names.

None
source_field list[str]

Restrict which fields are returned in the response.

None
additional_field list[str]

Request additional columns from the search index.

None
search_field list[str]

Restrict which fields the text query searches.

None
order_by OrderBy

Sort order. Default is DESCENDING.

DESCENDING
sort_by str

Field to sort by (for example createdAt).

None
max_items int

Maximum number of items to return in total. If None, fetches all.

None

Returns:

Type Description
Iterator[DocumentSearchItem]

Matching document search results.

Source code in src/albert/collections/projects.py
@validate_call
def document_search(
    self,
    *,
    linked_to: SearchProjectId,
    text: str | None = None,
    source_field: list[str] | None = None,
    additional_field: list[str] | None = None,
    search_field: list[str] | None = None,
    order_by: OrderBy = OrderBy.DESCENDING,
    sort_by: str | None = None,
    offset: int | None = None,
    max_items: int | None = None,
) -> Iterator[DocumentSearchItem]:
    """Search for documents (attachments) linked to a project.

    Each result is a lightweight
    [`DocumentSearchItem`][albert.resources.projects.DocumentSearchItem] describing an
    attachment (name, MIME type, size, uploader) rather than the file itself.

    !!! example
        ```python
        for doc in client.projects.document_search(linked_to="PRO123"):
            print(doc.name, doc.mime_type)
        ```

    Parameters
    ----------
    linked_to : SearchProjectId
        The project to filter documents by (format ``PRO...``, e.g.
        ``"PRO123"``).
    text : str, optional
        Full-text search query for document names.
    source_field : list[str], optional
        Restrict which fields are returned in the response.
    additional_field : list[str], optional
        Request additional columns from the search index.
    search_field : list[str], optional
        Restrict which fields the ``text`` query searches.
    order_by : OrderBy, optional
        Sort order. Default is DESCENDING.
    sort_by : str, optional
        Field to sort by (for example ``createdAt``).
    max_items : int, optional
        Maximum number of items to return in total. If None, fetches all.

    Returns
    -------
    Iterator[DocumentSearchItem]
        Matching document search results.
    """
    query_params = {
        "linkedTo": linked_to,
        "text": text,
        "sourceField": source_field,
        "additionalField": additional_field,
        "searchField": search_field,
        "order": order_by,
        "sortBy": sort_by,
        "offset": offset,
    }

    return AlbertPaginator(
        mode=PaginationMode.OFFSET,
        path=f"{self.base_path}/documentsearch",
        session=self.session,
        params=query_params,
        max_items=max_items,
        deserialize=lambda items: [DocumentSearchItem(**item) for item in items],
    )

get_all

get_all(
    *,
    text: str | None = None,
    status: list[str] | None = None,
    market_segment: list[str] | None = None,
    application: list[str] | None = None,
    technology: list[str] | None = None,
    created_by: list[str] | None = None,
    location: list[str] | None = None,
    program: list[str] | None = None,
    technical_lead: list[str] | None = None,
    from_created_at: str | None = None,
    to_created_at: str | None = None,
    updated_by: str | list[str] | None = None,
    from_updated_at: str | None = None,
    to_updated_at: str | None = None,
    facet_field: str | None = None,
    facet_text: str | None = None,
    contains_field: list[str] | None = None,
    contains_text: list[str] | None = None,
    linked_to: str | None = None,
    my_project: bool | None = None,
    my_role: list[str] | None = None,
    metadata_filters: dict[str, Any] | None = None,
    additional_field: list[str] | None = None,
    custom_fields: dict[str, Any] | None = None,
    formula_access: list[str] | None = None,
    linked_to_grid: str | None = None,
    source_field: list[str] | None = None,
    order_by: OrderBy = DESCENDING,
    sort_by: str | None = None,
    offset: int | None = None,
    max_items: int | None = None,
) -> Iterator[Project]

Get fully populated projects matching optional filters.

Accepts the same filters as search, but yields complete Project entities by fetching each match individually via get_by_id. This is convenient but slower; prefer search when you only need IDs or a few summary fields.

Example

projects = client.projects.get_all(text="coatings", max_items=10)
for project in projects:
    print(project.id, project.description)
# has_more / total are preserved through hydration.
if projects.has_more:
    print(f"Sample only; ~{projects.total} total matches")

Parameters:

Name Type Description Default
text str

Full-text search query.

None
status list[str]

Filter by project statuses.

None
market_segment list[str]

Filter by market segment.

None
application list[str]

Filter by application.

None
technology list[str]

Filter by technology tags.

None
created_by list[str]

Filter by creator. Accepts user display name(s) or UserId(s) (e.g. "USR4227" or "Jane Doe").

None
location list[str]

Filter by location(s).

None
program list[str]

Filter by project program (custom field).

None
technical_lead list[str]

Filter by technical lead (custom field).

None
from_created_at str

Only include projects created on or after this date, formatted as YYYY-MM-DD.

None
to_created_at str

Only include projects created on or before this date, formatted as YYYY-MM-DD.

None
updated_by str or list[str]

Filter by user(s) who last updated the project. Accepts UserId(s) only (e.g. "USR4227"), not display names.

None
from_updated_at str

Only include projects updated on or after this date (ISO 8601).

None
to_updated_at str

Only include projects updated on or before this date (ISO 8601).

None
facet_field str

Facet field to filter on.

None
facet_text str

Facet text to search for.

None
contains_field list[str]

Fields to search inside.

None
contains_text list[str]

Values to search for within the contains_field.

None
linked_to str

Entity ID the project is linked to.

None
my_project bool

If True, return only projects owned by current user.

None
my_role list[str]

User roles to filter by.

None
metadata_filters dict[str, Any]

Filter by custom field (metadata) values.

None
additional_field list[str]

Request additional columns from the search index.

None
custom_fields dict[str, Any]

Filter by custom field values.

None
formula_access list[str]

Filter by formula access level.

None
linked_to_grid str

Text for linked-to dropdown search in grid/report flows.

None
source_field list[str]

Restrict which fields are returned in the response.

None
order_by OrderBy

Sort order. Default is DESCENDING.

DESCENDING
sort_by str

Field to sort by.

None
max_items int

Maximum number of items to return in total. If None, fetches all available items.

None

Returns:

Type Description
Iterator[Project]

An iterator of fully populated Project entities. Preserves has_more / total from the underlying search paginator.

Source code in src/albert/collections/projects.py
@validate_call
def get_all(
    self,
    *,
    text: str | None = None,
    status: list[str] | None = None,
    market_segment: list[str] | None = None,
    application: list[str] | None = None,
    technology: list[str] | None = None,
    created_by: list[str] | None = None,
    location: list[str] | None = None,
    program: list[str] | None = None,
    technical_lead: list[str] | None = None,
    from_created_at: str | None = None,
    to_created_at: str | None = None,
    updated_by: str | list[str] | None = None,
    from_updated_at: str | None = None,
    to_updated_at: str | None = None,
    facet_field: str | None = None,
    facet_text: str | None = None,
    contains_field: list[str] | None = None,
    contains_text: list[str] | None = None,
    linked_to: str | None = None,
    my_project: bool | None = None,
    my_role: list[str] | None = None,
    metadata_filters: dict[str, Any] | None = None,
    additional_field: list[str] | None = None,
    custom_fields: dict[str, Any] | None = None,
    formula_access: list[str] | None = None,
    linked_to_grid: str | None = None,
    source_field: list[str] | None = None,
    order_by: OrderBy = OrderBy.DESCENDING,
    sort_by: str | None = None,
    offset: int | None = None,
    max_items: int | None = None,
) -> Iterator[Project]:
    """Get fully populated projects matching optional filters.

    Accepts the same filters as [`search`][albert.collections.projects.ProjectCollection.search], but yields complete
    [`Project`][albert.resources.projects.Project] entities by fetching each
    match individually via [`get_by_id`][albert.collections.projects.ProjectCollection.get_by_id]. This is convenient but slower;
    prefer [`search`][albert.collections.projects.ProjectCollection.search] when you only need IDs or a few summary fields.

    !!! example
        ```python
        projects = client.projects.get_all(text="coatings", max_items=10)
        for project in projects:
            print(project.id, project.description)
        # has_more / total are preserved through hydration.
        if projects.has_more:
            print(f"Sample only; ~{projects.total} total matches")
        ```

    Parameters
    ----------
    text : str, optional
        Full-text search query.
    status : list[str], optional
        Filter by project statuses.
    market_segment : list[str], optional
        Filter by market segment.
    application : list[str], optional
        Filter by application.
    technology : list[str], optional
        Filter by technology tags.
    created_by : list[str], optional
        Filter by creator. Accepts user display name(s) or UserId(s) (e.g.
        ``"USR4227"`` or ``"Jane Doe"``).
    location : list[str], optional
        Filter by location(s).
    program : list[str], optional
        Filter by project program (custom field).
    technical_lead : list[str], optional
        Filter by technical lead (custom field).
    from_created_at : str, optional
        Only include projects created on or after this date, formatted as
        ``YYYY-MM-DD``.
    to_created_at : str, optional
        Only include projects created on or before this date, formatted as
        ``YYYY-MM-DD``.
    updated_by : str or list[str], optional
        Filter by user(s) who last updated the project. Accepts UserId(s)
        only (e.g. ``"USR4227"``), not display names.
    from_updated_at : str, optional
        Only include projects updated on or after this date (ISO 8601).
    to_updated_at : str, optional
        Only include projects updated on or before this date (ISO 8601).
    facet_field : str, optional
        Facet field to filter on.
    facet_text : str, optional
        Facet text to search for.
    contains_field : list[str], optional
        Fields to search inside.
    contains_text : list[str], optional
        Values to search for within the `contains_field`.
    linked_to : str, optional
        Entity ID the project is linked to.
    my_project : bool, optional
        If True, return only projects owned by current user.
    my_role : list[str], optional
        User roles to filter by.
    metadata_filters : dict[str, Any], optional
        Filter by custom field (metadata) values.
    additional_field : list[str], optional
        Request additional columns from the search index.
    custom_fields : dict[str, Any], optional
        Filter by custom field values.
    formula_access : list[str], optional
        Filter by formula access level.
    linked_to_grid : str, optional
        Text for linked-to dropdown search in grid/report flows.
    source_field : list[str], optional
        Restrict which fields are returned in the response.
    order_by : OrderBy, optional
        Sort order. Default is DESCENDING.
    sort_by : str, optional
        Field to sort by.
    max_items : int, optional
        Maximum number of items to return in total. If None, fetches all available items.

    Returns
    -------
    Iterator[Project]
        An iterator of fully populated Project entities. Preserves ``has_more`` /
        ``total`` from the underlying search paginator.
    """

    def _hydrate(project: ProjectSearchItem) -> Project | None:
        project_id = getattr(project, "albertId", None) or getattr(project, "id", None)
        if not project_id:
            return None
        id = project_id if str(project_id).startswith("PRO") else f"PRO{project_id}"
        try:
            return self.get_by_id(id=id)
        except AlbertHTTPError as e:
            logger.warning(f"Error fetching project details {id}: {e}")
            return None

    return MappedPaginator(
        self.search(
            text=text,
            status=status,
            market_segment=market_segment,
            application=application,
            technology=technology,
            created_by=created_by,
            location=location,
            program=program,
            technical_lead=technical_lead,
            from_created_at=from_created_at,
            to_created_at=to_created_at,
            updated_by=updated_by,
            from_updated_at=from_updated_at,
            to_updated_at=to_updated_at,
            facet_field=facet_field,
            facet_text=facet_text,
            contains_field=contains_field,
            contains_text=contains_text,
            linked_to=linked_to,
            my_project=my_project,
            my_role=my_role,
            metadata_filters=metadata_filters,
            additional_field=additional_field,
            custom_fields=custom_fields,
            formula_access=formula_access,
            linked_to_grid=linked_to_grid,
            source_field=source_field,
            order_by=order_by,
            sort_by=sort_by,
            offset=offset,
            max_items=max_items,
        ),
        _hydrate,
    )