better-notion 1.0.0__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- better_notion/_sdk/client.py +106 -3
- better_notion/_sdk/plugins.py +180 -0
- better_notion/plugins/base.py +99 -1
- better_notion/plugins/loader.py +51 -1
- better_notion/plugins/official/agents.py +150 -5
- better_notion/plugins/official/agents_cli.py +1767 -0
- better_notion/plugins/official/agents_sdk/__init__.py +30 -0
- better_notion/plugins/official/agents_sdk/managers.py +973 -0
- better_notion/plugins/official/agents_sdk/models.py +2256 -0
- better_notion/plugins/official/agents_sdk/plugin.py +146 -0
- {better_notion-1.0.0.dist-info → better_notion-1.1.0.dist-info}/METADATA +2 -2
- {better_notion-1.0.0.dist-info → better_notion-1.1.0.dist-info}/RECORD +15 -9
- {better_notion-1.0.0.dist-info → better_notion-1.1.0.dist-info}/WHEEL +0 -0
- {better_notion-1.0.0.dist-info → better_notion-1.1.0.dist-info}/entry_points.txt +0 -0
- {better_notion-1.0.0.dist-info → better_notion-1.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,2256 @@
|
|
|
1
|
+
"""Workflow entity models for the agents SDK plugin.
|
|
2
|
+
|
|
3
|
+
This module provides SDK model classes for workflow entities:
|
|
4
|
+
- Organization
|
|
5
|
+
- Project
|
|
6
|
+
- Version
|
|
7
|
+
- Task
|
|
8
|
+
|
|
9
|
+
These models inherit from BaseEntity and provide autonomous CRUD operations
|
|
10
|
+
with caching support through the plugin system.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
from better_notion._sdk.base.entity import BaseEntity
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from better_notion._sdk.client import NotionClient
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Organization(BaseEntity):
|
|
24
|
+
"""
|
|
25
|
+
Organization entity representing a company or team.
|
|
26
|
+
|
|
27
|
+
An organization contains multiple projects and serves as the top-level
|
|
28
|
+
grouping in the workflow hierarchy.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
id: Organization page ID
|
|
32
|
+
name: Organization name
|
|
33
|
+
slug: URL-safe identifier
|
|
34
|
+
description: Organization purpose
|
|
35
|
+
repository_url: Code repository URL
|
|
36
|
+
status: Organization status (Active, Archived, On Hold)
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
>>> org = await Organization.get("org_id", client=client)
|
|
40
|
+
>>> print(org.name)
|
|
41
|
+
>>> projects = await org.projects()
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, client: "NotionClient", data: dict[str, Any]) -> None:
|
|
45
|
+
"""Initialize organization with client and API data.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
client: NotionClient instance
|
|
49
|
+
data: Raw API response data
|
|
50
|
+
"""
|
|
51
|
+
super().__init__(client, data)
|
|
52
|
+
self._organization_cache = client.plugin_cache("organizations")
|
|
53
|
+
|
|
54
|
+
# ===== PROPERTIES =====
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def name(self) -> str:
|
|
58
|
+
"""Get organization name from title property.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Organization name as string
|
|
62
|
+
"""
|
|
63
|
+
title_prop = self._data["properties"].get("Name") or self._data["properties"].get("name")
|
|
64
|
+
if title_prop and title_prop.get("type") == "title":
|
|
65
|
+
title_data = title_prop.get("title", [])
|
|
66
|
+
if title_data:
|
|
67
|
+
return title_data[0].get("plain_text", "")
|
|
68
|
+
return ""
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def slug(self) -> str:
|
|
72
|
+
"""Get organization slug (URL-safe identifier).
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Slug string or empty string if not set
|
|
76
|
+
"""
|
|
77
|
+
slug_prop = self._data["properties"].get("Slug") or self._data["properties"].get("slug")
|
|
78
|
+
if slug_prop and slug_prop.get("type") == "rich_text":
|
|
79
|
+
text_data = slug_prop.get("rich_text", [])
|
|
80
|
+
if text_data:
|
|
81
|
+
return text_data[0].get("plain_text", "")
|
|
82
|
+
return ""
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def description(self) -> str:
|
|
86
|
+
"""Get organization description.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Description string
|
|
90
|
+
"""
|
|
91
|
+
desc_prop = self._data["properties"].get("Description") or self._data["properties"].get("description")
|
|
92
|
+
if desc_prop and desc_prop.get("type") == "rich_text":
|
|
93
|
+
text_data = desc_prop.get("rich_text", [])
|
|
94
|
+
if text_data:
|
|
95
|
+
return text_data[0].get("plain_text", "")
|
|
96
|
+
return ""
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def repository_url(self) -> str | None:
|
|
100
|
+
"""Get repository URL.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Repository URL string or None
|
|
104
|
+
"""
|
|
105
|
+
repo_prop = self._data["properties"].get("Repository URL") or self._data["properties"].get("repository_url")
|
|
106
|
+
if repo_prop and repo_prop.get("type") == "url":
|
|
107
|
+
return repo_prop.get("url")
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def status(self) -> str:
|
|
112
|
+
"""Get organization status.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Status string (Active, Archived, On Hold)
|
|
116
|
+
"""
|
|
117
|
+
status_prop = self._data["properties"].get("Status") or self._data["properties"].get("status")
|
|
118
|
+
if status_prop and status_prop.get("type") == "select":
|
|
119
|
+
select_data = status_prop.get("select")
|
|
120
|
+
if select_data:
|
|
121
|
+
return select_data.get("name", "Unknown")
|
|
122
|
+
return "Unknown"
|
|
123
|
+
|
|
124
|
+
# ===== AUTONOMOUS METHODS =====
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
async def get(cls, org_id: str, *, client: "NotionClient") -> "Organization":
|
|
128
|
+
"""
|
|
129
|
+
Get an organization by ID.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
org_id: Organization page ID
|
|
133
|
+
client: NotionClient instance
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Organization instance
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
Exception: If API call fails
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
>>> org = await Organization.get("org_123", client=client)
|
|
143
|
+
"""
|
|
144
|
+
# Check plugin cache
|
|
145
|
+
cache = client.plugin_cache("organizations")
|
|
146
|
+
if cache and org_id in cache:
|
|
147
|
+
return cache[org_id]
|
|
148
|
+
|
|
149
|
+
# Fetch from API
|
|
150
|
+
data = await client._api.pages.get(page_id=org_id)
|
|
151
|
+
org = cls(client, data)
|
|
152
|
+
|
|
153
|
+
# Cache it
|
|
154
|
+
if cache:
|
|
155
|
+
cache[org_id] = org
|
|
156
|
+
|
|
157
|
+
return org
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
async def create(
|
|
161
|
+
cls,
|
|
162
|
+
*,
|
|
163
|
+
client: "NotionClient",
|
|
164
|
+
database_id: str,
|
|
165
|
+
name: str,
|
|
166
|
+
slug: str | None = None,
|
|
167
|
+
description: str | None = None,
|
|
168
|
+
repository_url: str | None = None,
|
|
169
|
+
status: str = "Active",
|
|
170
|
+
) -> "Organization":
|
|
171
|
+
"""
|
|
172
|
+
Create a new organization.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
client: NotionClient instance
|
|
176
|
+
database_id: Organizations database ID
|
|
177
|
+
name: Organization name
|
|
178
|
+
slug: URL-safe identifier (defaults to name if not provided)
|
|
179
|
+
description: Organization description
|
|
180
|
+
repository_url: Code repository URL
|
|
181
|
+
status: Organization status (default: Active)
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Created Organization instance
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
Exception: If API call fails
|
|
188
|
+
|
|
189
|
+
Example:
|
|
190
|
+
>>> org = await Organization.create(
|
|
191
|
+
... client=client,
|
|
192
|
+
... database_id="db_123",
|
|
193
|
+
... name="My Organization",
|
|
194
|
+
... slug="my-org",
|
|
195
|
+
... description="A great organization"
|
|
196
|
+
... )
|
|
197
|
+
"""
|
|
198
|
+
from better_notion._api.properties import Title, RichText, URL, Select
|
|
199
|
+
|
|
200
|
+
# Build properties
|
|
201
|
+
properties: dict[str, Any] = {
|
|
202
|
+
"Name": Title(name),
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if slug:
|
|
206
|
+
properties["Slug"] = RichText(slug)
|
|
207
|
+
if description:
|
|
208
|
+
properties["Description"] = RichText(description)
|
|
209
|
+
if repository_url:
|
|
210
|
+
properties["Repository URL"] = URL(repository_url)
|
|
211
|
+
if status:
|
|
212
|
+
properties["Status"] = Select(status)
|
|
213
|
+
|
|
214
|
+
# Create page
|
|
215
|
+
data = await client._api.pages.create(
|
|
216
|
+
parent={"database_id": database_id},
|
|
217
|
+
properties=properties,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
org = cls(client, data)
|
|
221
|
+
|
|
222
|
+
# Cache it
|
|
223
|
+
cache = client.plugin_cache("organizations")
|
|
224
|
+
if cache:
|
|
225
|
+
cache[org.id] = org
|
|
226
|
+
|
|
227
|
+
return org
|
|
228
|
+
|
|
229
|
+
async def update(
|
|
230
|
+
self,
|
|
231
|
+
*,
|
|
232
|
+
name: str | None = None,
|
|
233
|
+
slug: str | None = None,
|
|
234
|
+
description: str | None = None,
|
|
235
|
+
repository_url: str | None = None,
|
|
236
|
+
status: str | None = None,
|
|
237
|
+
) -> "Organization":
|
|
238
|
+
"""
|
|
239
|
+
Update organization properties.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
name: New name
|
|
243
|
+
slug: New slug
|
|
244
|
+
description: New description
|
|
245
|
+
repository_url: New repository URL
|
|
246
|
+
status: New status
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Updated Organization instance
|
|
250
|
+
|
|
251
|
+
Example:
|
|
252
|
+
>>> org = await org.update(status="Archived")
|
|
253
|
+
"""
|
|
254
|
+
from better_notion._api.properties import Title, RichText, URL, Select
|
|
255
|
+
|
|
256
|
+
# Build properties to update
|
|
257
|
+
properties: dict[str, Any] = {}
|
|
258
|
+
|
|
259
|
+
if name is not None:
|
|
260
|
+
properties["Name"] = Title(name)
|
|
261
|
+
if slug is not None:
|
|
262
|
+
properties["Slug"] = RichText(slug)
|
|
263
|
+
if description is not None:
|
|
264
|
+
properties["Description"] = RichText(description)
|
|
265
|
+
if repository_url is not None:
|
|
266
|
+
properties["Repository URL"] = URL(repository_url)
|
|
267
|
+
if status is not None:
|
|
268
|
+
properties["Status"] = Select(status)
|
|
269
|
+
|
|
270
|
+
# Update page
|
|
271
|
+
data = await self._client._api.pages.update(
|
|
272
|
+
page_id=self.id,
|
|
273
|
+
properties=properties,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Update instance
|
|
277
|
+
self._data = data
|
|
278
|
+
|
|
279
|
+
# Invalidate cache
|
|
280
|
+
cache = self._client.plugin_cache("organizations")
|
|
281
|
+
if cache and self.id in cache:
|
|
282
|
+
del cache[self.id]
|
|
283
|
+
|
|
284
|
+
return self
|
|
285
|
+
|
|
286
|
+
async def delete(self) -> None:
|
|
287
|
+
"""
|
|
288
|
+
Delete the organization.
|
|
289
|
+
|
|
290
|
+
Example:
|
|
291
|
+
>>> await org.delete()
|
|
292
|
+
"""
|
|
293
|
+
await self._client._api.pages.delete(page_id=self.id)
|
|
294
|
+
|
|
295
|
+
# Invalidate cache
|
|
296
|
+
cache = self._client.plugin_cache("organizations")
|
|
297
|
+
if cache and self.id in cache:
|
|
298
|
+
del cache[self.id]
|
|
299
|
+
|
|
300
|
+
async def projects(self) -> list["Project"]:
|
|
301
|
+
"""
|
|
302
|
+
Get all projects in this organization.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
List of Project instances
|
|
306
|
+
|
|
307
|
+
Example:
|
|
308
|
+
>>> projects = await org.projects()
|
|
309
|
+
"""
|
|
310
|
+
# Get database ID from workspace config
|
|
311
|
+
import json
|
|
312
|
+
from pathlib import Path
|
|
313
|
+
|
|
314
|
+
config_path = Path.home() / ".notion" / "workspace.json"
|
|
315
|
+
if not config_path.exists():
|
|
316
|
+
return []
|
|
317
|
+
|
|
318
|
+
config = json.loads(config_path.read_text())
|
|
319
|
+
projects_db_id = config.get("Projects")
|
|
320
|
+
|
|
321
|
+
if not projects_db_id:
|
|
322
|
+
return []
|
|
323
|
+
|
|
324
|
+
# Query projects with relation filter
|
|
325
|
+
from better_notion._api.properties import Relation
|
|
326
|
+
|
|
327
|
+
response = await self._client._api.databases.query(
|
|
328
|
+
database_id=projects_db_id,
|
|
329
|
+
filter={
|
|
330
|
+
"property": "Organization",
|
|
331
|
+
"relation": {"contains": self.id},
|
|
332
|
+
},
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
from better_notion.plugins.official.agents_sdk.models import Project
|
|
336
|
+
return [Project(self._client, page_data) for page_data in response.get("results", [])]
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class Project(BaseEntity):
|
|
340
|
+
"""
|
|
341
|
+
Project entity representing a software project.
|
|
342
|
+
|
|
343
|
+
A project belongs to an organization and contains multiple versions.
|
|
344
|
+
|
|
345
|
+
Attributes:
|
|
346
|
+
id: Project page ID
|
|
347
|
+
name: Project name
|
|
348
|
+
slug: URL-safe identifier
|
|
349
|
+
description: Project description
|
|
350
|
+
repository: Git repository URL
|
|
351
|
+
status: Project status (Active, Archived, Planning, Completed)
|
|
352
|
+
tech_stack: List of technologies used
|
|
353
|
+
role: Project role (Developer, PM, QA, etc.)
|
|
354
|
+
organization: Parent organization
|
|
355
|
+
|
|
356
|
+
Example:
|
|
357
|
+
>>> project = await Project.get("project_id", client=client)
|
|
358
|
+
>>> print(project.name)
|
|
359
|
+
>>> versions = await project.versions()
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
def __init__(self, client: "NotionClient", data: dict[str, Any]) -> None:
|
|
363
|
+
"""Initialize project with client and API data.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
client: NotionClient instance
|
|
367
|
+
data: Raw API response data
|
|
368
|
+
"""
|
|
369
|
+
super().__init__(client, data)
|
|
370
|
+
self._project_cache = client.plugin_cache("projects")
|
|
371
|
+
|
|
372
|
+
# ===== PROPERTIES =====
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def name(self) -> str:
|
|
376
|
+
"""Get project name from title property."""
|
|
377
|
+
title_prop = self._data["properties"].get("Name") or self._data["properties"].get("name")
|
|
378
|
+
if title_prop and title_prop.get("type") == "title":
|
|
379
|
+
title_data = title_prop.get("title", [])
|
|
380
|
+
if title_data:
|
|
381
|
+
return title_data[0].get("plain_text", "")
|
|
382
|
+
return ""
|
|
383
|
+
|
|
384
|
+
@property
|
|
385
|
+
def slug(self) -> str:
|
|
386
|
+
"""Get project slug."""
|
|
387
|
+
slug_prop = self._data["properties"].get("Slug") or self._data["properties"].get("slug")
|
|
388
|
+
if slug_prop and slug_prop.get("type") == "rich_text":
|
|
389
|
+
text_data = slug_prop.get("rich_text", [])
|
|
390
|
+
if text_data:
|
|
391
|
+
return text_data[0].get("plain_text", "")
|
|
392
|
+
return ""
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def description(self) -> str:
|
|
396
|
+
"""Get project description."""
|
|
397
|
+
desc_prop = self._data["properties"].get("Description") or self._data["properties"].get("description")
|
|
398
|
+
if desc_prop and desc_prop.get("type") == "rich_text":
|
|
399
|
+
text_data = desc_prop.get("rich_text", [])
|
|
400
|
+
if text_data:
|
|
401
|
+
return text_data[0].get("plain_text", "")
|
|
402
|
+
return ""
|
|
403
|
+
|
|
404
|
+
@property
|
|
405
|
+
def repository(self) -> str | None:
|
|
406
|
+
"""Get repository URL."""
|
|
407
|
+
repo_prop = self._data["properties"].get("Repository") or self._data["properties"].get("repository")
|
|
408
|
+
if repo_prop and repo_prop.get("type") == "url":
|
|
409
|
+
return repo_prop.get("url")
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
@property
|
|
413
|
+
def status(self) -> str:
|
|
414
|
+
"""Get project status."""
|
|
415
|
+
status_prop = self._data["properties"].get("Status") or self._data["properties"].get("status")
|
|
416
|
+
if status_prop and status_prop.get("type") == "select":
|
|
417
|
+
select_data = status_prop.get("select")
|
|
418
|
+
if select_data:
|
|
419
|
+
return select_data.get("name", "Unknown")
|
|
420
|
+
return "Unknown"
|
|
421
|
+
|
|
422
|
+
@property
|
|
423
|
+
def tech_stack(self) -> list[str]:
|
|
424
|
+
"""Get tech stack as list of strings."""
|
|
425
|
+
tech_prop = self._data["properties"].get("Tech Stack") or self._data["properties"].get("tech_stack")
|
|
426
|
+
if tech_prop and tech_prop.get("type") == "multi_select":
|
|
427
|
+
multi_data = tech_prop.get("multi_select", [])
|
|
428
|
+
return [item.get("name", "") for item in multi_data]
|
|
429
|
+
return []
|
|
430
|
+
|
|
431
|
+
@property
|
|
432
|
+
def role(self) -> str:
|
|
433
|
+
"""Get project role."""
|
|
434
|
+
role_prop = self._data["properties"].get("Role") or self._data["properties"].get("role")
|
|
435
|
+
if role_prop and role_prop.get("type") == "select":
|
|
436
|
+
select_data = role_prop.get("select")
|
|
437
|
+
if select_data:
|
|
438
|
+
return select_data.get("name", "Unknown")
|
|
439
|
+
return "Unknown"
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def organization_id(self) -> str | None:
|
|
443
|
+
"""Get parent organization ID."""
|
|
444
|
+
org_prop = self._data["properties"].get("Organization") or self._data["properties"].get("organization")
|
|
445
|
+
if org_prop and org_prop.get("type") == "relation":
|
|
446
|
+
relations = org_prop.get("relation", [])
|
|
447
|
+
if relations:
|
|
448
|
+
return relations[0].get("id")
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
# ===== AUTONOMOUS METHODS =====
|
|
452
|
+
|
|
453
|
+
@classmethod
|
|
454
|
+
async def get(cls, project_id: str, *, client: "NotionClient") -> "Project":
|
|
455
|
+
"""Get a project by ID."""
|
|
456
|
+
# Check plugin cache
|
|
457
|
+
cache = client.plugin_cache("projects")
|
|
458
|
+
if cache and project_id in cache:
|
|
459
|
+
return cache[project_id]
|
|
460
|
+
|
|
461
|
+
# Fetch from API
|
|
462
|
+
data = await client._api.pages.get(page_id=project_id)
|
|
463
|
+
project = cls(client, data)
|
|
464
|
+
|
|
465
|
+
# Cache it
|
|
466
|
+
if cache:
|
|
467
|
+
cache[project_id] = project
|
|
468
|
+
|
|
469
|
+
return project
|
|
470
|
+
|
|
471
|
+
@classmethod
|
|
472
|
+
async def create(
|
|
473
|
+
cls,
|
|
474
|
+
*,
|
|
475
|
+
client: "NotionClient",
|
|
476
|
+
database_id: str,
|
|
477
|
+
name: str,
|
|
478
|
+
organization_id: str,
|
|
479
|
+
slug: str | None = None,
|
|
480
|
+
description: str | None = None,
|
|
481
|
+
repository: str | None = None,
|
|
482
|
+
status: str = "Active",
|
|
483
|
+
tech_stack: list[str] | None = None,
|
|
484
|
+
role: str = "Developer",
|
|
485
|
+
) -> "Project":
|
|
486
|
+
"""Create a new project."""
|
|
487
|
+
from better_notion._api.properties import Title, RichText, URL, Select, MultiSelect, Relation
|
|
488
|
+
|
|
489
|
+
# Build properties
|
|
490
|
+
properties: dict[str, Any] = {
|
|
491
|
+
"Name": Title(name),
|
|
492
|
+
"Organization": Relation([organization_id]),
|
|
493
|
+
"Role": Select(role),
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if slug:
|
|
497
|
+
properties["Slug"] = RichText(slug)
|
|
498
|
+
if description:
|
|
499
|
+
properties["Description"] = RichText(description)
|
|
500
|
+
if repository:
|
|
501
|
+
properties["Repository"] = URL(repository)
|
|
502
|
+
if status:
|
|
503
|
+
properties["Status"] = Select(status)
|
|
504
|
+
if tech_stack:
|
|
505
|
+
properties["Tech Stack"] = MultiSelect(tech_stack)
|
|
506
|
+
|
|
507
|
+
# Create page
|
|
508
|
+
data = await client._api.pages.create(
|
|
509
|
+
parent={"database_id": database_id},
|
|
510
|
+
properties=properties,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
project = cls(client, data)
|
|
514
|
+
|
|
515
|
+
# Cache it
|
|
516
|
+
cache = client.plugin_cache("projects")
|
|
517
|
+
if cache:
|
|
518
|
+
cache[project.id] = project
|
|
519
|
+
|
|
520
|
+
return project
|
|
521
|
+
|
|
522
|
+
async def update(
|
|
523
|
+
self,
|
|
524
|
+
*,
|
|
525
|
+
name: str | None = None,
|
|
526
|
+
slug: str | None = None,
|
|
527
|
+
description: str | None = None,
|
|
528
|
+
repository: str | None = None,
|
|
529
|
+
status: str | None = None,
|
|
530
|
+
tech_stack: list[str] | None = None,
|
|
531
|
+
role: str | None = None,
|
|
532
|
+
) -> "Project":
|
|
533
|
+
"""Update project properties."""
|
|
534
|
+
from better_notion._api.properties import Title, RichText, URL, Select, MultiSelect
|
|
535
|
+
|
|
536
|
+
# Build properties to update
|
|
537
|
+
properties: dict[str, Any] = {}
|
|
538
|
+
|
|
539
|
+
if name is not None:
|
|
540
|
+
properties["Name"] = Title(name)
|
|
541
|
+
if slug is not None:
|
|
542
|
+
properties["Slug"] = RichText(slug)
|
|
543
|
+
if description is not None:
|
|
544
|
+
properties["Description"] = RichText(description)
|
|
545
|
+
if repository is not None:
|
|
546
|
+
properties["Repository"] = URL(repository)
|
|
547
|
+
if status is not None:
|
|
548
|
+
properties["Status"] = Select(status)
|
|
549
|
+
if tech_stack is not None:
|
|
550
|
+
properties["Tech Stack"] = MultiSelect(tech_stack)
|
|
551
|
+
if role is not None:
|
|
552
|
+
properties["Role"] = Select(role)
|
|
553
|
+
|
|
554
|
+
# Update page
|
|
555
|
+
data = await self._client._api.pages.update(
|
|
556
|
+
page_id=self.id,
|
|
557
|
+
properties=properties,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
# Update instance
|
|
561
|
+
self._data = data
|
|
562
|
+
|
|
563
|
+
# Invalidate cache
|
|
564
|
+
cache = self._client.plugin_cache("projects")
|
|
565
|
+
if cache and self.id in cache:
|
|
566
|
+
del cache[self.id]
|
|
567
|
+
|
|
568
|
+
return self
|
|
569
|
+
|
|
570
|
+
async def delete(self) -> None:
|
|
571
|
+
"""Delete the project."""
|
|
572
|
+
await self._client._api.pages.delete(page_id=self.id)
|
|
573
|
+
|
|
574
|
+
# Invalidate cache
|
|
575
|
+
cache = self._client.plugin_cache("projects")
|
|
576
|
+
if cache and self.id in cache:
|
|
577
|
+
del cache[self.id]
|
|
578
|
+
|
|
579
|
+
async def organization(self) -> Organization | None:
|
|
580
|
+
"""Get parent organization."""
|
|
581
|
+
if self.organization_id:
|
|
582
|
+
return await Organization.get(self.organization_id, client=self._client)
|
|
583
|
+
return None
|
|
584
|
+
|
|
585
|
+
async def versions(self) -> list["Version"]:
|
|
586
|
+
"""Get all versions in this project."""
|
|
587
|
+
# Get database ID from workspace config
|
|
588
|
+
import json
|
|
589
|
+
from pathlib import Path
|
|
590
|
+
|
|
591
|
+
config_path = Path.home() / ".notion" / "workspace.json"
|
|
592
|
+
if not config_path.exists():
|
|
593
|
+
return []
|
|
594
|
+
|
|
595
|
+
config = json.loads(config_path.read_text())
|
|
596
|
+
versions_db_id = config.get("Versions")
|
|
597
|
+
|
|
598
|
+
if not versions_db_id:
|
|
599
|
+
return []
|
|
600
|
+
|
|
601
|
+
# Query versions with relation filter
|
|
602
|
+
response = await self._client._api.databases.query(
|
|
603
|
+
database_id=versions_db_id,
|
|
604
|
+
filter={
|
|
605
|
+
"property": "Project",
|
|
606
|
+
"relation": {"contains": self.id},
|
|
607
|
+
},
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
return [Version(self._client, page_data) for page_data in response.get("results", [])]
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
class Version(BaseEntity):
|
|
614
|
+
"""
|
|
615
|
+
Version entity representing a project version/release.
|
|
616
|
+
|
|
617
|
+
A version belongs to a project and contains multiple tasks.
|
|
618
|
+
|
|
619
|
+
Attributes:
|
|
620
|
+
id: Version page ID
|
|
621
|
+
name: Version name (e.g., "v1.0.0")
|
|
622
|
+
project: Parent project
|
|
623
|
+
status: Version status (Planning, Alpha, Beta, RC, In Progress, Released)
|
|
624
|
+
type: Version type (Major, Minor, Patch, Hotfix)
|
|
625
|
+
branch_name: Git branch name
|
|
626
|
+
progress: Progress percentage (0-100)
|
|
627
|
+
|
|
628
|
+
Example:
|
|
629
|
+
>>> version = await Version.get("version_id", client=client)
|
|
630
|
+
>>> print(version.name)
|
|
631
|
+
>>> tasks = await version.tasks()
|
|
632
|
+
"""
|
|
633
|
+
|
|
634
|
+
def __init__(self, client: "NotionClient", data: dict[str, Any]) -> None:
|
|
635
|
+
"""Initialize version with client and API data."""
|
|
636
|
+
super().__init__(client, data)
|
|
637
|
+
self._version_cache = client.plugin_cache("versions")
|
|
638
|
+
|
|
639
|
+
# ===== PROPERTIES =====
|
|
640
|
+
|
|
641
|
+
@property
|
|
642
|
+
def name(self) -> str:
|
|
643
|
+
"""Get version name from title property."""
|
|
644
|
+
title_prop = self._data["properties"].get("Version") or self._data["properties"].get("version")
|
|
645
|
+
if title_prop and title_prop.get("type") == "title":
|
|
646
|
+
title_data = title_prop.get("title", [])
|
|
647
|
+
if title_data:
|
|
648
|
+
return title_data[0].get("plain_text", "")
|
|
649
|
+
return ""
|
|
650
|
+
|
|
651
|
+
@property
|
|
652
|
+
def status(self) -> str:
|
|
653
|
+
"""Get version status."""
|
|
654
|
+
status_prop = self._data["properties"].get("Status") or self._data["properties"].get("status")
|
|
655
|
+
if status_prop and status_prop.get("type") == "select":
|
|
656
|
+
select_data = status_prop.get("select")
|
|
657
|
+
if select_data:
|
|
658
|
+
return select_data.get("name", "Unknown")
|
|
659
|
+
return "Unknown"
|
|
660
|
+
|
|
661
|
+
@property
|
|
662
|
+
def version_type(self) -> str:
|
|
663
|
+
"""Get version type."""
|
|
664
|
+
type_prop = self._data["properties"].get("Type") or self._data["properties"].get("type")
|
|
665
|
+
if type_prop and type_prop.get("type") == "select":
|
|
666
|
+
select_data = type_prop.get("select")
|
|
667
|
+
if select_data:
|
|
668
|
+
return select_data.get("name", "Unknown")
|
|
669
|
+
return "Unknown"
|
|
670
|
+
|
|
671
|
+
@property
|
|
672
|
+
def branch_name(self) -> str:
|
|
673
|
+
"""Get git branch name."""
|
|
674
|
+
branch_prop = self._data["properties"].get("Branch Name") or self._data["properties"].get("branch_name")
|
|
675
|
+
if branch_prop and branch_prop.get("type") == "rich_text":
|
|
676
|
+
text_data = branch_prop.get("rich_text", [])
|
|
677
|
+
if text_data:
|
|
678
|
+
return text_data[0].get("plain_text", "")
|
|
679
|
+
return ""
|
|
680
|
+
|
|
681
|
+
@property
|
|
682
|
+
def progress(self) -> int:
|
|
683
|
+
"""Get progress percentage."""
|
|
684
|
+
progress_prop = self._data["properties"].get("Progress") or self._data["properties"].get("progress")
|
|
685
|
+
if progress_prop and progress_prop.get("type") == "number":
|
|
686
|
+
return progress_prop.get("number", 0) or 0
|
|
687
|
+
return 0
|
|
688
|
+
|
|
689
|
+
@property
|
|
690
|
+
def project_id(self) -> str | None:
|
|
691
|
+
"""Get parent project ID."""
|
|
692
|
+
project_prop = self._data["properties"].get("Project") or self._data["properties"].get("project")
|
|
693
|
+
if project_prop and project_prop.get("type") == "relation":
|
|
694
|
+
relations = project_prop.get("relation", [])
|
|
695
|
+
if relations:
|
|
696
|
+
return relations[0].get("id")
|
|
697
|
+
return None
|
|
698
|
+
|
|
699
|
+
# ===== AUTONOMOUS METHODS =====
|
|
700
|
+
|
|
701
|
+
@classmethod
|
|
702
|
+
async def get(cls, version_id: str, *, client: "NotionClient") -> "Version":
|
|
703
|
+
"""Get a version by ID."""
|
|
704
|
+
# Check plugin cache
|
|
705
|
+
cache = client.plugin_cache("versions")
|
|
706
|
+
if cache and version_id in cache:
|
|
707
|
+
return cache[version_id]
|
|
708
|
+
|
|
709
|
+
# Fetch from API
|
|
710
|
+
data = await client._api.pages.get(page_id=version_id)
|
|
711
|
+
version = cls(client, data)
|
|
712
|
+
|
|
713
|
+
# Cache it
|
|
714
|
+
if cache:
|
|
715
|
+
cache[version_id] = version
|
|
716
|
+
|
|
717
|
+
return version
|
|
718
|
+
|
|
719
|
+
@classmethod
|
|
720
|
+
async def create(
|
|
721
|
+
cls,
|
|
722
|
+
*,
|
|
723
|
+
client: "NotionClient",
|
|
724
|
+
database_id: str,
|
|
725
|
+
name: str,
|
|
726
|
+
project_id: str,
|
|
727
|
+
status: str = "Planning",
|
|
728
|
+
version_type: str = "Minor",
|
|
729
|
+
branch_name: str | None = None,
|
|
730
|
+
progress: int = 0,
|
|
731
|
+
) -> "Version":
|
|
732
|
+
"""Create a new version."""
|
|
733
|
+
from better_notion._api.properties import Title, Select, RichText, Number, Relation
|
|
734
|
+
|
|
735
|
+
# Build properties
|
|
736
|
+
properties: dict[str, Any] = {
|
|
737
|
+
"Version": Title(name),
|
|
738
|
+
"Project": Relation([project_id]),
|
|
739
|
+
"Status": Select(status),
|
|
740
|
+
"Type": Select(version_type),
|
|
741
|
+
"Progress": Number(progress),
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if branch_name:
|
|
745
|
+
properties["Branch Name"] = RichText(branch_name)
|
|
746
|
+
|
|
747
|
+
# Create page
|
|
748
|
+
data = await client._api.pages.create(
|
|
749
|
+
parent={"database_id": database_id},
|
|
750
|
+
properties=properties,
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
version = cls(client, data)
|
|
754
|
+
|
|
755
|
+
# Cache it
|
|
756
|
+
cache = client.plugin_cache("versions")
|
|
757
|
+
if cache:
|
|
758
|
+
cache[version.id] = version
|
|
759
|
+
|
|
760
|
+
return version
|
|
761
|
+
|
|
762
|
+
async def update(
|
|
763
|
+
self,
|
|
764
|
+
*,
|
|
765
|
+
name: str | None = None,
|
|
766
|
+
status: str | None = None,
|
|
767
|
+
version_type: str | None = None,
|
|
768
|
+
branch_name: str | None = None,
|
|
769
|
+
progress: int | None = None,
|
|
770
|
+
) -> "Version":
|
|
771
|
+
"""Update version properties."""
|
|
772
|
+
from better_notion._api.properties import Title, Select, RichText, Number
|
|
773
|
+
|
|
774
|
+
# Build properties to update
|
|
775
|
+
properties: dict[str, Any] = {}
|
|
776
|
+
|
|
777
|
+
if name is not None:
|
|
778
|
+
properties["Version"] = Title(name)
|
|
779
|
+
if status is not None:
|
|
780
|
+
properties["Status"] = Select(status)
|
|
781
|
+
if version_type is not None:
|
|
782
|
+
properties["Type"] = Select(version_type)
|
|
783
|
+
if branch_name is not None:
|
|
784
|
+
properties["Branch Name"] = RichText(branch_name)
|
|
785
|
+
if progress is not None:
|
|
786
|
+
properties["Progress"] = Number(progress)
|
|
787
|
+
|
|
788
|
+
# Update page
|
|
789
|
+
data = await self._client._api.pages.update(
|
|
790
|
+
page_id=self.id,
|
|
791
|
+
properties=properties,
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
# Update instance
|
|
795
|
+
self._data = data
|
|
796
|
+
|
|
797
|
+
# Invalidate cache
|
|
798
|
+
cache = self._client.plugin_cache("versions")
|
|
799
|
+
if cache and self.id in cache:
|
|
800
|
+
del cache[self.id]
|
|
801
|
+
|
|
802
|
+
return self
|
|
803
|
+
|
|
804
|
+
async def delete(self) -> None:
|
|
805
|
+
"""Delete the version."""
|
|
806
|
+
await self._client._api.pages.delete(page_id=self.id)
|
|
807
|
+
|
|
808
|
+
# Invalidate cache
|
|
809
|
+
cache = self._client.plugin_cache("versions")
|
|
810
|
+
if cache and self.id in cache:
|
|
811
|
+
del cache[self.id]
|
|
812
|
+
|
|
813
|
+
async def project(self) -> Project | None:
|
|
814
|
+
"""Get parent project."""
|
|
815
|
+
if self.project_id:
|
|
816
|
+
return await Project.get(self.project_id, client=self._client)
|
|
817
|
+
return None
|
|
818
|
+
|
|
819
|
+
async def tasks(self) -> list["Task"]:
|
|
820
|
+
"""Get all tasks in this version."""
|
|
821
|
+
# Get database ID from workspace config
|
|
822
|
+
import json
|
|
823
|
+
from pathlib import Path
|
|
824
|
+
|
|
825
|
+
config_path = Path.home() / ".notion" / "workspace.json"
|
|
826
|
+
if not config_path.exists():
|
|
827
|
+
return []
|
|
828
|
+
|
|
829
|
+
config = json.loads(config_path.read_text())
|
|
830
|
+
tasks_db_id = config.get("Tasks")
|
|
831
|
+
|
|
832
|
+
if not tasks_db_id:
|
|
833
|
+
return []
|
|
834
|
+
|
|
835
|
+
# Query tasks with relation filter
|
|
836
|
+
response = await self._client._api.databases.query(
|
|
837
|
+
database_id=tasks_db_id,
|
|
838
|
+
filter={
|
|
839
|
+
"property": "Version",
|
|
840
|
+
"relation": {"contains": self.id},
|
|
841
|
+
},
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
return [Task(self._client, page_data) for page_data in response.get("results", [])]
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
class Task(BaseEntity):
|
|
848
|
+
"""
|
|
849
|
+
Task entity representing a work task.
|
|
850
|
+
|
|
851
|
+
A task belongs to a version and can have dependencies on other tasks.
|
|
852
|
+
|
|
853
|
+
Attributes:
|
|
854
|
+
id: Task page ID
|
|
855
|
+
title: Task title
|
|
856
|
+
version: Parent version
|
|
857
|
+
status: Task status (Backlog, Claimed, In Progress, In Review, Completed)
|
|
858
|
+
type: Task type (New Feature, Refactor, Documentation, Test, Bug Fix)
|
|
859
|
+
priority: Task priority (Critical, High, Medium, Low)
|
|
860
|
+
dependencies: List of task IDs this task depends on
|
|
861
|
+
estimated_hours: Estimated hours to complete
|
|
862
|
+
actual_hours: Actual hours spent
|
|
863
|
+
|
|
864
|
+
Example:
|
|
865
|
+
>>> task = await Task.get("task_id", client=client)
|
|
866
|
+
>>> await task.claim()
|
|
867
|
+
>>> await task.start()
|
|
868
|
+
>>> await task.complete()
|
|
869
|
+
"""
|
|
870
|
+
|
|
871
|
+
def __init__(self, client: "NotionClient", data: dict[str, Any]) -> None:
|
|
872
|
+
"""Initialize task with client and API data."""
|
|
873
|
+
super().__init__(client, data)
|
|
874
|
+
self._task_cache = client.plugin_cache("tasks")
|
|
875
|
+
|
|
876
|
+
# ===== PROPERTIES =====
|
|
877
|
+
|
|
878
|
+
@property
|
|
879
|
+
def title(self) -> str:
|
|
880
|
+
"""Get task title from title property."""
|
|
881
|
+
title_prop = self._data["properties"].get("Title") or self._data["properties"].get("title")
|
|
882
|
+
if title_prop and title_prop.get("type") == "title":
|
|
883
|
+
title_data = title_prop.get("title", [])
|
|
884
|
+
if title_data:
|
|
885
|
+
return title_data[0].get("plain_text", "")
|
|
886
|
+
return ""
|
|
887
|
+
|
|
888
|
+
@property
|
|
889
|
+
def status(self) -> str:
|
|
890
|
+
"""Get task status."""
|
|
891
|
+
status_prop = self._data["properties"].get("Status") or self._data["properties"].get("status")
|
|
892
|
+
if status_prop and status_prop.get("type") == "select":
|
|
893
|
+
select_data = status_prop.get("select")
|
|
894
|
+
if select_data:
|
|
895
|
+
return select_data.get("name", "Unknown")
|
|
896
|
+
return "Unknown"
|
|
897
|
+
|
|
898
|
+
@property
|
|
899
|
+
def task_type(self) -> str:
|
|
900
|
+
"""Get task type."""
|
|
901
|
+
type_prop = self._data["properties"].get("Type") or self._data["properties"].get("type")
|
|
902
|
+
if type_prop and type_prop.get("type") == "select":
|
|
903
|
+
select_data = type_prop.get("select")
|
|
904
|
+
if select_data:
|
|
905
|
+
return select_data.get("name", "Unknown")
|
|
906
|
+
return "Unknown"
|
|
907
|
+
|
|
908
|
+
@property
|
|
909
|
+
def priority(self) -> str:
|
|
910
|
+
"""Get task priority."""
|
|
911
|
+
priority_prop = self._data["properties"].get("Priority") or self._data["properties"].get("priority")
|
|
912
|
+
if priority_prop and priority_prop.get("type") == "select":
|
|
913
|
+
select_data = priority_prop.get("select")
|
|
914
|
+
if select_data:
|
|
915
|
+
return select_data.get("name", "Unknown")
|
|
916
|
+
return "Unknown"
|
|
917
|
+
|
|
918
|
+
@property
|
|
919
|
+
def version_id(self) -> str | None:
|
|
920
|
+
"""Get parent version ID."""
|
|
921
|
+
version_prop = self._data["properties"].get("Version") or self._data["properties"].get("version")
|
|
922
|
+
if version_prop and version_prop.get("type") == "relation":
|
|
923
|
+
relations = version_prop.get("relation", [])
|
|
924
|
+
if relations:
|
|
925
|
+
return relations[0].get("id")
|
|
926
|
+
return None
|
|
927
|
+
|
|
928
|
+
@property
|
|
929
|
+
def dependency_ids(self) -> list[str]:
|
|
930
|
+
"""Get list of task IDs this task depends on."""
|
|
931
|
+
dep_prop = self._data["properties"].get("Dependencies") or self._data["properties"].get("dependencies")
|
|
932
|
+
if dep_prop and dep_prop.get("type") == "relation":
|
|
933
|
+
relations = dep_prop.get("relation", [])
|
|
934
|
+
return [r.get("id", "") for r in relations if r.get("id")]
|
|
935
|
+
return []
|
|
936
|
+
|
|
937
|
+
@property
|
|
938
|
+
def estimated_hours(self) -> int | None:
|
|
939
|
+
"""Get estimated hours."""
|
|
940
|
+
hours_prop = self._data["properties"].get("Estimated Hours") or self._data["properties"].get("estimated_hours")
|
|
941
|
+
if hours_prop and hours_prop.get("type") == "number":
|
|
942
|
+
return hours_prop.get("number")
|
|
943
|
+
return None
|
|
944
|
+
|
|
945
|
+
@property
|
|
946
|
+
def actual_hours(self) -> int | None:
|
|
947
|
+
"""Get actual hours spent."""
|
|
948
|
+
hours_prop = self._data["properties"].get("Actual Hours") or self._data["properties"].get("actual_hours")
|
|
949
|
+
if hours_prop and hours_prop.get("type") == "number":
|
|
950
|
+
return hours_prop.get("number")
|
|
951
|
+
return None
|
|
952
|
+
|
|
953
|
+
# ===== AUTONOMOUS METHODS =====
|
|
954
|
+
|
|
955
|
+
@classmethod
|
|
956
|
+
async def get(cls, task_id: str, *, client: "NotionClient") -> "Task":
|
|
957
|
+
"""Get a task by ID."""
|
|
958
|
+
# Check plugin cache
|
|
959
|
+
cache = client.plugin_cache("tasks")
|
|
960
|
+
if cache and task_id in cache:
|
|
961
|
+
return cache[task_id]
|
|
962
|
+
|
|
963
|
+
# Fetch from API
|
|
964
|
+
data = await client._api.pages.get(page_id=task_id)
|
|
965
|
+
task = cls(client, data)
|
|
966
|
+
|
|
967
|
+
# Cache it
|
|
968
|
+
if cache:
|
|
969
|
+
cache[task_id] = task
|
|
970
|
+
|
|
971
|
+
return task
|
|
972
|
+
|
|
973
|
+
@classmethod
|
|
974
|
+
async def create(
|
|
975
|
+
cls,
|
|
976
|
+
*,
|
|
977
|
+
client: "NotionClient",
|
|
978
|
+
database_id: str,
|
|
979
|
+
title: str,
|
|
980
|
+
version_id: str,
|
|
981
|
+
status: str = "Backlog",
|
|
982
|
+
task_type: str = "New Feature",
|
|
983
|
+
priority: str = "Medium",
|
|
984
|
+
dependency_ids: list[str] | None = None,
|
|
985
|
+
estimated_hours: int | None = None,
|
|
986
|
+
) -> "Task":
|
|
987
|
+
"""Create a new task."""
|
|
988
|
+
from better_notion._api.properties import Title, Select, Number, Relation
|
|
989
|
+
|
|
990
|
+
# Build properties
|
|
991
|
+
properties: dict[str, Any] = {
|
|
992
|
+
"Title": Title(title),
|
|
993
|
+
"Version": Relation([version_id]),
|
|
994
|
+
"Status": Select(status),
|
|
995
|
+
"Type": Select(task_type),
|
|
996
|
+
"Priority": Select(priority),
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if dependency_ids:
|
|
1000
|
+
properties["Dependencies"] = Relation(dependency_ids)
|
|
1001
|
+
if estimated_hours is not None:
|
|
1002
|
+
properties["Estimated Hours"] = Number(estimated_hours)
|
|
1003
|
+
|
|
1004
|
+
# Create page
|
|
1005
|
+
data = await client._api.pages.create(
|
|
1006
|
+
parent={"database_id": database_id},
|
|
1007
|
+
properties=properties,
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
task = cls(client, data)
|
|
1011
|
+
|
|
1012
|
+
# Cache it
|
|
1013
|
+
cache = client.plugin_cache("tasks")
|
|
1014
|
+
if cache:
|
|
1015
|
+
cache[task.id] = task
|
|
1016
|
+
|
|
1017
|
+
return task
|
|
1018
|
+
|
|
1019
|
+
async def update(
|
|
1020
|
+
self,
|
|
1021
|
+
*,
|
|
1022
|
+
title: str | None = None,
|
|
1023
|
+
status: str | None = None,
|
|
1024
|
+
task_type: str | None = None,
|
|
1025
|
+
priority: str | None = None,
|
|
1026
|
+
dependency_ids: list[str] | None = None,
|
|
1027
|
+
estimated_hours: int | None = None,
|
|
1028
|
+
actual_hours: int | None = None,
|
|
1029
|
+
) -> "Task":
|
|
1030
|
+
"""Update task properties."""
|
|
1031
|
+
from better_notion._api.properties import Title, Select, Number, Relation
|
|
1032
|
+
|
|
1033
|
+
# Build properties to update
|
|
1034
|
+
properties: dict[str, Any] = {}
|
|
1035
|
+
|
|
1036
|
+
if title is not None:
|
|
1037
|
+
properties["Title"] = Title(title)
|
|
1038
|
+
if status is not None:
|
|
1039
|
+
properties["Status"] = Select(status)
|
|
1040
|
+
if task_type is not None:
|
|
1041
|
+
properties["Type"] = Select(task_type)
|
|
1042
|
+
if priority is not None:
|
|
1043
|
+
properties["Priority"] = Select(priority)
|
|
1044
|
+
if dependency_ids is not None:
|
|
1045
|
+
properties["Dependencies"] = Relation(dependency_ids)
|
|
1046
|
+
if estimated_hours is not None:
|
|
1047
|
+
properties["Estimated Hours"] = Number(estimated_hours)
|
|
1048
|
+
if actual_hours is not None:
|
|
1049
|
+
properties["Actual Hours"] = Number(actual_hours)
|
|
1050
|
+
|
|
1051
|
+
# Update page
|
|
1052
|
+
data = await self._client._api.pages.update(
|
|
1053
|
+
page_id=self.id,
|
|
1054
|
+
properties=properties,
|
|
1055
|
+
)
|
|
1056
|
+
|
|
1057
|
+
# Update instance
|
|
1058
|
+
self._data = data
|
|
1059
|
+
|
|
1060
|
+
# Invalidate cache
|
|
1061
|
+
cache = self._client.plugin_cache("tasks")
|
|
1062
|
+
if cache and self.id in cache:
|
|
1063
|
+
del cache[self.id]
|
|
1064
|
+
|
|
1065
|
+
return self
|
|
1066
|
+
|
|
1067
|
+
async def delete(self) -> None:
|
|
1068
|
+
"""Delete the task."""
|
|
1069
|
+
await self._client._api.pages.delete(page_id=self.id)
|
|
1070
|
+
|
|
1071
|
+
# Invalidate cache
|
|
1072
|
+
cache = self._client.plugin_cache("tasks")
|
|
1073
|
+
if cache and self.id in cache:
|
|
1074
|
+
del cache[self.id]
|
|
1075
|
+
|
|
1076
|
+
async def version(self) -> Version | None:
|
|
1077
|
+
"""Get parent version."""
|
|
1078
|
+
if self.version_id:
|
|
1079
|
+
return await Version.get(self.version_id, client=self._client)
|
|
1080
|
+
return None
|
|
1081
|
+
|
|
1082
|
+
async def dependencies(self) -> list["Task"]:
|
|
1083
|
+
"""Get all tasks this task depends on."""
|
|
1084
|
+
tasks = []
|
|
1085
|
+
for dep_id in self.dependency_ids:
|
|
1086
|
+
try:
|
|
1087
|
+
task = await Task.get(dep_id, client=self._client)
|
|
1088
|
+
tasks.append(task)
|
|
1089
|
+
except Exception:
|
|
1090
|
+
pass
|
|
1091
|
+
return tasks
|
|
1092
|
+
|
|
1093
|
+
# ===== WORKFLOW METHODS =====
|
|
1094
|
+
|
|
1095
|
+
async def claim(self) -> "Task":
|
|
1096
|
+
"""
|
|
1097
|
+
Claim this task (transition to Claimed status).
|
|
1098
|
+
|
|
1099
|
+
Returns:
|
|
1100
|
+
Updated Task instance
|
|
1101
|
+
|
|
1102
|
+
Example:
|
|
1103
|
+
>>> await task.claim()
|
|
1104
|
+
"""
|
|
1105
|
+
return await self.update(status="Claimed")
|
|
1106
|
+
|
|
1107
|
+
async def start(self) -> "Task":
|
|
1108
|
+
"""
|
|
1109
|
+
Start working on this task (transition to In Progress).
|
|
1110
|
+
|
|
1111
|
+
Returns:
|
|
1112
|
+
Updated Task instance
|
|
1113
|
+
|
|
1114
|
+
Example:
|
|
1115
|
+
>>> await task.start()
|
|
1116
|
+
"""
|
|
1117
|
+
return await self.update(status="In Progress")
|
|
1118
|
+
|
|
1119
|
+
async def complete(self, actual_hours: int | None = None) -> "Task":
|
|
1120
|
+
"""
|
|
1121
|
+
Complete this task (transition to Completed).
|
|
1122
|
+
|
|
1123
|
+
Args:
|
|
1124
|
+
actual_hours: Actual hours spent (optional)
|
|
1125
|
+
|
|
1126
|
+
Returns:
|
|
1127
|
+
Updated Task instance
|
|
1128
|
+
|
|
1129
|
+
Example:
|
|
1130
|
+
>>> await task.complete(actual_hours=3)
|
|
1131
|
+
"""
|
|
1132
|
+
return await self.update(status="Completed", actual_hours=actual_hours)
|
|
1133
|
+
|
|
1134
|
+
async def can_start(self) -> bool:
|
|
1135
|
+
"""
|
|
1136
|
+
Check if this task can start (all dependencies completed).
|
|
1137
|
+
|
|
1138
|
+
Returns:
|
|
1139
|
+
True if all dependencies are completed
|
|
1140
|
+
|
|
1141
|
+
Example:
|
|
1142
|
+
>>> if await task.can_start():
|
|
1143
|
+
... await task.start()
|
|
1144
|
+
"""
|
|
1145
|
+
for dep in await self.dependencies():
|
|
1146
|
+
if dep.status != "Completed":
|
|
1147
|
+
return False
|
|
1148
|
+
return True
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
class Idea(BaseEntity):
|
|
1152
|
+
"""
|
|
1153
|
+
Idea entity representing an improvement opportunity discovered during work.
|
|
1154
|
+
|
|
1155
|
+
Ideas enable continuous improvement by capturing innovations and
|
|
1156
|
+
problems discovered during development work.
|
|
1157
|
+
|
|
1158
|
+
Attributes:
|
|
1159
|
+
id: Idea page ID
|
|
1160
|
+
title: Idea title
|
|
1161
|
+
category: Idea category (Feature, Improvement, Refactor, etc.)
|
|
1162
|
+
status: Idea status (New, Evaluated, Accepted, Rejected, Deferred)
|
|
1163
|
+
description: Detailed explanation
|
|
1164
|
+
proposed_solution: How to implement
|
|
1165
|
+
benefits: Why this is valuable
|
|
1166
|
+
effort_estimate: Implementation effort (Small, Medium, Large)
|
|
1167
|
+
context: What you were doing when you thought of this
|
|
1168
|
+
project_id: Related project ID (optional)
|
|
1169
|
+
related_task_id: Task created from this idea (optional)
|
|
1170
|
+
|
|
1171
|
+
Example:
|
|
1172
|
+
>>> idea = await Idea.create(
|
|
1173
|
+
... client=client,
|
|
1174
|
+
... database_id=db_id,
|
|
1175
|
+
... title="Add caching layer",
|
|
1176
|
+
... category="Improvement",
|
|
1177
|
+
... description="Would improve performance"
|
|
1178
|
+
... )
|
|
1179
|
+
>>> await idea.accept()
|
|
1180
|
+
>>> task = await idea.create_task(version_id=ver.id)
|
|
1181
|
+
"""
|
|
1182
|
+
|
|
1183
|
+
def __init__(self, client: "NotionClient", data: dict[str, Any]) -> None:
|
|
1184
|
+
"""Initialize idea with client and API data."""
|
|
1185
|
+
super().__init__(client, data)
|
|
1186
|
+
self._idea_cache = client.plugin_cache("ideas")
|
|
1187
|
+
|
|
1188
|
+
# ===== PROPERTIES =====
|
|
1189
|
+
|
|
1190
|
+
@property
|
|
1191
|
+
def title(self) -> str:
|
|
1192
|
+
"""Get idea title."""
|
|
1193
|
+
title_prop = self._data["properties"].get("Title") or self._data["properties"].get("title")
|
|
1194
|
+
if title_prop and title_prop.get("type") == "title":
|
|
1195
|
+
title_data = title_prop.get("title", [])
|
|
1196
|
+
if title_data:
|
|
1197
|
+
return title_data[0].get("plain_text", "")
|
|
1198
|
+
return ""
|
|
1199
|
+
|
|
1200
|
+
@property
|
|
1201
|
+
def category(self) -> str:
|
|
1202
|
+
"""Get idea category."""
|
|
1203
|
+
cat_prop = self._data["properties"].get("Category") or self._data["properties"].get("category")
|
|
1204
|
+
if cat_prop and cat_prop.get("type") == "select":
|
|
1205
|
+
select_data = cat_prop.get("select")
|
|
1206
|
+
if select_data:
|
|
1207
|
+
return select_data.get("name", "Unknown")
|
|
1208
|
+
return "Unknown"
|
|
1209
|
+
|
|
1210
|
+
@property
|
|
1211
|
+
def status(self) -> str:
|
|
1212
|
+
"""Get idea status."""
|
|
1213
|
+
status_prop = self._data["properties"].get("Status") or self._data["properties"].get("status")
|
|
1214
|
+
if status_prop and status_prop.get("type") == "select":
|
|
1215
|
+
select_data = status_prop.get("select")
|
|
1216
|
+
if select_data:
|
|
1217
|
+
return select_data.get("name", "Unknown")
|
|
1218
|
+
return "Unknown"
|
|
1219
|
+
|
|
1220
|
+
@property
|
|
1221
|
+
def description(self) -> str:
|
|
1222
|
+
"""Get idea description."""
|
|
1223
|
+
desc_prop = self._data["properties"].get("Description") or self._data["properties"].get("description")
|
|
1224
|
+
if desc_prop and desc_prop.get("type") == "rich_text":
|
|
1225
|
+
text_data = desc_prop.get("rich_text", [])
|
|
1226
|
+
if text_data:
|
|
1227
|
+
return text_data[0].get("plain_text", "")
|
|
1228
|
+
return ""
|
|
1229
|
+
|
|
1230
|
+
@property
|
|
1231
|
+
def proposed_solution(self) -> str:
|
|
1232
|
+
"""Get proposed solution."""
|
|
1233
|
+
sol_prop = self._data["properties"].get("Proposed Solution") or self._data["properties"].get("proposed_solution")
|
|
1234
|
+
if sol_prop and sol_prop.get("type") == "rich_text":
|
|
1235
|
+
text_data = sol_prop.get("rich_text", [])
|
|
1236
|
+
if text_data:
|
|
1237
|
+
return text_data[0].get("plain_text", "")
|
|
1238
|
+
return ""
|
|
1239
|
+
|
|
1240
|
+
@property
|
|
1241
|
+
def benefits(self) -> str:
|
|
1242
|
+
"""Get idea benefits."""
|
|
1243
|
+
ben_prop = self._data["properties"].get("Benefits") or self._data["properties"].get("benefits")
|
|
1244
|
+
if ben_prop and ben_prop.get("type") == "rich_text":
|
|
1245
|
+
text_data = ben_prop.get("rich_text", [])
|
|
1246
|
+
if text_data:
|
|
1247
|
+
return text_data[0].get("plain_text", "")
|
|
1248
|
+
return ""
|
|
1249
|
+
|
|
1250
|
+
@property
|
|
1251
|
+
def effort_estimate(self) -> str:
|
|
1252
|
+
"""Get effort estimate."""
|
|
1253
|
+
effort_prop = self._data["properties"].get("Effort Estimate") or self._data["properties"].get("effort_estimate")
|
|
1254
|
+
if effort_prop and effort_prop.get("type") == "select":
|
|
1255
|
+
select_data = effort_prop.get("select")
|
|
1256
|
+
if select_data:
|
|
1257
|
+
return select_data.get("name", "Unknown")
|
|
1258
|
+
return "Unknown"
|
|
1259
|
+
|
|
1260
|
+
@property
|
|
1261
|
+
def context(self) -> str:
|
|
1262
|
+
"""Get idea context."""
|
|
1263
|
+
ctx_prop = self._data["properties"].get("Context") or self._data["properties"].get("context")
|
|
1264
|
+
if ctx_prop and ctx_prop.get("type") == "rich_text":
|
|
1265
|
+
text_data = ctx_prop.get("rich_text", [])
|
|
1266
|
+
if text_data:
|
|
1267
|
+
return text_data[0].get("plain_text", "")
|
|
1268
|
+
return ""
|
|
1269
|
+
|
|
1270
|
+
@property
|
|
1271
|
+
def project_id(self) -> str | None:
|
|
1272
|
+
"""Get related project ID."""
|
|
1273
|
+
proj_prop = self._data["properties"].get("Project") or self._data["properties"].get("project")
|
|
1274
|
+
if proj_prop and proj_prop.get("type") == "relation":
|
|
1275
|
+
relations = proj_prop.get("relation", [])
|
|
1276
|
+
if relations:
|
|
1277
|
+
return relations[0].get("id")
|
|
1278
|
+
return None
|
|
1279
|
+
|
|
1280
|
+
@property
|
|
1281
|
+
def related_task_id(self) -> str | None:
|
|
1282
|
+
"""Get ID of task created from this idea."""
|
|
1283
|
+
task_prop = self._data["properties"].get("Related Task") or self._data["properties"].get("related_task")
|
|
1284
|
+
if task_prop and task_prop.get("type") == "relation":
|
|
1285
|
+
relations = task_prop.get("relation", [])
|
|
1286
|
+
if relations:
|
|
1287
|
+
return relations[0].get("id")
|
|
1288
|
+
return None
|
|
1289
|
+
|
|
1290
|
+
# ===== AUTONOMOUS METHODS =====
|
|
1291
|
+
|
|
1292
|
+
@classmethod
|
|
1293
|
+
async def get(cls, idea_id: str, *, client: "NotionClient") -> "Idea":
|
|
1294
|
+
"""Get an idea by ID."""
|
|
1295
|
+
cache = client.plugin_cache("ideas")
|
|
1296
|
+
if cache and idea_id in cache:
|
|
1297
|
+
return cache[idea_id]
|
|
1298
|
+
|
|
1299
|
+
data = await client._api.pages.get(page_id=idea_id)
|
|
1300
|
+
idea = cls(client, data)
|
|
1301
|
+
|
|
1302
|
+
if cache:
|
|
1303
|
+
cache[idea_id] = idea
|
|
1304
|
+
|
|
1305
|
+
return idea
|
|
1306
|
+
|
|
1307
|
+
@classmethod
|
|
1308
|
+
async def create(
|
|
1309
|
+
cls,
|
|
1310
|
+
*,
|
|
1311
|
+
client: "NotionClient",
|
|
1312
|
+
database_id: str,
|
|
1313
|
+
title: str,
|
|
1314
|
+
category: str,
|
|
1315
|
+
status: str = "New",
|
|
1316
|
+
description: str | None = None,
|
|
1317
|
+
proposed_solution: str | None = None,
|
|
1318
|
+
benefits: str | None = None,
|
|
1319
|
+
effort_estimate: str = "Medium",
|
|
1320
|
+
context: str | None = None,
|
|
1321
|
+
project_id: str | None = None,
|
|
1322
|
+
) -> "Idea":
|
|
1323
|
+
"""Create a new idea."""
|
|
1324
|
+
from better_notion._api.properties import Title, RichText, Select, Relation
|
|
1325
|
+
|
|
1326
|
+
properties: dict[str, Any] = {
|
|
1327
|
+
"Title": Title(title),
|
|
1328
|
+
"Category": Select(category),
|
|
1329
|
+
"Status": Select(status),
|
|
1330
|
+
"Effort Estimate": Select(effort_estimate),
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
if description:
|
|
1334
|
+
properties["Description"] = RichText(description)
|
|
1335
|
+
if proposed_solution:
|
|
1336
|
+
properties["Proposed Solution"] = RichText(proposed_solution)
|
|
1337
|
+
if benefits:
|
|
1338
|
+
properties["Benefits"] = RichText(benefits)
|
|
1339
|
+
if context:
|
|
1340
|
+
properties["Context"] = RichText(context)
|
|
1341
|
+
if project_id:
|
|
1342
|
+
properties["Project"] = Relation([project_id])
|
|
1343
|
+
|
|
1344
|
+
data = await client._api.pages.create(
|
|
1345
|
+
parent={"database_id": database_id},
|
|
1346
|
+
properties=properties,
|
|
1347
|
+
)
|
|
1348
|
+
|
|
1349
|
+
idea = cls(client, data)
|
|
1350
|
+
|
|
1351
|
+
cache = client.plugin_cache("ideas")
|
|
1352
|
+
if cache:
|
|
1353
|
+
cache[idea.id] = idea
|
|
1354
|
+
|
|
1355
|
+
return idea
|
|
1356
|
+
|
|
1357
|
+
async def update(
|
|
1358
|
+
self,
|
|
1359
|
+
*,
|
|
1360
|
+
title: str | None = None,
|
|
1361
|
+
category: str | None = None,
|
|
1362
|
+
status: str | None = None,
|
|
1363
|
+
description: str | None = None,
|
|
1364
|
+
proposed_solution: str | None = None,
|
|
1365
|
+
benefits: str | None = None,
|
|
1366
|
+
effort_estimate: str | None = None,
|
|
1367
|
+
context: str | None = None,
|
|
1368
|
+
) -> "Idea":
|
|
1369
|
+
"""Update idea properties."""
|
|
1370
|
+
from better_notion._api.properties import Title, RichText, Select
|
|
1371
|
+
|
|
1372
|
+
properties: dict[str, Any] = {}
|
|
1373
|
+
|
|
1374
|
+
if title is not None:
|
|
1375
|
+
properties["Title"] = Title(title)
|
|
1376
|
+
if category is not None:
|
|
1377
|
+
properties["Category"] = Select(category)
|
|
1378
|
+
if status is not None:
|
|
1379
|
+
properties["Status"] = Select(status)
|
|
1380
|
+
if description is not None:
|
|
1381
|
+
properties["Description"] = RichText(description)
|
|
1382
|
+
if proposed_solution is not None:
|
|
1383
|
+
properties["Proposed Solution"] = RichText(proposed_solution)
|
|
1384
|
+
if benefits is not None:
|
|
1385
|
+
properties["Benefits"] = RichText(benefits)
|
|
1386
|
+
if effort_estimate is not None:
|
|
1387
|
+
properties["Effort Estimate"] = Select(effort_estimate)
|
|
1388
|
+
if context is not None:
|
|
1389
|
+
properties["Context"] = RichText(context)
|
|
1390
|
+
|
|
1391
|
+
data = await self._client._api.pages.update(
|
|
1392
|
+
page_id=self.id,
|
|
1393
|
+
properties=properties,
|
|
1394
|
+
)
|
|
1395
|
+
|
|
1396
|
+
self._data = data
|
|
1397
|
+
|
|
1398
|
+
cache = self._client.plugin_cache("ideas")
|
|
1399
|
+
if cache and self.id in cache:
|
|
1400
|
+
del cache[self.id]
|
|
1401
|
+
|
|
1402
|
+
return self
|
|
1403
|
+
|
|
1404
|
+
async def delete(self) -> None:
|
|
1405
|
+
"""Delete the idea."""
|
|
1406
|
+
await self._client._api.pages.delete(page_id=self.id)
|
|
1407
|
+
|
|
1408
|
+
cache = self._client.plugin_cache("ideas")
|
|
1409
|
+
if cache and self.id in cache:
|
|
1410
|
+
del cache[self.id]
|
|
1411
|
+
|
|
1412
|
+
# ===== WORKFLOW METHODS =====
|
|
1413
|
+
|
|
1414
|
+
async def accept(self) -> "Idea":
|
|
1415
|
+
"""
|
|
1416
|
+
Accept this idea (transition to Accepted status).
|
|
1417
|
+
|
|
1418
|
+
Returns:
|
|
1419
|
+
Updated Idea instance
|
|
1420
|
+
|
|
1421
|
+
Example:
|
|
1422
|
+
>>> await idea.accept()
|
|
1423
|
+
"""
|
|
1424
|
+
return await self.update(status="Accepted")
|
|
1425
|
+
|
|
1426
|
+
async def reject(self, reason: str) -> "Idea":
|
|
1427
|
+
"""
|
|
1428
|
+
Reject this idea (transition to Rejected status).
|
|
1429
|
+
|
|
1430
|
+
Args:
|
|
1431
|
+
reason: Rejection reason stored in context
|
|
1432
|
+
|
|
1433
|
+
Returns:
|
|
1434
|
+
Updated Idea instance
|
|
1435
|
+
|
|
1436
|
+
Example:
|
|
1437
|
+
>>> await idea.reject("Not technically feasible")
|
|
1438
|
+
"""
|
|
1439
|
+
return await self.update(
|
|
1440
|
+
status="Rejected",
|
|
1441
|
+
context=f"Rejected: {reason}",
|
|
1442
|
+
)
|
|
1443
|
+
|
|
1444
|
+
async def defer(self) -> "Idea":
|
|
1445
|
+
"""
|
|
1446
|
+
Defer this idea (transition to Deferred status).
|
|
1447
|
+
|
|
1448
|
+
Returns:
|
|
1449
|
+
Updated Idea instance
|
|
1450
|
+
|
|
1451
|
+
Example:
|
|
1452
|
+
>>> await idea.defer()
|
|
1453
|
+
"""
|
|
1454
|
+
return await self.update(status="Deferred")
|
|
1455
|
+
|
|
1456
|
+
async def create_task(
|
|
1457
|
+
self,
|
|
1458
|
+
*,
|
|
1459
|
+
version_id: str,
|
|
1460
|
+
title: str | None = None,
|
|
1461
|
+
task_type: str = "New Feature",
|
|
1462
|
+
priority: str = "Medium",
|
|
1463
|
+
) -> Task:
|
|
1464
|
+
"""
|
|
1465
|
+
Create a task from this idea.
|
|
1466
|
+
|
|
1467
|
+
Args:
|
|
1468
|
+
version_id: Version to create task in
|
|
1469
|
+
title: Task title (defaults to idea title)
|
|
1470
|
+
task_type: Type of task
|
|
1471
|
+
priority: Task priority
|
|
1472
|
+
|
|
1473
|
+
Returns:
|
|
1474
|
+
Created Task instance
|
|
1475
|
+
|
|
1476
|
+
Example:
|
|
1477
|
+
>>> task = await idea.create_task(version_id=ver.id, priority="High")
|
|
1478
|
+
"""
|
|
1479
|
+
from better_notion.plugins.official.agents_sdk.models import Task
|
|
1480
|
+
|
|
1481
|
+
# Get database ID
|
|
1482
|
+
import json
|
|
1483
|
+
from pathlib import Path
|
|
1484
|
+
|
|
1485
|
+
config_path = Path.home() / ".notion" / "workspace.json"
|
|
1486
|
+
config = json.loads(config_path.read_text())
|
|
1487
|
+
tasks_db_id = config.get("Tasks")
|
|
1488
|
+
|
|
1489
|
+
if not tasks_db_id:
|
|
1490
|
+
raise ValueError("Tasks database not found in workspace config")
|
|
1491
|
+
|
|
1492
|
+
task = await Task.create(
|
|
1493
|
+
client=self._client,
|
|
1494
|
+
database_id=tasks_db_id,
|
|
1495
|
+
title=title or self.title,
|
|
1496
|
+
version_id=version_id,
|
|
1497
|
+
task_type=task_type,
|
|
1498
|
+
priority=priority,
|
|
1499
|
+
)
|
|
1500
|
+
|
|
1501
|
+
# Link idea to task
|
|
1502
|
+
await self.link_to_task(task.id)
|
|
1503
|
+
|
|
1504
|
+
return task
|
|
1505
|
+
|
|
1506
|
+
async def link_to_task(self, task_id: str) -> None:
|
|
1507
|
+
"""
|
|
1508
|
+
Link this idea to a task.
|
|
1509
|
+
|
|
1510
|
+
Args:
|
|
1511
|
+
task_id: Task ID to link to
|
|
1512
|
+
|
|
1513
|
+
Example:
|
|
1514
|
+
>>> await idea.link_to_task(task.id)
|
|
1515
|
+
"""
|
|
1516
|
+
from better_notion._api.properties import Relation
|
|
1517
|
+
|
|
1518
|
+
await self._client._api.pages.update(
|
|
1519
|
+
page_id=self.id,
|
|
1520
|
+
properties={
|
|
1521
|
+
"Related Task": Relation([task_id]),
|
|
1522
|
+
},
|
|
1523
|
+
)
|
|
1524
|
+
|
|
1525
|
+
|
|
1526
|
+
class WorkIssue(BaseEntity):
|
|
1527
|
+
"""
|
|
1528
|
+
Work Issue entity representing a development-time problem.
|
|
1529
|
+
|
|
1530
|
+
Work Issues track blockers, confusions, documentation gaps,
|
|
1531
|
+
tooling limitations, and other problems encountered during development.
|
|
1532
|
+
|
|
1533
|
+
Attributes:
|
|
1534
|
+
id: Work Issue page ID
|
|
1535
|
+
title: Issue title
|
|
1536
|
+
project_id: Affected project ID
|
|
1537
|
+
task_id: Task where issue occurred (optional)
|
|
1538
|
+
type: Issue type (Blocker, Confusion, Documentation, Tooling, etc.)
|
|
1539
|
+
severity: Issue severity (Critical, High, Medium, Low)
|
|
1540
|
+
status: Issue status (Open, Investigating, Resolved, Won't Fix, Deferred)
|
|
1541
|
+
description: What happened
|
|
1542
|
+
context: Environment details
|
|
1543
|
+
proposed_solution: How to fix
|
|
1544
|
+
related_idea_id: Idea this inspired (optional)
|
|
1545
|
+
|
|
1546
|
+
Example:
|
|
1547
|
+
>>> issue = await WorkIssue.create(
|
|
1548
|
+
... client=client,
|
|
1549
|
+
... database_id=db_id,
|
|
1550
|
+
... title="API documentation unclear",
|
|
1551
|
+
... project_id=proj.id,
|
|
1552
|
+
... type="Documentation",
|
|
1553
|
+
... severity="Medium"
|
|
1554
|
+
... )
|
|
1555
|
+
>>> await issue.resolve("Updated docs with examples")
|
|
1556
|
+
>>> idea = await issue.create_idea_from_solution()
|
|
1557
|
+
"""
|
|
1558
|
+
|
|
1559
|
+
def __init__(self, client: "NotionClient", data: dict[str, Any]) -> None:
|
|
1560
|
+
"""Initialize work issue with client and API data."""
|
|
1561
|
+
super().__init__(client, data)
|
|
1562
|
+
self._work_issue_cache = client.plugin_cache("work_issues")
|
|
1563
|
+
|
|
1564
|
+
# ===== PROPERTIES =====
|
|
1565
|
+
|
|
1566
|
+
@property
|
|
1567
|
+
def title(self) -> str:
|
|
1568
|
+
"""Get issue title."""
|
|
1569
|
+
title_prop = self._data["properties"].get("Title") or self._data["properties"].get("title")
|
|
1570
|
+
if title_prop and title_prop.get("type") == "title":
|
|
1571
|
+
title_data = title_prop.get("title", [])
|
|
1572
|
+
if title_data:
|
|
1573
|
+
return title_data[0].get("plain_text", "")
|
|
1574
|
+
return ""
|
|
1575
|
+
|
|
1576
|
+
@property
|
|
1577
|
+
def project_id(self) -> str | None:
|
|
1578
|
+
"""Get affected project ID."""
|
|
1579
|
+
proj_prop = self._data["properties"].get("Project") or self._data["properties"].get("project")
|
|
1580
|
+
if proj_prop and proj_prop.get("type") == "relation":
|
|
1581
|
+
relations = proj_prop.get("relation", [])
|
|
1582
|
+
if relations:
|
|
1583
|
+
return relations[0].get("id")
|
|
1584
|
+
return None
|
|
1585
|
+
|
|
1586
|
+
@property
|
|
1587
|
+
def task_id(self) -> str | None:
|
|
1588
|
+
"""Get related task ID."""
|
|
1589
|
+
task_prop = self._data["properties"].get("Task") or self._data["properties"].get("task")
|
|
1590
|
+
if task_prop and task_prop.get("type") == "relation":
|
|
1591
|
+
relations = task_prop.get("relation", [])
|
|
1592
|
+
if relations:
|
|
1593
|
+
return relations[0].get("id")
|
|
1594
|
+
return None
|
|
1595
|
+
|
|
1596
|
+
@property
|
|
1597
|
+
def type(self) -> str:
|
|
1598
|
+
"""Get issue type."""
|
|
1599
|
+
type_prop = self._data["properties"].get("Type") or self._data["properties"].get("type")
|
|
1600
|
+
if type_prop and type_prop.get("type") == "select":
|
|
1601
|
+
select_data = type_prop.get("select")
|
|
1602
|
+
if select_data:
|
|
1603
|
+
return select_data.get("name", "Unknown")
|
|
1604
|
+
return "Unknown"
|
|
1605
|
+
|
|
1606
|
+
@property
|
|
1607
|
+
def severity(self) -> str:
|
|
1608
|
+
"""Get issue severity."""
|
|
1609
|
+
sev_prop = self._data["properties"].get("Severity") or self._data["properties"].get("severity")
|
|
1610
|
+
if sev_prop and sev_prop.get("type") == "select":
|
|
1611
|
+
select_data = sev_prop.get("select")
|
|
1612
|
+
if select_data:
|
|
1613
|
+
return select_data.get("name", "Unknown")
|
|
1614
|
+
return "Unknown"
|
|
1615
|
+
|
|
1616
|
+
@property
|
|
1617
|
+
def status(self) -> str:
|
|
1618
|
+
"""Get issue status."""
|
|
1619
|
+
status_prop = self._data["properties"].get("Status") or self._data["properties"].get("status")
|
|
1620
|
+
if status_prop and status_prop.get("type") == "select":
|
|
1621
|
+
select_data = status_prop.get("select")
|
|
1622
|
+
if select_data:
|
|
1623
|
+
return select_data.get("name", "Unknown")
|
|
1624
|
+
return "Unknown"
|
|
1625
|
+
|
|
1626
|
+
@property
|
|
1627
|
+
def description(self) -> str:
|
|
1628
|
+
"""Get issue description."""
|
|
1629
|
+
desc_prop = self._data["properties"].get("Description") or self._data["properties"].get("description")
|
|
1630
|
+
if desc_prop and desc_prop.get("type") == "rich_text":
|
|
1631
|
+
text_data = desc_prop.get("rich_text", [])
|
|
1632
|
+
if text_data:
|
|
1633
|
+
return text_data[0].get("plain_text", "")
|
|
1634
|
+
return ""
|
|
1635
|
+
|
|
1636
|
+
@property
|
|
1637
|
+
def context(self) -> str:
|
|
1638
|
+
"""Get issue context."""
|
|
1639
|
+
ctx_prop = self._data["properties"].get("Context") or self._data["properties"].get("context")
|
|
1640
|
+
if ctx_prop and ctx_prop.get("type") == "rich_text":
|
|
1641
|
+
text_data = ctx_prop.get("rich_text", [])
|
|
1642
|
+
if text_data:
|
|
1643
|
+
return text_data[0].get("plain_text", "")
|
|
1644
|
+
return ""
|
|
1645
|
+
|
|
1646
|
+
@property
|
|
1647
|
+
def proposed_solution(self) -> str:
|
|
1648
|
+
"""Get proposed solution."""
|
|
1649
|
+
sol_prop = self._data["properties"].get("Proposed Solution") or self._data["properties"].get("proposed_solution")
|
|
1650
|
+
if sol_prop and sol_prop.get("type") == "rich_text":
|
|
1651
|
+
text_data = sol_prop.get("rich_text", [])
|
|
1652
|
+
if text_data:
|
|
1653
|
+
return text_data[0].get("plain_text", "")
|
|
1654
|
+
return ""
|
|
1655
|
+
|
|
1656
|
+
@property
|
|
1657
|
+
def related_idea_id(self) -> str | None:
|
|
1658
|
+
"""Get related idea ID."""
|
|
1659
|
+
idea_prop = self._data["properties"].get("Related Idea") or self._data["properties"].get("related_idea")
|
|
1660
|
+
if idea_prop and idea_prop.get("type") == "relation":
|
|
1661
|
+
relations = idea_prop.get("relation", [])
|
|
1662
|
+
if relations:
|
|
1663
|
+
return relations[0].get("id")
|
|
1664
|
+
return None
|
|
1665
|
+
|
|
1666
|
+
# ===== AUTONOMOUS METHODS =====
|
|
1667
|
+
|
|
1668
|
+
@classmethod
|
|
1669
|
+
async def get(cls, issue_id: str, *, client: "NotionClient") -> "WorkIssue":
|
|
1670
|
+
"""Get a work issue by ID."""
|
|
1671
|
+
cache = client.plugin_cache("work_issues")
|
|
1672
|
+
if cache and issue_id in cache:
|
|
1673
|
+
return cache[issue_id]
|
|
1674
|
+
|
|
1675
|
+
data = await client._api.pages.get(page_id=issue_id)
|
|
1676
|
+
issue = cls(client, data)
|
|
1677
|
+
|
|
1678
|
+
if cache:
|
|
1679
|
+
cache[issue_id] = issue
|
|
1680
|
+
|
|
1681
|
+
return issue
|
|
1682
|
+
|
|
1683
|
+
@classmethod
|
|
1684
|
+
async def create(
|
|
1685
|
+
cls,
|
|
1686
|
+
*,
|
|
1687
|
+
client: "NotionClient",
|
|
1688
|
+
database_id: str,
|
|
1689
|
+
title: str,
|
|
1690
|
+
project_id: str,
|
|
1691
|
+
task_id: str | None = None,
|
|
1692
|
+
type: str = "Blocker",
|
|
1693
|
+
severity: str = "Medium",
|
|
1694
|
+
status: str = "Open",
|
|
1695
|
+
description: str | None = None,
|
|
1696
|
+
context: str | None = None,
|
|
1697
|
+
proposed_solution: str | None = None,
|
|
1698
|
+
) -> "WorkIssue":
|
|
1699
|
+
"""Create a new work issue."""
|
|
1700
|
+
from better_notion._api.properties import Title, RichText, Select, Relation
|
|
1701
|
+
|
|
1702
|
+
properties: dict[str, Any] = {
|
|
1703
|
+
"Title": Title(title),
|
|
1704
|
+
"Project": Relation([project_id]),
|
|
1705
|
+
"Type": Select(type),
|
|
1706
|
+
"Severity": Select(severity),
|
|
1707
|
+
"Status": Select(status),
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
if task_id:
|
|
1711
|
+
properties["Task"] = Relation([task_id])
|
|
1712
|
+
if description:
|
|
1713
|
+
properties["Description"] = RichText(description)
|
|
1714
|
+
if context:
|
|
1715
|
+
properties["Context"] = RichText(context)
|
|
1716
|
+
if proposed_solution:
|
|
1717
|
+
properties["Proposed Solution"] = RichText(proposed_solution)
|
|
1718
|
+
|
|
1719
|
+
data = await client._api.pages.create(
|
|
1720
|
+
parent={"database_id": database_id},
|
|
1721
|
+
properties=properties,
|
|
1722
|
+
)
|
|
1723
|
+
|
|
1724
|
+
issue = cls(client, data)
|
|
1725
|
+
|
|
1726
|
+
cache = client.plugin_cache("work_issues")
|
|
1727
|
+
if cache:
|
|
1728
|
+
cache[issue.id] = issue
|
|
1729
|
+
|
|
1730
|
+
return issue
|
|
1731
|
+
|
|
1732
|
+
async def update(
|
|
1733
|
+
self,
|
|
1734
|
+
*,
|
|
1735
|
+
title: str | None = None,
|
|
1736
|
+
type: str | None = None,
|
|
1737
|
+
severity: str | None = None,
|
|
1738
|
+
status: str | None = None,
|
|
1739
|
+
description: str | None = None,
|
|
1740
|
+
context: str | None = None,
|
|
1741
|
+
proposed_solution: str | None = None,
|
|
1742
|
+
) -> "WorkIssue":
|
|
1743
|
+
"""Update work issue properties."""
|
|
1744
|
+
from better_notion._api.properties import Title, RichText, Select
|
|
1745
|
+
|
|
1746
|
+
properties: dict[str, Any] = {}
|
|
1747
|
+
|
|
1748
|
+
if title is not None:
|
|
1749
|
+
properties["Title"] = Title(title)
|
|
1750
|
+
if type is not None:
|
|
1751
|
+
properties["Type"] = Select(type)
|
|
1752
|
+
if severity is not None:
|
|
1753
|
+
properties["Severity"] = Select(severity)
|
|
1754
|
+
if status is not None:
|
|
1755
|
+
properties["Status"] = Select(status)
|
|
1756
|
+
if description is not None:
|
|
1757
|
+
properties["Description"] = RichText(description)
|
|
1758
|
+
if context is not None:
|
|
1759
|
+
properties["Context"] = RichText(context)
|
|
1760
|
+
if proposed_solution is not None:
|
|
1761
|
+
properties["Proposed Solution"] = RichText(proposed_solution)
|
|
1762
|
+
|
|
1763
|
+
data = await self._client._api.pages.update(
|
|
1764
|
+
page_id=self.id,
|
|
1765
|
+
properties=properties,
|
|
1766
|
+
)
|
|
1767
|
+
|
|
1768
|
+
self._data = data
|
|
1769
|
+
|
|
1770
|
+
cache = self._client.plugin_cache("work_issues")
|
|
1771
|
+
if cache and self.id in cache:
|
|
1772
|
+
del cache[self.id]
|
|
1773
|
+
|
|
1774
|
+
return self
|
|
1775
|
+
|
|
1776
|
+
async def delete(self) -> None:
|
|
1777
|
+
"""Delete the work issue."""
|
|
1778
|
+
await self._client._api.pages.delete(page_id=self.id)
|
|
1779
|
+
|
|
1780
|
+
cache = self._client.plugin_cache("work_issues")
|
|
1781
|
+
if cache and self.id in cache:
|
|
1782
|
+
del cache[self.id]
|
|
1783
|
+
|
|
1784
|
+
# ===== WORKFLOW METHODS =====
|
|
1785
|
+
|
|
1786
|
+
async def resolve(self, solution: str) -> "WorkIssue":
|
|
1787
|
+
"""
|
|
1788
|
+
Resolve this issue.
|
|
1789
|
+
|
|
1790
|
+
Args:
|
|
1791
|
+
solution: How the issue was resolved
|
|
1792
|
+
|
|
1793
|
+
Returns:
|
|
1794
|
+
Updated WorkIssue instance
|
|
1795
|
+
|
|
1796
|
+
Example:
|
|
1797
|
+
>>> await issue.resolve("Updated documentation")
|
|
1798
|
+
"""
|
|
1799
|
+
return await self.update(
|
|
1800
|
+
status="Resolved",
|
|
1801
|
+
proposed_solution=solution,
|
|
1802
|
+
)
|
|
1803
|
+
|
|
1804
|
+
async def investigate(self) -> "WorkIssue":
|
|
1805
|
+
"""
|
|
1806
|
+
Mark issue as under investigation.
|
|
1807
|
+
|
|
1808
|
+
Returns:
|
|
1809
|
+
Updated WorkIssue instance
|
|
1810
|
+
|
|
1811
|
+
Example:
|
|
1812
|
+
>>> await issue.investigate()
|
|
1813
|
+
"""
|
|
1814
|
+
return await self.update(status="Investigating")
|
|
1815
|
+
|
|
1816
|
+
async def link_to_idea(self, idea_id: str) -> None:
|
|
1817
|
+
"""
|
|
1818
|
+
Link this issue to an idea.
|
|
1819
|
+
|
|
1820
|
+
Args:
|
|
1821
|
+
idea_id: Idea ID to link to
|
|
1822
|
+
|
|
1823
|
+
Example:
|
|
1824
|
+
>>> await issue.link_to_idea(idea.id)
|
|
1825
|
+
"""
|
|
1826
|
+
from better_notion._api.properties import Relation
|
|
1827
|
+
|
|
1828
|
+
await self._client._api.pages.update(
|
|
1829
|
+
page_id=self.id,
|
|
1830
|
+
properties={
|
|
1831
|
+
"Related Idea": Relation([idea_id]),
|
|
1832
|
+
},
|
|
1833
|
+
)
|
|
1834
|
+
|
|
1835
|
+
async def create_idea_from_solution(self) -> Idea:
|
|
1836
|
+
"""
|
|
1837
|
+
Create an idea from this issue's solution.
|
|
1838
|
+
|
|
1839
|
+
Returns:
|
|
1840
|
+
Created Idea instance
|
|
1841
|
+
|
|
1842
|
+
Example:
|
|
1843
|
+
>>> idea = await issue.create_idea_from_solution()
|
|
1844
|
+
"""
|
|
1845
|
+
from better_notion.plugins.official.agents_sdk.models import Idea
|
|
1846
|
+
|
|
1847
|
+
import json
|
|
1848
|
+
from pathlib import Path
|
|
1849
|
+
|
|
1850
|
+
config_path = Path.home() / ".notion" / "workspace.json"
|
|
1851
|
+
config = json.loads(config_path.read_text())
|
|
1852
|
+
ideas_db_id = config.get("Ideas")
|
|
1853
|
+
|
|
1854
|
+
if not ideas_db_id:
|
|
1855
|
+
raise ValueError("Ideas database not found in workspace config")
|
|
1856
|
+
|
|
1857
|
+
idea = await Idea.create(
|
|
1858
|
+
client=self._client,
|
|
1859
|
+
database_id=ideas_db_id,
|
|
1860
|
+
title=f"Prevent issue: {self.title}",
|
|
1861
|
+
category="Process",
|
|
1862
|
+
status="New",
|
|
1863
|
+
description=f"Issue {self.id} revealed process gap",
|
|
1864
|
+
proposed_solution=self.proposed_solution or "Implement prevention",
|
|
1865
|
+
context=self.context,
|
|
1866
|
+
project_id=self.project_id,
|
|
1867
|
+
)
|
|
1868
|
+
|
|
1869
|
+
# Link issue to idea
|
|
1870
|
+
await self.link_to_idea(idea.id)
|
|
1871
|
+
|
|
1872
|
+
return idea
|
|
1873
|
+
|
|
1874
|
+
|
|
1875
|
+
class Incident(BaseEntity):
|
|
1876
|
+
"""
|
|
1877
|
+
Incident entity representing a production incident.
|
|
1878
|
+
|
|
1879
|
+
Incidents track production problems separate from development
|
|
1880
|
+
work issues. Critical for production reliability and MTTR tracking.
|
|
1881
|
+
|
|
1882
|
+
Attributes:
|
|
1883
|
+
id: Incident page ID
|
|
1884
|
+
title: Incident title
|
|
1885
|
+
project_id: Affected project ID
|
|
1886
|
+
affected_version_id: Version where incident occurred
|
|
1887
|
+
severity: Incident severity (Critical, High, Medium, Low)
|
|
1888
|
+
type: Incident type (Bug, Crash, Performance, Security, etc.)
|
|
1889
|
+
status: Incident status (Open, Investigating, Fix in Progress, Resolved)
|
|
1890
|
+
fix_task_id: Task to fix this incident (optional)
|
|
1891
|
+
root_cause: Analysis of what went wrong
|
|
1892
|
+
discovery_date: When incident was discovered
|
|
1893
|
+
resolved_date: When incident was resolved (optional)
|
|
1894
|
+
|
|
1895
|
+
Example:
|
|
1896
|
+
>>> incident = await Incident.create(
|
|
1897
|
+
... client=client,
|
|
1898
|
+
... database_id=db_id,
|
|
1899
|
+
... title="Production database down",
|
|
1900
|
+
... project_id=proj.id,
|
|
1901
|
+
... affected_version_id=ver.id,
|
|
1902
|
+
... severity="Critical",
|
|
1903
|
+
... type="Outage"
|
|
1904
|
+
... )
|
|
1905
|
+
>>> task = await incident.create_fix_task(version_id=hotfix.id)
|
|
1906
|
+
>>> await incident.resolve("Config error in connection pool")
|
|
1907
|
+
>>> mttr = incident.calculate_mttr()
|
|
1908
|
+
"""
|
|
1909
|
+
|
|
1910
|
+
def __init__(self, client: "NotionClient", data: dict[str, Any]) -> None:
|
|
1911
|
+
"""Initialize incident with client and API data."""
|
|
1912
|
+
super().__init__(client, data)
|
|
1913
|
+
self._incident_cache = client.plugin_cache("incidents")
|
|
1914
|
+
|
|
1915
|
+
# ===== PROPERTIES =====
|
|
1916
|
+
|
|
1917
|
+
@property
|
|
1918
|
+
def title(self) -> str:
|
|
1919
|
+
"""Get incident title."""
|
|
1920
|
+
title_prop = self._data["properties"].get("Title") or self._data["properties"].get("title")
|
|
1921
|
+
if title_prop and title_prop.get("type") == "title":
|
|
1922
|
+
title_data = title_prop.get("title", [])
|
|
1923
|
+
if title_data:
|
|
1924
|
+
return title_data[0].get("plain_text", "")
|
|
1925
|
+
return ""
|
|
1926
|
+
|
|
1927
|
+
@property
|
|
1928
|
+
def project_id(self) -> str | None:
|
|
1929
|
+
"""Get affected project ID."""
|
|
1930
|
+
proj_prop = self._data["properties"].get("Project") or self._data["properties"].get("project")
|
|
1931
|
+
if proj_prop and proj_prop.get("type") == "relation":
|
|
1932
|
+
relations = proj_prop.get("relation", [])
|
|
1933
|
+
if relations:
|
|
1934
|
+
return relations[0].get("id")
|
|
1935
|
+
return None
|
|
1936
|
+
|
|
1937
|
+
@property
|
|
1938
|
+
def affected_version_id(self) -> str | None:
|
|
1939
|
+
"""Get affected version ID."""
|
|
1940
|
+
ver_prop = self._data["properties"].get("Affected Version") or self._data["properties"].get("affected_version")
|
|
1941
|
+
if ver_prop and ver_prop.get("type") == "relation":
|
|
1942
|
+
relations = ver_prop.get("relation", [])
|
|
1943
|
+
if relations:
|
|
1944
|
+
return relations[0].get("id")
|
|
1945
|
+
return None
|
|
1946
|
+
|
|
1947
|
+
@property
|
|
1948
|
+
def severity(self) -> str:
|
|
1949
|
+
"""Get incident severity."""
|
|
1950
|
+
sev_prop = self._data["properties"].get("Severity") or self._data["properties"].get("severity")
|
|
1951
|
+
if sev_prop and sev_prop.get("type") == "select":
|
|
1952
|
+
select_data = sev_prop.get("select")
|
|
1953
|
+
if select_data:
|
|
1954
|
+
return select_data.get("name", "Unknown")
|
|
1955
|
+
return "Unknown"
|
|
1956
|
+
|
|
1957
|
+
@property
|
|
1958
|
+
def type(self) -> str:
|
|
1959
|
+
"""Get incident type."""
|
|
1960
|
+
type_prop = self._data["properties"].get("Type") or self._data["properties"].get("type")
|
|
1961
|
+
if type_prop and type_prop.get("type") == "select":
|
|
1962
|
+
select_data = type_prop.get("select")
|
|
1963
|
+
if select_data:
|
|
1964
|
+
return select_data.get("name", "Unknown")
|
|
1965
|
+
return "Unknown"
|
|
1966
|
+
|
|
1967
|
+
@property
|
|
1968
|
+
def status(self) -> str:
|
|
1969
|
+
"""Get incident status."""
|
|
1970
|
+
status_prop = self._data["properties"].get("Status") or self._data["properties"].get("status")
|
|
1971
|
+
if status_prop and status_prop.get("type") == "select":
|
|
1972
|
+
select_data = status_prop.get("select")
|
|
1973
|
+
if select_data:
|
|
1974
|
+
return select_data.get("name", "Unknown")
|
|
1975
|
+
return "Unknown"
|
|
1976
|
+
|
|
1977
|
+
@property
|
|
1978
|
+
def fix_task_id(self) -> str | None:
|
|
1979
|
+
"""Get fix task ID."""
|
|
1980
|
+
task_prop = self._data["properties"].get("Fix Task") or self._data["properties"].get("fix_task")
|
|
1981
|
+
if task_prop and task_prop.get("type") == "relation":
|
|
1982
|
+
relations = task_prop.get("relation", [])
|
|
1983
|
+
if relations:
|
|
1984
|
+
return relations[0].get("id")
|
|
1985
|
+
return None
|
|
1986
|
+
|
|
1987
|
+
@property
|
|
1988
|
+
def root_cause(self) -> str:
|
|
1989
|
+
"""Get root cause analysis."""
|
|
1990
|
+
rc_prop = self._data["properties"].get("Root Cause") or self._data["properties"].get("root_cause")
|
|
1991
|
+
if rc_prop and rc_prop.get("type") == "rich_text":
|
|
1992
|
+
text_data = rc_prop.get("rich_text", [])
|
|
1993
|
+
if text_data:
|
|
1994
|
+
return text_data[0].get("plain_text", "")
|
|
1995
|
+
return ""
|
|
1996
|
+
|
|
1997
|
+
@property
|
|
1998
|
+
def discovery_date(self) -> str | None:
|
|
1999
|
+
"""Get discovery date."""
|
|
2000
|
+
date_prop = self._data["properties"].get("Discovery Date") or self._data["properties"].get("discovery_date")
|
|
2001
|
+
if date_prop and date_prop.get("type") == "date":
|
|
2002
|
+
date_data = date_prop.get("date")
|
|
2003
|
+
if date_data and date_data.get("start"):
|
|
2004
|
+
return date_data["start"]
|
|
2005
|
+
return None
|
|
2006
|
+
|
|
2007
|
+
@property
|
|
2008
|
+
def resolved_date(self) -> str | None:
|
|
2009
|
+
"""Get resolved date."""
|
|
2010
|
+
date_prop = self._data["properties"].get("Resolved Date") or self._data["properties"].get("resolved_date")
|
|
2011
|
+
if date_prop and date_prop.get("type") == "date":
|
|
2012
|
+
date_data = date_prop.get("date")
|
|
2013
|
+
if date_data and date_data.get("start"):
|
|
2014
|
+
return date_data["start"]
|
|
2015
|
+
return None
|
|
2016
|
+
|
|
2017
|
+
# ===== AUTONOMOUS METHODS =====
|
|
2018
|
+
|
|
2019
|
+
@classmethod
|
|
2020
|
+
async def get(cls, incident_id: str, *, client: "NotionClient") -> "Incident":
|
|
2021
|
+
"""Get an incident by ID."""
|
|
2022
|
+
cache = client.plugin_cache("incidents")
|
|
2023
|
+
if cache and incident_id in cache:
|
|
2024
|
+
return cache[incident_id]
|
|
2025
|
+
|
|
2026
|
+
data = await client._api.pages.get(page_id=incident_id)
|
|
2027
|
+
incident = cls(client, data)
|
|
2028
|
+
|
|
2029
|
+
if cache:
|
|
2030
|
+
cache[incident_id] = incident
|
|
2031
|
+
|
|
2032
|
+
return incident
|
|
2033
|
+
|
|
2034
|
+
@classmethod
|
|
2035
|
+
async def create(
|
|
2036
|
+
cls,
|
|
2037
|
+
*,
|
|
2038
|
+
client: "NotionClient",
|
|
2039
|
+
database_id: str,
|
|
2040
|
+
title: str,
|
|
2041
|
+
project_id: str,
|
|
2042
|
+
affected_version_id: str,
|
|
2043
|
+
severity: str = "High",
|
|
2044
|
+
type: str = "Bug",
|
|
2045
|
+
status: str = "Open",
|
|
2046
|
+
discovery_date: str | None = None,
|
|
2047
|
+
) -> "Incident":
|
|
2048
|
+
"""Create a new incident."""
|
|
2049
|
+
from datetime import datetime
|
|
2050
|
+
from better_notion._api.properties import Title, Date, Select, Relation
|
|
2051
|
+
|
|
2052
|
+
properties: dict[str, Any] = {
|
|
2053
|
+
"Title": Title(title),
|
|
2054
|
+
"Project": Relation([project_id]),
|
|
2055
|
+
"Affected Version": Relation([affected_version_id]),
|
|
2056
|
+
"Severity": Select(severity),
|
|
2057
|
+
"Type": Select(type),
|
|
2058
|
+
"Status": Select(status),
|
|
2059
|
+
"Discovery Date": Date(discovery_date or datetime.now().isoformat()),
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
data = await client._api.pages.create(
|
|
2063
|
+
parent={"database_id": database_id},
|
|
2064
|
+
properties=properties,
|
|
2065
|
+
)
|
|
2066
|
+
|
|
2067
|
+
incident = cls(client, data)
|
|
2068
|
+
|
|
2069
|
+
cache = client.plugin_cache("incidents")
|
|
2070
|
+
if cache:
|
|
2071
|
+
cache[incident.id] = incident
|
|
2072
|
+
|
|
2073
|
+
return incident
|
|
2074
|
+
|
|
2075
|
+
async def update(
|
|
2076
|
+
self,
|
|
2077
|
+
*,
|
|
2078
|
+
title: str | None = None,
|
|
2079
|
+
severity: str | None = None,
|
|
2080
|
+
status: str | None = None,
|
|
2081
|
+
root_cause: str | None = None,
|
|
2082
|
+
resolved_date: str | None = None,
|
|
2083
|
+
) -> "Incident":
|
|
2084
|
+
"""Update incident properties."""
|
|
2085
|
+
from better_notion._api.properties import Title, RichText, Select, Date
|
|
2086
|
+
|
|
2087
|
+
properties: dict[str, Any] = {}
|
|
2088
|
+
|
|
2089
|
+
if title is not None:
|
|
2090
|
+
properties["Title"] = Title(title)
|
|
2091
|
+
if severity is not None:
|
|
2092
|
+
properties["Severity"] = Select(severity)
|
|
2093
|
+
if status is not None:
|
|
2094
|
+
properties["Status"] = Select(status)
|
|
2095
|
+
if root_cause is not None:
|
|
2096
|
+
properties["Root Cause"] = RichText(root_cause)
|
|
2097
|
+
if resolved_date is not None:
|
|
2098
|
+
properties["Resolved Date"] = Date(resolved_date)
|
|
2099
|
+
|
|
2100
|
+
data = await self._client._api.pages.update(
|
|
2101
|
+
page_id=self.id,
|
|
2102
|
+
properties=properties,
|
|
2103
|
+
)
|
|
2104
|
+
|
|
2105
|
+
self._data = data
|
|
2106
|
+
|
|
2107
|
+
cache = self._client.plugin_cache("incidents")
|
|
2108
|
+
if cache and self.id in cache:
|
|
2109
|
+
del cache[self.id]
|
|
2110
|
+
|
|
2111
|
+
return self
|
|
2112
|
+
|
|
2113
|
+
async def delete(self) -> None:
|
|
2114
|
+
"""Delete the incident."""
|
|
2115
|
+
await self._client._api.pages.delete(page_id=self.id)
|
|
2116
|
+
|
|
2117
|
+
cache = self._client.plugin_cache("incidents")
|
|
2118
|
+
if cache and self.id in cache:
|
|
2119
|
+
del cache[self.id]
|
|
2120
|
+
|
|
2121
|
+
# ===== WORKFLOW METHODS =====
|
|
2122
|
+
|
|
2123
|
+
async def investigate(self) -> "Incident":
|
|
2124
|
+
"""
|
|
2125
|
+
Mark incident as under investigation.
|
|
2126
|
+
|
|
2127
|
+
Returns:
|
|
2128
|
+
Updated Incident instance
|
|
2129
|
+
|
|
2130
|
+
Example:
|
|
2131
|
+
>>> await incident.investigate()
|
|
2132
|
+
"""
|
|
2133
|
+
return await self.update(status="Investigating")
|
|
2134
|
+
|
|
2135
|
+
async def assign(self, task_id: str) -> "Incident":
|
|
2136
|
+
"""
|
|
2137
|
+
Assign a fix task to this incident.
|
|
2138
|
+
|
|
2139
|
+
Args:
|
|
2140
|
+
task_id: Task ID that will fix this incident
|
|
2141
|
+
|
|
2142
|
+
Returns:
|
|
2143
|
+
Updated Incident instance
|
|
2144
|
+
|
|
2145
|
+
Example:
|
|
2146
|
+
>>> await incident.assign(task.id)
|
|
2147
|
+
"""
|
|
2148
|
+
from better_notion._api.properties import Relation
|
|
2149
|
+
|
|
2150
|
+
await self._client._api.pages.update(
|
|
2151
|
+
page_id=self.id,
|
|
2152
|
+
properties={
|
|
2153
|
+
"Fix Task": Relation([task_id]),
|
|
2154
|
+
"Status": Select("Fix in Progress"),
|
|
2155
|
+
},
|
|
2156
|
+
)
|
|
2157
|
+
|
|
2158
|
+
# Update local data
|
|
2159
|
+
self._data["properties"]["Fix Task"] = {"type": "relation", "relation": [{"id": task_id}]}
|
|
2160
|
+
self._data["properties"]["Status"] = {"type": "select", "select": {"name": "Fix in Progress"}}
|
|
2161
|
+
|
|
2162
|
+
return self
|
|
2163
|
+
|
|
2164
|
+
async def resolve(self, root_cause: str, resolved_date: str | None = None) -> "Incident":
|
|
2165
|
+
"""
|
|
2166
|
+
Resolve this incident.
|
|
2167
|
+
|
|
2168
|
+
Args:
|
|
2169
|
+
root_cause: Analysis of what went wrong
|
|
2170
|
+
resolved_date: When incident was resolved (defaults to now)
|
|
2171
|
+
|
|
2172
|
+
Returns:
|
|
2173
|
+
Updated Incident instance
|
|
2174
|
+
|
|
2175
|
+
Example:
|
|
2176
|
+
>>> await incident.resolve("Config error in connection pool")
|
|
2177
|
+
"""
|
|
2178
|
+
from datetime import datetime
|
|
2179
|
+
|
|
2180
|
+
return await self.update(
|
|
2181
|
+
status="Resolved",
|
|
2182
|
+
root_cause=root_cause,
|
|
2183
|
+
resolved_date=resolved_date or datetime.now().isoformat(),
|
|
2184
|
+
)
|
|
2185
|
+
|
|
2186
|
+
def calculate_mttr(self) -> float | None:
|
|
2187
|
+
"""
|
|
2188
|
+
Calculate Mean Time To Resolve in minutes.
|
|
2189
|
+
|
|
2190
|
+
Returns:
|
|
2191
|
+
MTTR in minutes, or None if not resolved
|
|
2192
|
+
|
|
2193
|
+
Example:
|
|
2194
|
+
>>> mttr_minutes = incident.calculate_mttr()
|
|
2195
|
+
>>> print(f"MTTR: {mttr_minutes:.1f} minutes")
|
|
2196
|
+
"""
|
|
2197
|
+
if not self.resolved_date or not self.discovery_date:
|
|
2198
|
+
return None
|
|
2199
|
+
|
|
2200
|
+
from datetime import datetime
|
|
2201
|
+
|
|
2202
|
+
resolved = datetime.fromisoformat(self.resolved_date)
|
|
2203
|
+
discovered = datetime.fromisoformat(self.discovery_date)
|
|
2204
|
+
|
|
2205
|
+
mttr_seconds = (resolved - discovered).total_seconds()
|
|
2206
|
+
return mttr_seconds / 60
|
|
2207
|
+
|
|
2208
|
+
async def create_fix_task(
|
|
2209
|
+
self,
|
|
2210
|
+
*,
|
|
2211
|
+
version_id: str,
|
|
2212
|
+
title: str | None = None,
|
|
2213
|
+
priority: str = "Critical",
|
|
2214
|
+
) -> Task:
|
|
2215
|
+
"""
|
|
2216
|
+
Create a fix task for this incident.
|
|
2217
|
+
|
|
2218
|
+
Args:
|
|
2219
|
+
version_id: Version to create fix in
|
|
2220
|
+
title: Task title (defaults to incident title)
|
|
2221
|
+
priority: Task priority
|
|
2222
|
+
|
|
2223
|
+
Returns:
|
|
2224
|
+
Created Task instance
|
|
2225
|
+
|
|
2226
|
+
Example:
|
|
2227
|
+
>>> task = await incident.create_fix_task(
|
|
2228
|
+
... version_id=hotfix.id,
|
|
2229
|
+
... priority="Critical"
|
|
2230
|
+
... )
|
|
2231
|
+
"""
|
|
2232
|
+
from better_notion.plugins.official.agents_sdk.models import Task
|
|
2233
|
+
|
|
2234
|
+
import json
|
|
2235
|
+
from pathlib import Path
|
|
2236
|
+
|
|
2237
|
+
config_path = Path.home() / ".notion" / "workspace.json"
|
|
2238
|
+
config = json.loads(config_path.read_text())
|
|
2239
|
+
tasks_db_id = config.get("Tasks")
|
|
2240
|
+
|
|
2241
|
+
if not tasks_db_id:
|
|
2242
|
+
raise ValueError("Tasks database not found in workspace config")
|
|
2243
|
+
|
|
2244
|
+
task = await Task.create(
|
|
2245
|
+
client=self._client,
|
|
2246
|
+
database_id=tasks_db_id,
|
|
2247
|
+
title=title or f"Fix incident: {self.title}",
|
|
2248
|
+
version_id=version_id,
|
|
2249
|
+
task_type="Bug Fix",
|
|
2250
|
+
priority=priority,
|
|
2251
|
+
)
|
|
2252
|
+
|
|
2253
|
+
# Assign task to incident
|
|
2254
|
+
await self.assign(task.id)
|
|
2255
|
+
|
|
2256
|
+
return task
|