better-notion 2.0.1__py3-none-any.whl → 2.1.1__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.
@@ -525,6 +525,241 @@ class TaskManager:
525
525
 
526
526
  return blocked_tasks
527
527
 
528
+ async def assign(self, task_id: str, assignee: str) -> dict:
529
+ """
530
+ Assign a task to a person.
531
+
532
+ Args:
533
+ task_id: Task ID to assign
534
+ assignee: Name of the person to assign to
535
+
536
+ Returns:
537
+ Dict with task_id, assigned_to, and previous assignee
538
+
539
+ Example:
540
+ >>> result = await manager.assign("task_123", "Alice Chen")
541
+ """
542
+ from better_notion.plugins.official.agents_sdk.models import Task
543
+
544
+ task = await Task.get(task_id, client=self._client)
545
+ previous = task.assignee
546
+
547
+ await task.assign_to(assignee)
548
+
549
+ return {
550
+ "task_id": task_id,
551
+ "assigned_to": assignee,
552
+ "previous": previous
553
+ }
554
+
555
+ async def unassign(self, task_id: str) -> dict:
556
+ """
557
+ Unassign a task.
558
+
559
+ Args:
560
+ task_id: Task ID to unassign
561
+
562
+ Returns:
563
+ Dict with task_id, assigned_to (None), and previous assignee
564
+
565
+ Example:
566
+ >>> result = await manager.unassign("task_123")
567
+ """
568
+ from better_notion.plugins.official.agents_sdk.models import Task
569
+
570
+ task = await Task.get(task_id, client=self._client)
571
+ previous = task.assignee
572
+
573
+ await task.unassign()
574
+
575
+ return {
576
+ "task_id": task_id,
577
+ "assigned_to": None,
578
+ "previous": previous
579
+ }
580
+
581
+ async def reassign(self, task_id: str, from_assignee: str, to_assignee: str) -> dict:
582
+ """
583
+ Reassign a task from one person to another.
584
+
585
+ Args:
586
+ task_id: Task ID to reassign
587
+ from_assignee: Current assignee (for validation)
588
+ to_assignee: New assignee
589
+
590
+ Returns:
591
+ Dict with task_id, from, and to
592
+
593
+ Raises:
594
+ ValueError: If task is not assigned to from_assignee
595
+
596
+ Example:
597
+ >>> result = await manager.reassign("task_123", "Alice", "Bob")
598
+ """
599
+ from better_notion.plugins.official.agents_sdk.models import Task
600
+
601
+ task = await Task.get(task_id, client=self._client)
602
+
603
+ if task.assignee != from_assignee:
604
+ raise ValueError(f"Task is not assigned to {from_assignee}")
605
+
606
+ await task.assign_to(to_assignee)
607
+
608
+ return {
609
+ "task_id": task_id,
610
+ "from": from_assignee,
611
+ "to": to_assignee
612
+ }
613
+
614
+ async def list_by_assignee(
615
+ self,
616
+ assignee: str,
617
+ status: str | None = None
618
+ ) -> list:
619
+ """
620
+ List tasks assigned to a person.
621
+
622
+ Args:
623
+ assignee: Name of the assignee
624
+ status: Optional status filter
625
+
626
+ Returns:
627
+ List of Task instances
628
+
629
+ Example:
630
+ >>> tasks = await manager.list_by_assignee("Alice Chen", status="In Progress")
631
+ """
632
+ from better_notion.plugins.official.agents_sdk.models import Task
633
+
634
+ database_id = self._get_database_id("Tasks")
635
+ if not database_id:
636
+ return []
637
+
638
+ filters: list[dict[str, Any]] = [
639
+ {
640
+ "property": "Assignee",
641
+ "select": {"equals": assignee}
642
+ }
643
+ ]
644
+
645
+ if status:
646
+ filters.append({
647
+ "property": "Status",
648
+ "select": {"equals": status}
649
+ })
650
+
651
+ response = await self._client._api.databases.query(
652
+ database_id=database_id,
653
+ filter={"and": filters} if len(filters) > 1 else filters[0]
654
+ )
655
+
656
+ return [
657
+ Task(self._client, page_data)
658
+ for page_data in response.get("results", [])
659
+ ]
660
+
661
+ async def list_unassigned(self) -> list:
662
+ """
663
+ List unassigned tasks.
664
+
665
+ Returns:
666
+ List of Task instances with no assignee
667
+
668
+ Example:
669
+ >>> tasks = await manager.list_unassigned()
670
+ """
671
+ from better_notion.plugins.official.agents_sdk.models import Task
672
+
673
+ database_id = self._get_database_id("Tasks")
674
+ if not database_id:
675
+ return []
676
+
677
+ response = await self._client._api.databases.query(
678
+ database_id=database_id,
679
+ filter={
680
+ "property": "Assignee",
681
+ "select": {"is_empty": True}
682
+ }
683
+ )
684
+
685
+ return [
686
+ Task(self._client, page_data)
687
+ for page_data in response.get("results", [])
688
+ ]
689
+
690
+ async def can_start(self, task_id: str) -> dict:
691
+ """
692
+ Check if a task can start (all dependencies completed).
693
+
694
+ Args:
695
+ task_id: Task ID to check
696
+
697
+ Returns:
698
+ Dict with can_start (bool), task_id, and incomplete_dependencies
699
+
700
+ Example:
701
+ >>> result = await manager.can_start("task_123")
702
+ """
703
+ from better_notion.plugins.official.agents_sdk.models import Task
704
+
705
+ task = await Task.get(task_id, client=self._client)
706
+ deps = await task.dependencies()
707
+ incomplete = [d for d in deps if d.status != "Completed"]
708
+
709
+ return {
710
+ "can_start": len(incomplete) == 0,
711
+ "task_id": task_id,
712
+ "incomplete_dependencies": [
713
+ {"id": d.id, "title": d.title, "status": d.status}
714
+ for d in incomplete
715
+ ]
716
+ }
717
+
718
+ async def deps(self, task_id: str) -> dict:
719
+ """
720
+ List all dependencies of a task.
721
+
722
+ Args:
723
+ task_id: Task ID
724
+
725
+ Returns:
726
+ Dict with task_id and dependencies list
727
+
728
+ Example:
729
+ >>> result = await manager.deps("task_123")
730
+ """
731
+ from better_notion.plugins.official.agents_sdk.models import Task
732
+
733
+ task = await Task.get(task_id, client=self._client)
734
+ deps = await task.dependencies()
735
+
736
+ return {
737
+ "task_id": task_id,
738
+ "dependencies": [
739
+ {
740
+ "id": d.id,
741
+ "title": d.title,
742
+ "status": d.status
743
+ }
744
+ for d in deps
745
+ ]
746
+ }
747
+
748
+ async def ready(self, version_id: str | None = None) -> list:
749
+ """
750
+ List all tasks ready to start (dependencies completed).
751
+
752
+ Args:
753
+ version_id: Optional version filter
754
+
755
+ Returns:
756
+ List of Task instances ready to start
757
+
758
+ Example:
759
+ >>> tasks = await manager.ready(version_id="ver_123")
760
+ """
761
+ return await self.find_ready(version_id)
762
+
528
763
  def _get_database_id(self, name: str) -> str | None:
529
764
  """Get database ID from workspace config."""
530
765
  return getattr(self._client, "_workspace_config", {}).get(name)
@@ -826,6 +1061,41 @@ class WorkIssueManager:
826
1061
 
827
1062
  return issue
828
1063
 
1064
+ async def list_blocked_by(self, work_issue_id: str) -> list:
1065
+ """
1066
+ List tasks blocked by a work issue.
1067
+
1068
+ Args:
1069
+ work_issue_id: Work issue ID
1070
+
1071
+ Returns:
1072
+ List of Task instances
1073
+
1074
+ Example:
1075
+ >>> tasks = await manager.list_blocked_by("issue_456")
1076
+ """
1077
+ from better_notion.plugins.official.agents_sdk.models import Task
1078
+
1079
+ database_id = self._get_database_id("Tasks")
1080
+ if not database_id:
1081
+ return []
1082
+
1083
+ response = await self._client._api._request(
1084
+ "POST",
1085
+ f"/databases/{database_id}/query",
1086
+ json={
1087
+ "filter": {
1088
+ "property": "Related Work Issue",
1089
+ "relation": {"contains": work_issue_id}
1090
+ }
1091
+ }
1092
+ )
1093
+
1094
+ return [
1095
+ Task(self._client, page_data)
1096
+ for page_data in response.get("results", [])
1097
+ ]
1098
+
829
1099
  def _get_database_id(self, name: str) -> str | None:
830
1100
  """Get database ID from workspace config."""
831
1101
  return getattr(self._client, "_workspace_config", {}).get(name)
@@ -992,6 +1262,89 @@ class IncidentManager:
992
1262
 
993
1263
  return mttr
994
1264
 
1265
+ async def link_to_work_issue(self, incident_id: str, work_issue_id: str) -> dict:
1266
+ """
1267
+ Link an incident to a work issue (root cause).
1268
+
1269
+ Args:
1270
+ incident_id: Incident ID
1271
+ work_issue_id: Work issue ID to link to
1272
+
1273
+ Returns:
1274
+ Dict with incident_id and work_issue_id
1275
+
1276
+ Example:
1277
+ >>> result = await manager.link_to_work_issue("inc_123", "issue_456")
1278
+ """
1279
+ from better_notion.plugins.official.agents_sdk.models import Incident
1280
+
1281
+ incident = await Incident.get(incident_id, client=self._client)
1282
+ await incident.link_to_work_issue(work_issue_id)
1283
+
1284
+ return {
1285
+ "incident_id": incident_id,
1286
+ "work_issue_id": work_issue_id,
1287
+ "linked": True
1288
+ }
1289
+
1290
+ async def unlink_work_issue(self, incident_id: str) -> dict:
1291
+ """
1292
+ Unlink an incident from its work issue.
1293
+
1294
+ Args:
1295
+ incident_id: Incident ID
1296
+
1297
+ Returns:
1298
+ Dict with incident_id and unlinked status
1299
+
1300
+ Example:
1301
+ >>> result = await manager.unlink_work_issue("inc_123")
1302
+ """
1303
+ from better_notion.plugins.official.agents_sdk.models import Incident
1304
+
1305
+ incident = await Incident.get(incident_id, client=self._client)
1306
+ await incident.unlink_work_issue()
1307
+
1308
+ return {
1309
+ "incident_id": incident_id,
1310
+ "unlinked": True
1311
+ }
1312
+
1313
+ async def list_caused_by(self, work_issue_id: str) -> list:
1314
+ """
1315
+ List all incidents caused by a work issue.
1316
+
1317
+ Args:
1318
+ work_issue_id: Work issue ID
1319
+
1320
+ Returns:
1321
+ List of Incident instances
1322
+
1323
+ Example:
1324
+ >>> incidents = await manager.list_caused_by("issue_456")
1325
+ """
1326
+ from better_notion.plugins.official.agents_sdk.models import Incident
1327
+
1328
+ database_id = self._get_database_id("Incidents")
1329
+ if not database_id:
1330
+ return []
1331
+
1332
+ response = await self._client._api._request(
1333
+ "POST",
1334
+ f"/databases/{database_id}/query",
1335
+ json={
1336
+ "filter": {
1337
+ "property": "Root Cause Work Issue",
1338
+ "relation": {"contains": work_issue_id}
1339
+ }
1340
+ }
1341
+ )
1342
+
1343
+ return [
1344
+ Incident(self._client, page_data)
1345
+ for page_data in response.get("results", [])
1346
+ ]
1347
+
995
1348
  def _get_database_id(self, name: str) -> str | None:
996
1349
  """Get database ID from workspace config."""
997
1350
  return getattr(self._client, "_workspace_config", {}).get(name)
@@ -1053,6 +1053,26 @@ class Task(DatabasePageEntityMixin, BaseEntity):
1053
1053
  return hours_prop.get("number")
1054
1054
  return None
1055
1055
 
1056
+ @property
1057
+ def assignee(self) -> str | None:
1058
+ """Get the assignee of this task."""
1059
+ assignee_prop = self._data["properties"].get("Assignee") or self._data["properties"].get("assignee")
1060
+ if assignee_prop and assignee_prop.get("type") == "select":
1061
+ select_data = assignee_prop.get("select")
1062
+ if select_data:
1063
+ return select_data.get("name")
1064
+ return None
1065
+
1066
+ @property
1067
+ def related_work_issue_id(self) -> str | None:
1068
+ """Get related work issue ID (blocking this task or caused by this task)."""
1069
+ issue_prop = self._data["properties"].get("Related Work Issue") or self._data["properties"].get("related_work_issue")
1070
+ if issue_prop and issue_prop.get("type") == "relation":
1071
+ relations = issue_prop.get("relation", [])
1072
+ if relations:
1073
+ return relations[0].get("id")
1074
+ return None
1075
+
1056
1076
  # ===== AUTONOMOUS METHODS =====
1057
1077
 
1058
1078
  @classmethod
@@ -1262,6 +1282,121 @@ class Task(DatabasePageEntityMixin, BaseEntity):
1262
1282
  return False
1263
1283
  return True
1264
1284
 
1285
+ async def related_work_issue(self) -> "WorkIssue | None":
1286
+ """
1287
+ Get the related work issue (blocking this task or caused by this task).
1288
+
1289
+ Returns:
1290
+ WorkIssue instance or None
1291
+
1292
+ Example:
1293
+ >>> issue = await task.related_work_issue()
1294
+ """
1295
+ from better_notion.plugins.official.agents_sdk.models import WorkIssue
1296
+
1297
+ issue_id = self.related_work_issue_id
1298
+ if not issue_id:
1299
+ return None
1300
+
1301
+ try:
1302
+ return await WorkIssue.get(issue_id, client=self._client)
1303
+ except Exception:
1304
+ return None
1305
+
1306
+ async def link_to_work_issue(self, work_issue_id: str) -> None:
1307
+ """
1308
+ Link this task to a work issue.
1309
+
1310
+ Args:
1311
+ work_issue_id: Work issue ID to link to
1312
+
1313
+ Example:
1314
+ >>> await task.link_to_work_issue(issue_id)
1315
+ """
1316
+ from better_notion._api.properties import Relation
1317
+
1318
+ await self._client._api.pages.update(
1319
+ page_id=self.id,
1320
+ properties={
1321
+ "Related Work Issue": Relation([work_issue_id]).to_dict(),
1322
+ },
1323
+ )
1324
+
1325
+ # Update local data
1326
+ self._data["properties"]["Related Work Issue"] = {
1327
+ "type": "relation",
1328
+ "relation": [{"id": work_issue_id}]
1329
+ }
1330
+
1331
+ async def unlink_work_issue(self) -> None:
1332
+ """
1333
+ Unlink this task from its work issue.
1334
+
1335
+ Example:
1336
+ >>> await task.unlink_work_issue()
1337
+ """
1338
+ from better_notion._api.properties import Relation
1339
+
1340
+ await self._client._api.pages.update(
1341
+ page_id=self.id,
1342
+ properties={
1343
+ "Related Work Issue": Relation([]).to_dict(),
1344
+ },
1345
+ )
1346
+
1347
+ # Update local data
1348
+ self._data["properties"]["Related Work Issue"] = {
1349
+ "type": "relation",
1350
+ "relation": []
1351
+ }
1352
+
1353
+ async def assign_to(self, assignee: str) -> None:
1354
+ """
1355
+ Assign this task to a person.
1356
+
1357
+ Args:
1358
+ assignee: Name of the person to assign to
1359
+
1360
+ Example:
1361
+ >>> await task.assign_to("Alice Chen")
1362
+ """
1363
+ from better_notion._api.properties import Select
1364
+
1365
+ await self._client._api.pages.update(
1366
+ page_id=self.id,
1367
+ properties={
1368
+ "Assignee": Select(name="Assignee", value=assignee).to_dict(),
1369
+ },
1370
+ )
1371
+
1372
+ # Update local data
1373
+ self._data["properties"]["Assignee"] = {
1374
+ "type": "select",
1375
+ "select": {"name": assignee}
1376
+ }
1377
+
1378
+ async def unassign(self) -> None:
1379
+ """
1380
+ Unassign this task.
1381
+
1382
+ Example:
1383
+ >>> await task.unassign()
1384
+ """
1385
+ from better_notion._api.properties import Select
1386
+
1387
+ await self._client._api.pages.update(
1388
+ page_id=self.id,
1389
+ properties={
1390
+ "Assignee": None,
1391
+ },
1392
+ )
1393
+
1394
+ # Update local data
1395
+ self._data["properties"]["Assignee"] = {
1396
+ "type": "select",
1397
+ "select": None
1398
+ }
1399
+
1265
1400
 
1266
1401
  class Idea(DatabasePageEntityMixin, BaseEntity):
1267
1402
  """
@@ -1790,6 +1925,24 @@ class WorkIssue(DatabasePageEntityMixin, BaseEntity):
1790
1925
  return relations[0].get("id")
1791
1926
  return None
1792
1927
 
1928
+ @property
1929
+ def caused_incident_ids(self) -> list[str]:
1930
+ """Get incident IDs caused by this work issue."""
1931
+ incidents_prop = self._data["properties"].get("Caused Incidents") or self._data["properties"].get("caused_incidents")
1932
+ if incidents_prop and incidents_prop.get("type") == "relation":
1933
+ relations = incidents_prop.get("relation", [])
1934
+ return [r.get("id", "") for r in relations if r.get("id")]
1935
+ return []
1936
+
1937
+ @property
1938
+ def blocking_task_ids(self) -> list[str]:
1939
+ """Get task IDs blocked by this work issue."""
1940
+ tasks_prop = self._data["properties"].get("Blocking Tasks") or self._data["properties"].get("blocking_tasks")
1941
+ if tasks_prop and tasks_prop.get("type") == "relation":
1942
+ relations = tasks_prop.get("relation", [])
1943
+ return [r.get("id", "") for r in relations if r.get("id")]
1944
+ return []
1945
+
1793
1946
  # ===== AUTONOMOUS METHODS =====
1794
1947
 
1795
1948
  @classmethod
@@ -2010,6 +2163,48 @@ class WorkIssue(DatabasePageEntityMixin, BaseEntity):
2010
2163
 
2011
2164
  return idea
2012
2165
 
2166
+ async def caused_incidents(self) -> list["Incident"]:
2167
+ """
2168
+ Get incidents caused by this work issue.
2169
+
2170
+ Returns:
2171
+ List of Incident instances
2172
+
2173
+ Example:
2174
+ >>> incidents = await issue.caused_incidents()
2175
+ """
2176
+ from better_notion.plugins.official.agents_sdk.models import Incident
2177
+
2178
+ incidents = []
2179
+ for incident_id in self.caused_incident_ids:
2180
+ try:
2181
+ incident = await Incident.get(incident_id, client=self._client)
2182
+ incidents.append(incident)
2183
+ except Exception:
2184
+ pass
2185
+ return incidents
2186
+
2187
+ async def blocking_tasks(self) -> list["Task"]:
2188
+ """
2189
+ Get tasks blocked by this work issue.
2190
+
2191
+ Returns:
2192
+ List of Task instances
2193
+
2194
+ Example:
2195
+ >>> tasks = await issue.blocking_tasks()
2196
+ """
2197
+ from better_notion.plugins.official.agents_sdk.models import Task
2198
+
2199
+ tasks = []
2200
+ for task_id in self.blocking_task_ids:
2201
+ try:
2202
+ task = await Task.get(task_id, client=self._client)
2203
+ tasks.append(task)
2204
+ except Exception:
2205
+ pass
2206
+ return tasks
2207
+
2013
2208
 
2014
2209
  class Incident(DatabasePageEntityMixin, BaseEntity):
2015
2210
  """
@@ -2153,6 +2348,16 @@ class Incident(DatabasePageEntityMixin, BaseEntity):
2153
2348
  return date_data["start"]
2154
2349
  return None
2155
2350
 
2351
+ @property
2352
+ def root_cause_work_issue_id(self) -> str | None:
2353
+ """Get the work issue ID that caused this incident."""
2354
+ issue_prop = self._data["properties"].get("Root Cause Work Issue") or self._data["properties"].get("root_cause_work_issue")
2355
+ if issue_prop and issue_prop.get("type") == "relation":
2356
+ relations = issue_prop.get("relation", [])
2357
+ if relations:
2358
+ return relations[0].get("id")
2359
+ return None
2360
+
2156
2361
  # ===== AUTONOMOUS METHODS =====
2157
2362
 
2158
2363
  @classmethod
@@ -2405,3 +2610,71 @@ class Incident(DatabasePageEntityMixin, BaseEntity):
2405
2610
  await self.assign(task.id)
2406
2611
 
2407
2612
  return task
2613
+
2614
+ async def root_cause_work_issue(self) -> "WorkIssue | None":
2615
+ """
2616
+ Get the work issue that caused this incident.
2617
+
2618
+ Returns:
2619
+ WorkIssue instance or None
2620
+
2621
+ Example:
2622
+ >>> issue = await incident.root_cause_work_issue()
2623
+ """
2624
+ from better_notion.plugins.official.agents_sdk.models import WorkIssue
2625
+
2626
+ issue_id = self.root_cause_work_issue_id
2627
+ if not issue_id:
2628
+ return None
2629
+
2630
+ try:
2631
+ return await WorkIssue.get(issue_id, client=self._client)
2632
+ except Exception:
2633
+ return None
2634
+
2635
+ async def link_to_work_issue(self, work_issue_id: str) -> None:
2636
+ """
2637
+ Link this incident to a work issue (root cause).
2638
+
2639
+ Args:
2640
+ work_issue_id: Work issue ID to link to
2641
+
2642
+ Example:
2643
+ >>> await incident.link_to_work_issue(issue_id)
2644
+ """
2645
+ from better_notion._api.properties import Relation
2646
+
2647
+ await self._client._api.pages.update(
2648
+ page_id=self.id,
2649
+ properties={
2650
+ "Root Cause Work Issue": Relation([work_issue_id]).to_dict(),
2651
+ },
2652
+ )
2653
+
2654
+ # Update local data
2655
+ self._data["properties"]["Root Cause Work Issue"] = {
2656
+ "type": "relation",
2657
+ "relation": [{"id": work_issue_id}]
2658
+ }
2659
+
2660
+ async def unlink_work_issue(self) -> None:
2661
+ """
2662
+ Unlink this incident from its work issue.
2663
+
2664
+ Example:
2665
+ >>> await incident.unlink_work_issue()
2666
+ """
2667
+ from better_notion._api.properties import Relation
2668
+
2669
+ await self._client._api.pages.update(
2670
+ page_id=self.id,
2671
+ properties={
2672
+ "Root Cause Work Issue": Relation([]).to_dict(),
2673
+ },
2674
+ )
2675
+
2676
+ # Update local data
2677
+ self._data["properties"]["Root Cause Work Issue"] = {
2678
+ "type": "relation",
2679
+ "relation": []
2680
+ }