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
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"])
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.
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"])
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")
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")
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")
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")