Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/albert/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

__all__ = ["Albert", "AlbertClientCredentials", "AlbertSSOClient"]

__version__ = "1.11.1"
__version__ = "1.11.2"
17 changes: 9 additions & 8 deletions src/albert/collections/btinsight.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from albert.core.session import AlbertSession
from albert.core.shared.enums import OrderBy, PaginationMode
from albert.core.shared.identifiers import BTInsightId
from albert.core.utils import ensure_list
from albert.resources.btinsight import BTInsight, BTInsightCategory, BTInsightState


Expand Down Expand Up @@ -132,17 +133,17 @@ def search(
"""
params = {
"offset": offset,
"order": OrderBy(order_by).value if order_by else None,
"order": order_by,
"sortBy": sort_by,
"text": text,
"name": name,
"name": ensure_list(name),
}
if state:
state = state if isinstance(state, list) else [state]
params["state"] = [BTInsightState(x).value for x in state]
if category:
category = category if isinstance(category, list) else [category]
params["category"] = [BTInsightCategory(x).value for x in category]

state_values = ensure_list(state)
params["state"] = state_values if state_values else None

category_values = ensure_list(category)
params["category"] = category_values if category_values else None

return AlbertPaginator(
mode=PaginationMode.OFFSET,
Expand Down
2 changes: 1 addition & 1 deletion src/albert/collections/cas.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def get_all(
An iterator over Cas entities.
"""

params: dict[str, Any] = {"orderBy": order_by.value}
params: dict[str, Any] = {"orderBy": order_by}
if id is not None:
yield self.get_by_id(id=id)
return
Expand Down
6 changes: 3 additions & 3 deletions src/albert/collections/companies.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from albert.core.pagination import AlbertPaginator, PaginationMode
from albert.core.session import AlbertSession
from albert.core.shared.identifiers import CompanyId
from albert.core.utils import ensure_list
from albert.exceptions import AlbertException
from albert.resources.companies import Company

Expand Down Expand Up @@ -62,9 +63,8 @@ def get_all(
"dupDetection": "false",
"startKey": start_key,
}
if name:
params["name"] = name if isinstance(name, list) else [name]
params["exactMatch"] = str(exact_match).lower()
params["name"] = ensure_list(name)
params["exactMatch"] = str(exact_match).lower()

return AlbertPaginator(
mode=PaginationMode.KEY,
Expand Down
259 changes: 233 additions & 26 deletions src/albert/collections/custom_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@
from pydantic import validate_call

from albert.collections.base import BaseCollection
from albert.collections.tags import TagCollection
from albert.core.logging import logger
from albert.core.pagination import AlbertPaginator
from albert.core.session import AlbertSession
from albert.core.shared.enums import PaginationMode
from albert.core.shared.enums import OrderBy, PaginationMode, Status
from albert.core.shared.identifiers import CustomTemplateId
from albert.exceptions import AlbertHTTPError
from albert.resources.custom_templates import CustomTemplate, CustomTemplateSearchItem
from albert.core.shared.models.patch import PatchOperation
from albert.core.utils import ensure_list
from albert.resources.acls import ACL
from albert.resources.custom_templates import (
CustomTemplate,
CustomTemplateSearchItem,
TemplateCategory,
)


class CustomTemplatesCollection(BaseCollection):
Expand All @@ -31,6 +38,94 @@ def __init__(self, *, session: AlbertSession):
self.base_path = f"/api/{CustomTemplatesCollection._api_version}/customtemplates"

@validate_call
def create(
self,
*,
custom_templates: CustomTemplate | list[CustomTemplate],
) -> list[CustomTemplate]:
"""
Creates one or more custom templates.

Parameters
----------
custom_templates : CustomTemplate | list[CustomTemplate]
The template entities to create.

Returns
-------
list[CustomTemplate]
The created CustomTemplate entities.
"""
templates = ensure_list(custom_templates) or []
if len(templates) == 0:
raise ValueError("At least one CustomTemplate must be provided.")

payload = [
template.model_dump(
mode="json",
by_alias=True,
exclude_none=True,
exclude_unset=True,
)
for template in templates
]
response = self.session.post(url=self.base_path, json=payload)
response_data = response.json()
created_payloads = (
(response_data or {}).get("CreatedItems")
if response.status_code == 206
else response_data
) or []

tag_collection = TagCollection(session=self.session)

def resolve_tag(tag_id: str | None) -> dict[str, str] | None:
if not tag_id:
return None
tag = tag_collection.get_by_id(id=tag_id)
return {"albertId": tag.id or tag_id, "name": tag.tag}

def populate_tag_names(section: dict | None) -> None:
if not isinstance(section, dict):
return
tags = section.get("Tags")
if not tags:
return
resolved_tags = []
for tag in tags:
if isinstance(tag, dict):
tag_id = tag.get("id") or tag.get("albertId")
elif isinstance(tag, str):
tag_id = tag
else:
tag_id = None

resolved_tag = resolve_tag(tag_id)
if resolved_tag:
resolved_tags.append(resolved_tag)
section["Tags"] = resolved_tags

for payload in created_payloads:
if not isinstance(payload, dict):
continue
populate_tag_names(payload.get("Data"))

if response.status_code == 206:
failed_items = response_data.get("FailedItems") or []
if failed_items:
error_messages = []
for failed in failed_items:
errors = failed.get("errors") or []
if errors:
error_messages.extend(err.get("msg", "Unknown error") for err in errors)
joined = " | ".join(error_messages) if error_messages else "Unknown error"
logger.warning(
"Custom template creation partially succeeded. Errors: %s",
joined,
)

return [CustomTemplate(**item) for item in created_payloads]

def get_by_id(self, *, id: CustomTemplateId) -> CustomTemplate:
"""Get a Custom Template by ID

Expand All @@ -52,8 +147,20 @@ def search(
self,
*,
text: str | None = None,
max_items: int | None = None,
offset: int | None = 0,
sort_by: str | None = None,
order_by: OrderBy | None = None,
status: Status | None = None,
created_by: str | None = None,
category: TemplateCategory | list[TemplateCategory] | None = None,
created_by_name: str | list[str] | None = None,
collaborator: str | list[str] | None = None,
facet_text: str | None = None,
facet_field: str | None = None,
contains_field: str | list[str] | None = None,
contains_text: str | list[str] | None = None,
my_role: str | list[str] | None = None,
max_items: int | None = None,
) -> Iterator[CustomTemplateSearchItem]:
"""
Search for CustomTemplate matching the provided criteria.
Expand All @@ -64,20 +171,57 @@ def search(
Parameters
----------
text : str, optional
Text to filter search results by.
max_items : int, optional
Maximum number of items to return in total. If None, fetches all available items.
Free text search term.
offset : int, optional
Offset to begin pagination at. Default is 0.
Starting offset for pagination.
sort_by : str, optional
Field to sort on.
order_by : OrderBy, optional
Sort direction for `sort_by`.
status : Status | str, optional
Filter results by template status.
created_by : str, optional
Filter by creator id.
category : TemplateCategory | list[TemplateCategory], optional
Filter by template categories.
created_by_name : str | list[str], optional
Filter by creator display name(s).
collaborator : str | list[str], optional
Filter by collaborator ids.
facet_text : str, optional
Filter text within a facet.
facet_field : str, optional
Facet field to search inside.
contains_field : str | list[str], optional
Fields to apply contains search to.
contains_text : str | list[str], optional
Text values for contains search.
my_role : str | list[str], optional
Restrict templates to roles held by the calling user.
max_items : int, optional
Maximum number of items to yield client-side.

Returns
-------
Iterator[CustomTemplateSearchItem]
An iterator of CustomTemplateSearchItem items.
"""

params = {
"text": text,
"offset": offset,
"sortBy": sort_by,
"order": order_by,
"status": status,
"createdBy": created_by,
"category": ensure_list(category),
"createdByName": ensure_list(created_by_name),
"collaborator": ensure_list(collaborator),
"facetText": facet_text,
"facetField": facet_field,
"containsField": ensure_list(contains_field),
"containsText": ensure_list(contains_text),
"myRole": ensure_list(my_role),
}

return AlbertPaginator(
Expand All @@ -94,32 +238,95 @@ def search(
def get_all(
self,
*,
text: str | None = None,
name: str | list[str] | None = None,
created_by: str | None = None,
category: TemplateCategory | None = None,
start_key: str | None = None,
max_items: int | None = None,
offset: int | None = 0,
) -> Iterator[CustomTemplate]:
"""
Retrieve fully hydrated CustomTemplate entities with optional filters.

This method returns complete entity data using `get_by_id`.
Use :meth:`search` for faster retrieval when you only need lightweight, partial (unhydrated) entities.
"""Iterate over CustomTemplate entities with optional filters.

Parameters
----------
text : str, optional
Text filter for template name or content.
name : str | list[str], optional
Filter by template name(s).
created_by : str, optional
Filter by creator id.
category : TemplateCategory, optional
Filter by category.
start_key : str, optional
Provide the `lastKey` from a previous request to resume pagination.
max_items : int, optional
Maximum number of items to return in total. If None, fetches all available items.
offset : int, optional
Offset for search pagination.
Maximum number of items to return.

Returns
-------
Iterator[CustomTemplate]
An iterator of CustomTemplate entities.
An iterator of CustomTemplates.
"""
for item in self.search(text=text, max_items=max_items, offset=offset):
try:
yield self.get_by_id(id=item.id)
except AlbertHTTPError as e:
logger.warning(f"Error hydrating custom template {item.id}: {e}")
params = {
"startKey": start_key,
"createdBy": created_by,
"category": category,
}
params["name"] = ensure_list(name)

return AlbertPaginator(
mode=PaginationMode.KEY,
path=self.base_path,
session=self.session,
params=params,
max_items=max_items,
deserialize=lambda items: [CustomTemplate(**item) for item in items],
)

@validate_call
def delete(self, *, id: CustomTemplateId) -> None:
"""Delete a custom template by id."""

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

@validate_call
def update_acl(
self,
*,
custom_template_id: CustomTemplateId,
acl_class: str | None = None,
acls: list[ACL] | None = None,
) -> CustomTemplate:
"""Replace the template's ACL class and/or entries with the provided values and return the updated template."""

if acl_class is None and not acls:
raise ValueError("Provide an ACL class and/or ACL entries to update.")

data = []

if acl_class is not None:
data.append(
{
"operation": PatchOperation.UPDATE.value,
"attribute": "class",
"newValue": acl_class,
}
)

if acls:
entries = []
for entry in acls:
payload: dict[str, str] = {"id": entry.id}
if entry.fgc is not None:
payload["fgc"] = getattr(entry.fgc, "value", entry.fgc)
entries.append(payload)

data.append(
{
"operation": PatchOperation.UPDATE.value,
"attribute": "ACL",
"newValue": entries,
}
)

url = f"{self.base_path}/{custom_template_id}/acl"
self.session.patch(url, json={"data": data})
return self.get_by_id(id=custom_template_id)
Loading