better-notion 1.0.1__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.
@@ -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