Skip to content
Merged
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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ class CasCollection(BaseCollection):
- Beta badge: `(🧪 Beta)` (with a space).
- No em dashes (`—`); use commas, colons, or parentheses (org language policy).

- Docstrings must describe **what** a method does from the caller's perspective — never mention internal implementation details or backend API specifics (e.g. diffing, patching, HTTP methods, "returned by the API").
- Docstrings must describe **what** a method does from the caller's perspective — never how it is wired to the backend. Banned terms in public docstrings: `endpoint`, `route`, HTTP verbs (`GET`, `POST`, `PATCH`, `DELETE`), HTTP status codes (`HTTP 200`, `404`), `request` / `response`, `payload`, `JSON`, `diffing`, `patching`, and phrasing like "returned by the API". Keep code comments and log messages free of these too whenever a caller-neutral phrasing exists. The standard `session` Parameters line and `base_path` Attributes line ("The base API route for \<entity\> requests") are the sanctioned exceptions.
- Wrong: `"""Update an attachment by diffing the current server state."""`
- Right: `"""Update an attachment."""`
- Wrong: `The updated attachment returned by the API.`
Expand Down
1 change: 1 addition & 0 deletions docs/collections/personalization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: albert.collections.personalization.PersonalizationCollection
1 change: 1 addition & 0 deletions docs/resources/personalization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: albert.resources.personalization
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ nav:
- Parameter Groups: collections/parameter_groups.md
- Parameters: collections/parameters.md
- PDF Generator: collections/pdf_generator.md
- Personalization: collections/personalization.md
- Pricings: collections/pricings.md
- Product Design: collections/product_design.md
- Projects: collections/projects.md
Expand Down Expand Up @@ -238,6 +239,7 @@ nav:
- Parameter Groups: resources/parameter_groups.md
- Parameters: resources/parameters.md
- PDF Generator: resources/pdf_generator.md
- Personalization: resources/personalization.md
- Pricings: resources/pricings.md
- Product Design: resources/product_design.md
- Projects: resources/projects.md
Expand Down
5 changes: 5 additions & 0 deletions src/albert/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from albert.collections.parameter_groups import ParameterGroupCollection
from albert.collections.parameters import ParameterCollection
from albert.collections.pdf_generator import PDFGeneratorCollection
from albert.collections.personalization import PersonalizationCollection
from albert.collections.pricings import PricingCollection
from albert.collections.product_design import ProductDesignCollection
from albert.collections.projects import ProjectCollection
Expand Down Expand Up @@ -332,6 +333,10 @@ def parameter_groups(self) -> ParameterGroupCollection:
def parameters(self) -> ParameterCollection:
return ParameterCollection(session=self.session)

@property
def personalization(self) -> PersonalizationCollection:
return PersonalizationCollection(session=self.session)

@property
def property_data(self) -> PropertyDataCollection:
return PropertyDataCollection(session=self.session)
Expand Down
231 changes: 231 additions & 0 deletions src/albert/collections/personalization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
from collections.abc import Iterator
from typing import Any

from pydantic import validate_call

from albert.collections.base import BaseCollection
from albert.collections.users import UserCollection
from albert.core.pagination import AlbertPaginator
from albert.core.session import AlbertSession
from albert.core.shared.enums import PaginationMode
from albert.resources.personalization import Personalization, PersonalizationCategory

# The list handler parses `limit` with no fallback, so the SDK always sends a page size.
_PERSONALIZATION_PAGE_LIMIT = 200


class PersonalizationCollection(BaseCollection):
"""Manage user personalization records in the Albert platform.

Personalization records store per-user preferences such as starred projects,
hidden rows, and saved filters. Records always belong to a user: records
created or listed without an explicit user belong to the current user.

This collection is accessed as ``client.personalization``.

!!! example
```python
from albert import Albert
from albert.resources.personalization import Personalization, PersonalizationCategory

client = Albert()
record = client.personalization.create(
personalization=Personalization(
category=PersonalizationCategory.STARRED_PROJECTS,
saved_id="PRO123",
saved_name="Weatherproof Coatings 2026",
)
)
print(record.id)
```

Parameters
----------
session : AlbertSession
The authenticated Albert session used for API calls.

Attributes
----------
base_path : str
The base API route for personalization requests.

Methods
-------
create(personalization) -> Personalization
Create a new personalization record for the current user.
get_by_id(id) -> Personalization
Get a single personalization record by its ID.
get_all(...) -> Iterator[Personalization]
Get personalization records, with optional filters.
delete(id) -> None
Delete a personalization record by its ID.
"""

_api_version = "v3"

def __init__(self, *, session: AlbertSession):
super().__init__(session=session)
self.base_path = f"/api/{PersonalizationCollection._api_version}/personalization"

@validate_call
def create(self, *, personalization: Personalization) -> Personalization:
"""Create a new personalization record for the current user.

!!! example
```python
from albert.resources.personalization import Personalization, PersonalizationCategory

record = client.personalization.create(
personalization=Personalization(
category=PersonalizationCategory.STARRED_PROJECTS,
saved_id="PRO123",
saved_name="Weatherproof Coatings 2026",
)
)
record.id
# 'USP123'
```

Parameters
----------
personalization : Personalization
The record to create. Requires ``category`` plus the fields the
category stores (e.g. ``saved_id`` and ``saved_name`` for a starred
project).

Returns
-------
Personalization
The created record, populated with its assigned ID.
"""
payload = personalization.model_dump(
by_alias=True, exclude_none=True, exclude_unset=True, mode="json"
)
response = self.session.post(self.base_path, json=[payload])
data = response.json()[0]
# The create response omits `category`; the value just sent is authoritative.
data.setdefault("category", payload["category"])
return Personalization(**data)

@validate_call
def get_by_id(self, *, id: str) -> Personalization:
"""Get a personalization record by its ID.

!!! example
```python
record = client.personalization.get_by_id(id="USP123")
record.category
# <PersonalizationCategory.STARRED_PROJECTS: 'Starred Projects'>
```

Parameters
----------
id : str
The ID of the record to retrieve (format ``USP...``).

Returns
-------
Personalization
The fully populated record.
"""
response = self.session.get(f"{self.base_path}/{id}")
return Personalization(**response.json())

@validate_call
def get_all(
self,
*,
category: PersonalizationCategory | None = None,
sub_category: str | None = None,
user_id: str | None = None,
max_items: int | None = None,
) -> Iterator[Personalization]:
"""Get personalization records, with optional filters.

When ``user_id`` is not given, records of the current user are returned.

!!! example
```python
from albert.resources.personalization import PersonalizationCategory

for record in client.personalization.get_all(
category=PersonalizationCategory.STARRED_PROJECTS
):
print(record.id, record.saved_id)
```

Parameters
----------
category : PersonalizationCategory, optional
Only return records in this category.
sub_category : str, optional
Only return records in this subcategory. Requires ``category``.
user_id : str, optional
Only return records belonging to this user. Defaults to the current
user.
max_items : int, optional
Maximum number of records to return in total. If None, returns all
matching records.

Returns
-------
Iterator[Personalization]
An iterator of matching records.

Raises
------
ValueError
If ``sub_category`` is given without ``category``.
"""
params = self._get_all_params(
category=category, sub_category=sub_category, user_id=user_id
)
if "createdBy" not in params:
params["createdBy"] = UserCollection(session=self.session).get_current_user().id
return AlbertPaginator(
mode=PaginationMode.KEY,
path=self.base_path,
session=self.session,
params=params,
max_items=max_items,
deserialize=lambda items: [Personalization(**item) for item in items],
)

@validate_call
def delete(self, *, id: str) -> None:
"""Delete a personalization record by its ID.

!!! example
```python
client.personalization.delete(id="USP123")
```

Parameters
----------
id : str
The ID of the record to delete (format ``USP...``).

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

@staticmethod
def _get_all_params(
*,
category: PersonalizationCategory | None,
sub_category: str | None,
user_id: str | None,
) -> dict[str, Any]:
"""Build the query parameters for listing personalization records."""
if sub_category and not category:
raise ValueError("`category` is required when `sub_category` is provided.")
params: dict[str, Any] = {"limit": _PERSONALIZATION_PAGE_LIMIT}
if user_id:
params["createdBy"] = user_id
if category:
params["category"] = category.value
if sub_category:
params["subCategory"] = sub_category
return params
Loading
Loading