akiflow.label

Label API — create, update, delete, and list Akiflow labels (projects).

  1"""Label API — create, update, delete, and list Akiflow labels (projects)."""
  2
  3from __future__ import annotations
  4
  5import uuid
  6from datetime import datetime, timezone
  7from typing import TYPE_CHECKING, Any
  8
  9if TYPE_CHECKING:
 10    from .client import Akiflow
 11
 12
 13class Label:
 14    """Operations on Akiflow labels/projects, available as `client.label`.
 15
 16    Labels are what Akiflow calls "projects" in the UI. Tasks reference
 17    labels via their `listId` field. Labels can be organized into folders
 18    using `parent_id` and `type="folder"`.
 19
 20    Example:
 21        ```python
 22        from akiflow import Akiflow
 23
 24        client = Akiflow(refresh_token="def50200...")
 25
 26        # Create a label
 27        label = client.label.create("My Project", color="palette-green")
 28
 29        # List all labels
 30        result = client.label.list()
 31        for lb in result["data"]:
 32            print(lb["title"], lb["id"])
 33
 34        # Find label ID by name
 35        label_id = client.label.get_id("My Project")
 36
 37        # Update
 38        client.label.update(label["id"], title="Renamed Project")
 39
 40        # Delete
 41        client.label.delete(label["id"])
 42        ```
 43    """
 44
 45    # Cache of name -> id, populated on first lookup
 46    _name_cache: dict[str, str] | None = None
 47
 48    def __init__(self, client: Akiflow):
 49        self._client = client
 50        self._name_cache = None
 51
 52    def list(self, *, sync_token: str | None = None, limit: int = 2500) -> dict:
 53        """Fetch labels, with optional incremental sync.
 54
 55        Args:
 56            sync_token: Cursor from a previous `list()` response.
 57            limit: Max labels per page (default 2500).
 58
 59        Returns:
 60            Dict with `data` (list of label dicts), `sync_token`, and
 61            `has_next_page`.
 62        """
 63        params: dict[str, Any] = {"limit": str(limit)}
 64        if sync_token:
 65            params["sync_token"] = sync_token
 66        return self._client._get("/v5/labels", params=params)
 67
 68    def create(
 69        self,
 70        title: str,
 71        *,
 72        color: str | None = None,
 73        icon: str | None = None,
 74        parent_id: str | None = None,
 75        type: str | None = None,
 76        **extra: Any,
 77    ) -> dict:
 78        """Create a new label.
 79
 80        Args:
 81            title: Label name.
 82            color: Color palette name (e.g. `"palette-green"`, `"palette-pink"`).
 83            icon: Emoji icon.
 84            parent_id: UUID of a folder label to nest under.
 85            type: Set to `"folder"` to create a folder, or `None` for a label.
 86            **extra: Additional fields passed directly to the API.
 87
 88        Returns:
 89            The created label dict.
 90
 91        Example:
 92            ```python
 93            label = client.label.create("Work", color="palette-cobalt")
 94            folder = client.label.create("Area", type="folder")
 95            nested = client.label.create("Sub-project", parent_id=folder["id"])
 96            ```
 97        """
 98        now = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
 99        label_id = str(uuid.uuid4())
100        sorting = int(datetime.now(timezone.utc).timestamp() * 1000)
101
102        label: dict[str, Any] = {
103            "id": label_id,
104            "title": title,
105            "color": color,
106            "icon": icon,
107            "sorting": sorting,
108            "parent_id": parent_id,
109            "type": type,
110            "data": {},
111            "global_created_at": now,
112            "global_updated_at": now,
113            "deleted_at": None,
114            **extra,
115        }
116
117        resp = self._client._patch("/v5/labels", json=[label])
118        created = resp["data"][0] if resp.get("data") else resp
119        # Invalidate name cache
120        self._name_cache = None
121        return created
122
123    def update(self, label_id: str, **fields: Any) -> dict:
124        """Update a label by ID.
125
126        Args:
127            label_id: UUID of the label to update.
128            **fields: Any label fields to update (e.g. `title`, `color`, `icon`).
129
130        Returns:
131            The updated label dict.
132
133        Example:
134            ```python
135            client.label.update(label_id, title="New Name", color="palette-red")
136            ```
137        """
138        now = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
139        payload: dict[str, Any] = {
140            "id": label_id,
141            "global_updated_at": now,
142        }
143        payload.update(fields)
144
145        resp = self._client._patch("/v5/labels", json=[payload])
146        self._name_cache = None
147        if resp.get("data"):
148            return resp["data"][0]
149        return resp
150
151    def delete(self, label_id: str) -> dict:
152        """Soft-delete a label.
153
154        Args:
155            label_id: UUID of the label to delete.
156
157        Returns:
158            The updated label dict with `deleted_at` set.
159
160        Example:
161            ```python
162            client.label.delete("d7f7c026-bd8a-4c3a-8c16-d9677ee959e9")
163            ```
164        """
165        now = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
166        payload: dict[str, Any] = {
167            "id": label_id,
168            "title": None,
169            "color": None,
170            "sorting": None,
171            "icon": None,
172            "parent_id": None,
173            "type": None,
174            "data": {},
175            "deleted_at": now,
176            "global_updated_at": now,
177        }
178        resp = self._client._patch("/v5/labels", json=[payload])
179        self._name_cache = None
180        if resp.get("data"):
181            return resp["data"][0]
182        return resp
183
184    def _build_name_cache(self) -> dict[str, str]:
185        """Fetch all labels and build a lowercase name -> id mapping."""
186        result = self.list()
187        cache: dict[str, str] = {}
188        for lb in result.get("data", []):
189            if lb.get("title") and not lb.get("deleted_at"):
190                cache[lb["title"].lower()] = lb["id"]
191        return cache
192
193    def get_id(self, name: str) -> str | None:
194        """Resolve a label name to its UUID.
195
196        Case-insensitive. Returns `None` if no label matches.
197
198        Args:
199            name: Label name to look up.
200
201        Returns:
202            Label UUID, or `None` if not found.
203
204        Example:
205            ```python
206            label_id = client.label.get_id("Work")
207            ```
208        """
209        if self._name_cache is None:
210            self._name_cache = self._build_name_cache()
211        return self._name_cache.get(name.lower())
212
213    def resolve_id(self, label: str) -> str:
214        """Resolve a label name or UUID to a UUID.
215
216        If `label` looks like a UUID, returns it as-is. Otherwise, looks
217        it up by name (case-insensitive).
218
219        Args:
220            label: Label UUID or name.
221
222        Returns:
223            Label UUID.
224
225        Raises:
226            ValueError: If the name doesn't match any label.
227
228        Example:
229            ```python
230            # Both return the same UUID:
231            client.label.resolve_id("d7f7c026-bd8a-4c3a-8c16-d9677ee959e9")
232            client.label.resolve_id("Work")
233            ```
234        """
235        try:
236            uuid.UUID(label)
237            return label
238        except ValueError:
239            pass
240
241        label_id = self.get_id(label)
242        if label_id is None:
243            raise ValueError(f"No label found with name: {label!r}")
244        return label_id
class Label:
 14class Label:
 15    """Operations on Akiflow labels/projects, available as `client.label`.
 16
 17    Labels are what Akiflow calls "projects" in the UI. Tasks reference
 18    labels via their `listId` field. Labels can be organized into folders
 19    using `parent_id` and `type="folder"`.
 20
 21    Example:
 22        ```python
 23        from akiflow import Akiflow
 24
 25        client = Akiflow(refresh_token="def50200...")
 26
 27        # Create a label
 28        label = client.label.create("My Project", color="palette-green")
 29
 30        # List all labels
 31        result = client.label.list()
 32        for lb in result["data"]:
 33            print(lb["title"], lb["id"])
 34
 35        # Find label ID by name
 36        label_id = client.label.get_id("My Project")
 37
 38        # Update
 39        client.label.update(label["id"], title="Renamed Project")
 40
 41        # Delete
 42        client.label.delete(label["id"])
 43        ```
 44    """
 45
 46    # Cache of name -> id, populated on first lookup
 47    _name_cache: dict[str, str] | None = None
 48
 49    def __init__(self, client: Akiflow):
 50        self._client = client
 51        self._name_cache = None
 52
 53    def list(self, *, sync_token: str | None = None, limit: int = 2500) -> dict:
 54        """Fetch labels, with optional incremental sync.
 55
 56        Args:
 57            sync_token: Cursor from a previous `list()` response.
 58            limit: Max labels per page (default 2500).
 59
 60        Returns:
 61            Dict with `data` (list of label dicts), `sync_token`, and
 62            `has_next_page`.
 63        """
 64        params: dict[str, Any] = {"limit": str(limit)}
 65        if sync_token:
 66            params["sync_token"] = sync_token
 67        return self._client._get("/v5/labels", params=params)
 68
 69    def create(
 70        self,
 71        title: str,
 72        *,
 73        color: str | None = None,
 74        icon: str | None = None,
 75        parent_id: str | None = None,
 76        type: str | None = None,
 77        **extra: Any,
 78    ) -> dict:
 79        """Create a new label.
 80
 81        Args:
 82            title: Label name.
 83            color: Color palette name (e.g. `"palette-green"`, `"palette-pink"`).
 84            icon: Emoji icon.
 85            parent_id: UUID of a folder label to nest under.
 86            type: Set to `"folder"` to create a folder, or `None` for a label.
 87            **extra: Additional fields passed directly to the API.
 88
 89        Returns:
 90            The created label dict.
 91
 92        Example:
 93            ```python
 94            label = client.label.create("Work", color="palette-cobalt")
 95            folder = client.label.create("Area", type="folder")
 96            nested = client.label.create("Sub-project", parent_id=folder["id"])
 97            ```
 98        """
 99        now = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
100        label_id = str(uuid.uuid4())
101        sorting = int(datetime.now(timezone.utc).timestamp() * 1000)
102
103        label: dict[str, Any] = {
104            "id": label_id,
105            "title": title,
106            "color": color,
107            "icon": icon,
108            "sorting": sorting,
109            "parent_id": parent_id,
110            "type": type,
111            "data": {},
112            "global_created_at": now,
113            "global_updated_at": now,
114            "deleted_at": None,
115            **extra,
116        }
117
118        resp = self._client._patch("/v5/labels", json=[label])
119        created = resp["data"][0] if resp.get("data") else resp
120        # Invalidate name cache
121        self._name_cache = None
122        return created
123
124    def update(self, label_id: str, **fields: Any) -> dict:
125        """Update a label by ID.
126
127        Args:
128            label_id: UUID of the label to update.
129            **fields: Any label fields to update (e.g. `title`, `color`, `icon`).
130
131        Returns:
132            The updated label dict.
133
134        Example:
135            ```python
136            client.label.update(label_id, title="New Name", color="palette-red")
137            ```
138        """
139        now = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
140        payload: dict[str, Any] = {
141            "id": label_id,
142            "global_updated_at": now,
143        }
144        payload.update(fields)
145
146        resp = self._client._patch("/v5/labels", json=[payload])
147        self._name_cache = None
148        if resp.get("data"):
149            return resp["data"][0]
150        return resp
151
152    def delete(self, label_id: str) -> dict:
153        """Soft-delete a label.
154
155        Args:
156            label_id: UUID of the label to delete.
157
158        Returns:
159            The updated label dict with `deleted_at` set.
160
161        Example:
162            ```python
163            client.label.delete("d7f7c026-bd8a-4c3a-8c16-d9677ee959e9")
164            ```
165        """
166        now = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
167        payload: dict[str, Any] = {
168            "id": label_id,
169            "title": None,
170            "color": None,
171            "sorting": None,
172            "icon": None,
173            "parent_id": None,
174            "type": None,
175            "data": {},
176            "deleted_at": now,
177            "global_updated_at": now,
178        }
179        resp = self._client._patch("/v5/labels", json=[payload])
180        self._name_cache = None
181        if resp.get("data"):
182            return resp["data"][0]
183        return resp
184
185    def _build_name_cache(self) -> dict[str, str]:
186        """Fetch all labels and build a lowercase name -> id mapping."""
187        result = self.list()
188        cache: dict[str, str] = {}
189        for lb in result.get("data", []):
190            if lb.get("title") and not lb.get("deleted_at"):
191                cache[lb["title"].lower()] = lb["id"]
192        return cache
193
194    def get_id(self, name: str) -> str | None:
195        """Resolve a label name to its UUID.
196
197        Case-insensitive. Returns `None` if no label matches.
198
199        Args:
200            name: Label name to look up.
201
202        Returns:
203            Label UUID, or `None` if not found.
204
205        Example:
206            ```python
207            label_id = client.label.get_id("Work")
208            ```
209        """
210        if self._name_cache is None:
211            self._name_cache = self._build_name_cache()
212        return self._name_cache.get(name.lower())
213
214    def resolve_id(self, label: str) -> str:
215        """Resolve a label name or UUID to a UUID.
216
217        If `label` looks like a UUID, returns it as-is. Otherwise, looks
218        it up by name (case-insensitive).
219
220        Args:
221            label: Label UUID or name.
222
223        Returns:
224            Label UUID.
225
226        Raises:
227            ValueError: If the name doesn't match any label.
228
229        Example:
230            ```python
231            # Both return the same UUID:
232            client.label.resolve_id("d7f7c026-bd8a-4c3a-8c16-d9677ee959e9")
233            client.label.resolve_id("Work")
234            ```
235        """
236        try:
237            uuid.UUID(label)
238            return label
239        except ValueError:
240            pass
241
242        label_id = self.get_id(label)
243        if label_id is None:
244            raise ValueError(f"No label found with name: {label!r}")
245        return label_id

Operations on Akiflow labels/projects, available as client.label.

Labels are what Akiflow calls "projects" in the UI. Tasks reference labels via their listId field. Labels can be organized into folders using parent_id and type="folder".

Example:

from akiflow import Akiflow

client = Akiflow(refresh_token="def50200...")

# Create a label
label = client.label.create("My Project", color="palette-green")

# List all labels
result = client.label.list()
for lb in result["data"]:
    print(lb["title"], lb["id"])

# Find label ID by name
label_id = client.label.get_id("My Project")

# Update
client.label.update(label["id"], title="Renamed Project")

# Delete
client.label.delete(label["id"])
Label(client: akiflow.Akiflow)
49    def __init__(self, client: Akiflow):
50        self._client = client
51        self._name_cache = None
def list(self, *, sync_token: str | None = None, limit: int = 2500) -> dict:
53    def list(self, *, sync_token: str | None = None, limit: int = 2500) -> dict:
54        """Fetch labels, with optional incremental sync.
55
56        Args:
57            sync_token: Cursor from a previous `list()` response.
58            limit: Max labels per page (default 2500).
59
60        Returns:
61            Dict with `data` (list of label dicts), `sync_token`, and
62            `has_next_page`.
63        """
64        params: dict[str, Any] = {"limit": str(limit)}
65        if sync_token:
66            params["sync_token"] = sync_token
67        return self._client._get("/v5/labels", params=params)

Fetch labels, with optional incremental sync.

Args: sync_token: Cursor from a previous list() response. limit: Max labels per page (default 2500).

Returns: Dict with data (list of label dicts), sync_token, and has_next_page.

def create( self, title: str, *, color: str | None = None, icon: str | None = None, parent_id: str | None = None, type: str | None = None, **extra: Any) -> dict:
 69    def create(
 70        self,
 71        title: str,
 72        *,
 73        color: str | None = None,
 74        icon: str | None = None,
 75        parent_id: str | None = None,
 76        type: str | None = None,
 77        **extra: Any,
 78    ) -> dict:
 79        """Create a new label.
 80
 81        Args:
 82            title: Label name.
 83            color: Color palette name (e.g. `"palette-green"`, `"palette-pink"`).
 84            icon: Emoji icon.
 85            parent_id: UUID of a folder label to nest under.
 86            type: Set to `"folder"` to create a folder, or `None` for a label.
 87            **extra: Additional fields passed directly to the API.
 88
 89        Returns:
 90            The created label dict.
 91
 92        Example:
 93            ```python
 94            label = client.label.create("Work", color="palette-cobalt")
 95            folder = client.label.create("Area", type="folder")
 96            nested = client.label.create("Sub-project", parent_id=folder["id"])
 97            ```
 98        """
 99        now = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
100        label_id = str(uuid.uuid4())
101        sorting = int(datetime.now(timezone.utc).timestamp() * 1000)
102
103        label: dict[str, Any] = {
104            "id": label_id,
105            "title": title,
106            "color": color,
107            "icon": icon,
108            "sorting": sorting,
109            "parent_id": parent_id,
110            "type": type,
111            "data": {},
112            "global_created_at": now,
113            "global_updated_at": now,
114            "deleted_at": None,
115            **extra,
116        }
117
118        resp = self._client._patch("/v5/labels", json=[label])
119        created = resp["data"][0] if resp.get("data") else resp
120        # Invalidate name cache
121        self._name_cache = None
122        return created

Create a new label.

Args: title: Label name. color: Color palette name (e.g. "palette-green", "palette-pink"). icon: Emoji icon. parent_id: UUID of a folder label to nest under. type: Set to "folder" to create a folder, or None for a label. **extra: Additional fields passed directly to the API.

Returns: The created label dict.

Example:

label = client.label.create("Work", color="palette-cobalt")
folder = client.label.create("Area", type="folder")
nested = client.label.create("Sub-project", parent_id=folder["id"])
def update(self, label_id: str, **fields: Any) -> dict:
124    def update(self, label_id: str, **fields: Any) -> dict:
125        """Update a label by ID.
126
127        Args:
128            label_id: UUID of the label to update.
129            **fields: Any label fields to update (e.g. `title`, `color`, `icon`).
130
131        Returns:
132            The updated label dict.
133
134        Example:
135            ```python
136            client.label.update(label_id, title="New Name", color="palette-red")
137            ```
138        """
139        now = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
140        payload: dict[str, Any] = {
141            "id": label_id,
142            "global_updated_at": now,
143        }
144        payload.update(fields)
145
146        resp = self._client._patch("/v5/labels", json=[payload])
147        self._name_cache = None
148        if resp.get("data"):
149            return resp["data"][0]
150        return resp

Update a label by ID.

Args: label_id: UUID of the label to update. **fields: Any label fields to update (e.g. title, color, icon).

Returns: The updated label dict.

Example:

client.label.update(label_id, title="New Name", color="palette-red")
def delete(self, label_id: str) -> dict:
152    def delete(self, label_id: str) -> dict:
153        """Soft-delete a label.
154
155        Args:
156            label_id: UUID of the label to delete.
157
158        Returns:
159            The updated label dict with `deleted_at` set.
160
161        Example:
162            ```python
163            client.label.delete("d7f7c026-bd8a-4c3a-8c16-d9677ee959e9")
164            ```
165        """
166        now = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
167        payload: dict[str, Any] = {
168            "id": label_id,
169            "title": None,
170            "color": None,
171            "sorting": None,
172            "icon": None,
173            "parent_id": None,
174            "type": None,
175            "data": {},
176            "deleted_at": now,
177            "global_updated_at": now,
178        }
179        resp = self._client._patch("/v5/labels", json=[payload])
180        self._name_cache = None
181        if resp.get("data"):
182            return resp["data"][0]
183        return resp

Soft-delete a label.

Args: label_id: UUID of the label to delete.

Returns: The updated label dict with deleted_at set.

Example:

client.label.delete("d7f7c026-bd8a-4c3a-8c16-d9677ee959e9")
def get_id(self, name: str) -> str | None:
194    def get_id(self, name: str) -> str | None:
195        """Resolve a label name to its UUID.
196
197        Case-insensitive. Returns `None` if no label matches.
198
199        Args:
200            name: Label name to look up.
201
202        Returns:
203            Label UUID, or `None` if not found.
204
205        Example:
206            ```python
207            label_id = client.label.get_id("Work")
208            ```
209        """
210        if self._name_cache is None:
211            self._name_cache = self._build_name_cache()
212        return self._name_cache.get(name.lower())

Resolve a label name to its UUID.

Case-insensitive. Returns None if no label matches.

Args: name: Label name to look up.

Returns: Label UUID, or None if not found.

Example:

label_id = client.label.get_id("Work")
def resolve_id(self, label: str) -> str:
214    def resolve_id(self, label: str) -> str:
215        """Resolve a label name or UUID to a UUID.
216
217        If `label` looks like a UUID, returns it as-is. Otherwise, looks
218        it up by name (case-insensitive).
219
220        Args:
221            label: Label UUID or name.
222
223        Returns:
224            Label UUID.
225
226        Raises:
227            ValueError: If the name doesn't match any label.
228
229        Example:
230            ```python
231            # Both return the same UUID:
232            client.label.resolve_id("d7f7c026-bd8a-4c3a-8c16-d9677ee959e9")
233            client.label.resolve_id("Work")
234            ```
235        """
236        try:
237            uuid.UUID(label)
238            return label
239        except ValueError:
240            pass
241
242        label_id = self.get_id(label)
243        if label_id is None:
244            raise ValueError(f"No label found with name: {label!r}")
245        return label_id

Resolve a label name or UUID to a UUID.

If label looks like a UUID, returns it as-is. Otherwise, looks it up by name (case-insensitive).

Args: label: Label UUID or name.

Returns: Label UUID.

Raises: ValueError: If the name doesn't match any label.

Example:

# Both return the same UUID:
client.label.resolve_id("d7f7c026-bd8a-4c3a-8c16-d9677ee959e9")
client.label.resolve_id("Work")