better-notion 2.0.0__py3-none-any.whl → 2.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.

Potentially problematic release.


This version of better-notion might be problematic. Click here for more details.

@@ -910,8 +910,51 @@ class AgentsPlugin(CombinedPluginInterface):
910
910
  typer.echo(agents_cli.tasks_complete(task_id, actual_hours))
911
911
 
912
912
  @tasks_app.command("can-start")
913
- def tasks_can_start_cmd(task_id: str):
914
- typer.echo(agents_cli.tasks_can_start(task_id))
913
+ def tasks_can_start_cmd(
914
+ task_id: str,
915
+ explain: bool = typer.Option(False, "--explain", help="Show detailed explanation of blocking tasks")
916
+ ):
917
+ typer.echo(agents_cli.tasks_can_start(task_id, explain))
918
+
919
+ @tasks_app.command("deps")
920
+ def tasks_deps_cmd(task_id: str):
921
+ typer.echo(agents_cli.tasks_deps(task_id))
922
+
923
+ @tasks_app.command("ready")
924
+ def tasks_ready_cmd(
925
+ version_id: str = typer.Option(None, "--version-id", "-v", help="Filter by version ID")
926
+ ):
927
+ typer.echo(agents_cli.tasks_ready(version_id))
928
+
929
+ @tasks_app.command("assign")
930
+ def tasks_assign_cmd(
931
+ task_id: str,
932
+ to: str = typer.Option(..., "--to", help="Person to assign to")
933
+ ):
934
+ typer.echo(agents_cli.tasks_assign(task_id, to))
935
+
936
+ @tasks_app.command("unassign")
937
+ def tasks_unassign_cmd(task_id: str):
938
+ typer.echo(agents_cli.tasks_unassign(task_id))
939
+
940
+ @tasks_app.command("reassign")
941
+ def tasks_reassign_cmd(
942
+ task_id: str,
943
+ from_: str = typer.Option(..., "--from", help="Current assignee (for validation)"),
944
+ to: str = typer.Option(..., "--to", help="New assignee")
945
+ ):
946
+ typer.echo(agents_cli.tasks_reassign(task_id, from_, to))
947
+
948
+ @tasks_app.command("list-by-assignee")
949
+ def tasks_list_by_assignee_cmd(
950
+ assignee: str,
951
+ status: str = typer.Option(None, "--status", "-s", help="Filter by status")
952
+ ):
953
+ typer.echo(agents_cli.tasks_list_by_assignee(assignee, status))
954
+
955
+ @tasks_app.command("list-unassigned")
956
+ def tasks_list_unassigned_cmd():
957
+ typer.echo(agents_cli.tasks_list_unassigned())
915
958
 
916
959
  agents_app.add_typer(tasks_app)
917
960
 
@@ -997,6 +1040,10 @@ class AgentsPlugin(CombinedPluginInterface):
997
1040
  def work_issues_blockers_cmd(project_id: str):
998
1041
  typer.echo(agents_cli.work_issues_blockers(project_id))
999
1042
 
1043
+ @work_issues_app.command("list-blocked-by")
1044
+ def work_issues_list_blocked_by_cmd(work_issue_id: str):
1045
+ typer.echo(agents_cli.work_issues_list_blocked_by(work_issue_id))
1046
+
1000
1047
  agents_app.add_typer(work_issues_app)
1001
1048
 
1002
1049
  # Incidents commands (under agents)
@@ -1041,6 +1088,21 @@ class AgentsPlugin(CombinedPluginInterface):
1041
1088
  def incidents_sla_violations_cmd():
1042
1089
  typer.echo(agents_cli.incidents_sla_violations())
1043
1090
 
1091
+ @incidents_app.command("link-to-work-issue")
1092
+ def incidents_link_to_work_issue_cmd(
1093
+ incident_id: str,
1094
+ work_issue_id: str
1095
+ ):
1096
+ typer.echo(agents_cli.incidents_link_to_work_issue(incident_id, work_issue_id))
1097
+
1098
+ @incidents_app.command("unlink-work-issue")
1099
+ def incidents_unlink_work_issue_cmd(incident_id: str):
1100
+ typer.echo(agents_cli.incidents_unlink_work_issue(incident_id))
1101
+
1102
+ @incidents_app.command("list-caused-by")
1103
+ def incidents_list_caused_by_cmd(work_issue_id: str):
1104
+ typer.echo(agents_cli.incidents_list_caused_by(work_issue_id))
1105
+
1044
1106
  agents_app.add_typer(incidents_app)
1045
1107
 
1046
1108
  def register_sdk_models(self) -> dict[str, type]:
@@ -820,15 +820,17 @@ def tasks_complete(
820
820
  return asyncio.run(_complete())
821
821
 
822
822
 
823
- def tasks_can_start(task_id: str) -> str:
823
+ def tasks_can_start(task_id: str, explain: bool = False) -> str:
824
824
  """
825
825
  Check if a task can start (all dependencies completed).
826
826
 
827
827
  Args:
828
828
  task_id: Task page ID
829
+ explain: Show detailed explanation of blocking tasks
829
830
 
830
831
  Example:
831
832
  $ notion tasks can-start task_123
833
+ $ notion tasks can-start task_123 --explain
832
834
  """
833
835
  async def _can_start() -> str:
834
836
  try:
@@ -837,33 +839,17 @@ def tasks_can_start(task_id: str) -> str:
837
839
  # Register SDK plugin
838
840
  register_agents_sdk_plugin(client)
839
841
 
840
- # Get task and check
842
+ # Use manager method for detailed info
841
843
  manager = client.plugin_manager("tasks")
842
- task = await manager.get(task_id)
843
- can_start = await task.can_start()
844
-
845
- if not can_start:
846
- # Get incomplete dependencies
847
- incomplete = []
848
- for dep in await task.dependencies():
849
- if dep.status != "Completed":
850
- incomplete.append({
851
- "id": dep.id,
852
- "title": dep.title,
853
- "status": dep.status,
854
- })
844
+ result = await manager.can_start(task_id)
855
845
 
856
- return format_success({
857
- "task_id": task.id,
858
- "can_start": False,
859
- "incomplete_dependencies": incomplete,
860
- })
846
+ if explain and not result["can_start"]:
847
+ result["explanation"] = {
848
+ "blocking_tasks": result["incomplete_dependencies"],
849
+ "suggestion": "Wait for dependencies to complete before starting"
850
+ }
861
851
 
862
- return format_success({
863
- "task_id": task.id,
864
- "can_start": True,
865
- "message": "All dependencies are completed",
866
- })
852
+ return format_success(result)
867
853
 
868
854
  except Exception as e:
869
855
  return format_error("CAN_START_ERROR", str(e), retry=False)
@@ -871,6 +857,248 @@ def tasks_can_start(task_id: str) -> str:
871
857
  return asyncio.run(_can_start())
872
858
 
873
859
 
860
+ def tasks_deps(task_id: str) -> str:
861
+ """
862
+ List all dependencies of a task.
863
+
864
+ Args:
865
+ task_id: Task page ID
866
+
867
+ Example:
868
+ $ notion tasks deps task_123
869
+ """
870
+ async def _deps() -> str:
871
+ try:
872
+ client = get_client()
873
+
874
+ # Register SDK plugin
875
+ register_agents_sdk_plugin(client)
876
+
877
+ # Use manager method
878
+ manager = client.plugin_manager("tasks")
879
+ result = await manager.deps(task_id)
880
+
881
+ return format_success(result)
882
+
883
+ except Exception as e:
884
+ return format_error("DEPS_ERROR", str(e), retry=False)
885
+
886
+ return asyncio.run(_deps())
887
+
888
+
889
+ def tasks_ready(version_id: Optional[str] = None) -> str:
890
+ """
891
+ List all tasks ready to start (dependencies completed).
892
+
893
+ Args:
894
+ version_id: Filter by version ID
895
+
896
+ Example:
897
+ $ notion tasks ready
898
+ $ notion tasks ready --version-id ver_123
899
+ """
900
+ async def _ready() -> str:
901
+ try:
902
+ client = get_client()
903
+
904
+ # Register SDK plugin
905
+ register_agents_sdk_plugin(client)
906
+
907
+ # Use manager method
908
+ manager = client.plugin_manager("tasks")
909
+ tasks = await manager.ready(version_id)
910
+
911
+ return format_success({
912
+ "ready_tasks": [
913
+ {
914
+ "id": task.id,
915
+ "title": task.title,
916
+ "status": task.status,
917
+ "priority": task.priority,
918
+ }
919
+ for task in tasks
920
+ ],
921
+ "total": len(tasks)
922
+ })
923
+
924
+ except Exception as e:
925
+ return format_error("READY_ERROR", str(e), retry=False)
926
+
927
+ return asyncio.run(_ready())
928
+
929
+
930
+ def tasks_assign(task_id: str, to: str) -> str:
931
+ """
932
+ Assign a task to a person.
933
+
934
+ Args:
935
+ task_id: Task page ID
936
+ to: Name of the person to assign to
937
+
938
+ Example:
939
+ $ notion tasks assign task_123 --to "Alice Chen"
940
+ """
941
+ async def _assign() -> str:
942
+ try:
943
+ client = get_client()
944
+
945
+ # Register SDK plugin
946
+ register_agents_sdk_plugin(client)
947
+
948
+ # Use manager method
949
+ manager = client.plugin_manager("tasks")
950
+ result = await manager.assign(task_id, to)
951
+
952
+ return format_success(result)
953
+
954
+ except Exception as e:
955
+ return format_error("ASSIGN_ERROR", str(e), retry=False)
956
+
957
+ return asyncio.run(_assign())
958
+
959
+
960
+ def tasks_unassign(task_id: str) -> str:
961
+ """
962
+ Unassign a task.
963
+
964
+ Args:
965
+ task_id: Task page ID
966
+
967
+ Example:
968
+ $ notion tasks unassign task_123
969
+ """
970
+ async def _unassign() -> str:
971
+ try:
972
+ client = get_client()
973
+
974
+ # Register SDK plugin
975
+ register_agents_sdk_plugin(client)
976
+
977
+ # Use manager method
978
+ manager = client.plugin_manager("tasks")
979
+ result = await manager.unassign(task_id)
980
+
981
+ return format_success(result)
982
+
983
+ except Exception as e:
984
+ return format_error("UNASSIGN_ERROR", str(e), retry=False)
985
+
986
+ return asyncio.run(_unassign())
987
+
988
+
989
+ def tasks_reassign(task_id: str, from_: str, to: str) -> str:
990
+ """
991
+ Reassign a task from one person to another.
992
+
993
+ Args:
994
+ task_id: Task page ID
995
+ from_: Current assignee (for validation)
996
+ to: New assignee
997
+
998
+ Example:
999
+ $ notion tasks reassign task_123 --from "Alice" --to "Bob"
1000
+ """
1001
+ async def _reassign() -> str:
1002
+ try:
1003
+ client = get_client()
1004
+
1005
+ # Register SDK plugin
1006
+ register_agents_sdk_plugin(client)
1007
+
1008
+ # Use manager method
1009
+ manager = client.plugin_manager("tasks")
1010
+ result = await manager.reassign(task_id, from_, to)
1011
+
1012
+ return format_success(result)
1013
+
1014
+ except Exception as e:
1015
+ return format_error("REASSIGN_ERROR", str(e), retry=False)
1016
+
1017
+ return asyncio.run(_reassign())
1018
+
1019
+
1020
+ def tasks_list_by_assignee(
1021
+ assignee: str,
1022
+ status: Optional[str] = None
1023
+ ) -> str:
1024
+ """
1025
+ List tasks assigned to a person.
1026
+
1027
+ Args:
1028
+ assignee: Name of the assignee
1029
+ status: Optional status filter
1030
+
1031
+ Example:
1032
+ $ notion tasks list-by-assignee "Alice Chen"
1033
+ $ notion tasks list-by-assignee "Alice" --status "In Progress"
1034
+ """
1035
+ async def _list_by_assignee() -> str:
1036
+ try:
1037
+ client = get_client()
1038
+
1039
+ # Register SDK plugin
1040
+ register_agents_sdk_plugin(client)
1041
+
1042
+ # Use manager method
1043
+ manager = client.plugin_manager("tasks")
1044
+ tasks = await manager.list_by_assignee(assignee, status)
1045
+
1046
+ return format_success({
1047
+ "tasks": [
1048
+ {
1049
+ "id": task.id,
1050
+ "title": task.title,
1051
+ "status": task.status,
1052
+ "priority": task.priority,
1053
+ }
1054
+ for task in tasks
1055
+ ],
1056
+ "total": len(tasks)
1057
+ })
1058
+
1059
+ except Exception as e:
1060
+ return format_error("LIST_BY_ASSIGNEE_ERROR", str(e), retry=False)
1061
+
1062
+ return asyncio.run(_list_by_assignee())
1063
+
1064
+
1065
+ def tasks_list_unassigned() -> str:
1066
+ """
1067
+ List unassigned tasks.
1068
+
1069
+ Example:
1070
+ $ notion tasks list-unassigned
1071
+ """
1072
+ async def _list_unassigned() -> str:
1073
+ try:
1074
+ client = get_client()
1075
+
1076
+ # Register SDK plugin
1077
+ register_agents_sdk_plugin(client)
1078
+
1079
+ # Use manager method
1080
+ manager = client.plugin_manager("tasks")
1081
+ tasks = await manager.list_unassigned()
1082
+
1083
+ return format_success({
1084
+ "unassigned_tasks": [
1085
+ {
1086
+ "id": task.id,
1087
+ "title": task.title,
1088
+ "status": task.status,
1089
+ "priority": task.priority,
1090
+ }
1091
+ for task in tasks
1092
+ ],
1093
+ "total": len(tasks)
1094
+ })
1095
+
1096
+ except Exception as e:
1097
+ return format_error("LIST_UNASSIGNED_ERROR", str(e), retry=False)
1098
+
1099
+ return asyncio.run(_list_unassigned())
1100
+
1101
+
874
1102
  # ===== IDEAS =====
875
1103
 
876
1104
  def ideas_list(
@@ -1432,6 +1660,46 @@ def work_issues_blockers(project_id: str) -> str:
1432
1660
  return asyncio.run(_blockers())
1433
1661
 
1434
1662
 
1663
+ def work_issues_list_blocked_by(work_issue_id: str) -> str:
1664
+ """
1665
+ List tasks blocked by a work issue.
1666
+
1667
+ Args:
1668
+ work_issue_id: Work issue page ID
1669
+
1670
+ Example:
1671
+ $ notion work-issues list-blocked-by issue_456
1672
+ """
1673
+ async def _list_blocked_by() -> str:
1674
+ try:
1675
+ client = get_client()
1676
+
1677
+ # Register SDK plugin
1678
+ register_agents_sdk_plugin(client)
1679
+
1680
+ # Use manager method
1681
+ manager = client.plugin_manager("work_issues")
1682
+ tasks = await manager.list_blocked_by(work_issue_id)
1683
+
1684
+ return format_success({
1685
+ "blocked_tasks": [
1686
+ {
1687
+ "id": task.id,
1688
+ "title": task.title,
1689
+ "status": task.status,
1690
+ "priority": task.priority,
1691
+ }
1692
+ for task in tasks
1693
+ ],
1694
+ "total": len(tasks)
1695
+ })
1696
+
1697
+ except Exception as e:
1698
+ return format_error("LIST_BLOCKED_BY_ERROR", str(e), retry=False)
1699
+
1700
+ return asyncio.run(_list_blocked_by())
1701
+
1702
+
1435
1703
  # ===== INCIDENTS =====
1436
1704
 
1437
1705
  def incidents_list(
@@ -1572,8 +1840,8 @@ def incidents_create(
1572
1840
  "Project": {"relation": [{"id": project_id}]},
1573
1841
  "Severity": {"select": {"name": severity}},
1574
1842
  "Type": {"select": {"name": type}},
1575
- "Status": {"select": {"name": "Active"}},
1576
- "Discovery Date": {
1843
+ "Status": {"select": {"name": "Open"}},
1844
+ "Detected Date": {
1577
1845
  "date": {"start": datetime.now(timezone.utc).isoformat()}
1578
1846
  },
1579
1847
  }
@@ -1726,3 +1994,102 @@ def incidents_sla_violations() -> str:
1726
1994
  return format_error("SLA_VIOLATIONS_ERROR", str(e), retry=False)
1727
1995
 
1728
1996
  return asyncio.run(_sla_violations())
1997
+
1998
+
1999
+ def incidents_link_to_work_issue(incident_id: str, work_issue_id: str) -> str:
2000
+ """
2001
+ Link an incident to a work issue (root cause).
2002
+
2003
+ Args:
2004
+ incident_id: Incident page ID
2005
+ work_issue_id: Work issue page ID
2006
+
2007
+ Example:
2008
+ $ notion incidents link-to-work-issue inc_123 issue_456
2009
+ """
2010
+ async def _link_to_work_issue() -> str:
2011
+ try:
2012
+ client = get_client()
2013
+
2014
+ # Register SDK plugin
2015
+ register_agents_sdk_plugin(client)
2016
+
2017
+ # Use manager method
2018
+ manager = client.plugin_manager("incidents")
2019
+ result = await manager.link_to_work_issue(incident_id, work_issue_id)
2020
+
2021
+ return format_success(result)
2022
+
2023
+ except Exception as e:
2024
+ return format_error("LINK_TO_WORK_ISSUE_ERROR", str(e), retry=False)
2025
+
2026
+ return asyncio.run(_link_to_work_issue())
2027
+
2028
+
2029
+ def incidents_unlink_work_issue(incident_id: str) -> str:
2030
+ """
2031
+ Unlink an incident from its work issue.
2032
+
2033
+ Args:
2034
+ incident_id: Incident page ID
2035
+
2036
+ Example:
2037
+ $ notion incidents unlink-work-issue inc_123
2038
+ """
2039
+ async def _unlink_work_issue() -> str:
2040
+ try:
2041
+ client = get_client()
2042
+
2043
+ # Register SDK plugin
2044
+ register_agents_sdk_plugin(client)
2045
+
2046
+ # Use manager method
2047
+ manager = client.plugin_manager("incidents")
2048
+ result = await manager.unlink_work_issue(incident_id)
2049
+
2050
+ return format_success(result)
2051
+
2052
+ except Exception as e:
2053
+ return format_error("UNLINK_WORK_ISSUE_ERROR", str(e), retry=False)
2054
+
2055
+ return asyncio.run(_unlink_work_issue())
2056
+
2057
+
2058
+ def incidents_list_caused_by(work_issue_id: str) -> str:
2059
+ """
2060
+ List all incidents caused by a work issue.
2061
+
2062
+ Args:
2063
+ work_issue_id: Work issue page ID
2064
+
2065
+ Example:
2066
+ $ notion incidents list-caused-by issue_456
2067
+ """
2068
+ async def _list_caused_by() -> str:
2069
+ try:
2070
+ client = get_client()
2071
+
2072
+ # Register SDK plugin
2073
+ register_agents_sdk_plugin(client)
2074
+
2075
+ # Use manager method
2076
+ manager = client.plugin_manager("incidents")
2077
+ incidents = await manager.list_caused_by(work_issue_id)
2078
+
2079
+ return format_success({
2080
+ "incidents": [
2081
+ {
2082
+ "id": incident.id,
2083
+ "title": incident.title,
2084
+ "severity": incident.severity,
2085
+ "status": incident.status,
2086
+ }
2087
+ for incident in incidents
2088
+ ],
2089
+ "total": len(incidents)
2090
+ })
2091
+
2092
+ except Exception as e:
2093
+ return format_error("LIST_CAUSED_BY_ERROR", str(e), retry=False)
2094
+
2095
+ return asyncio.run(_list_caused_by())
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: better-notion
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: A high-level Python SDK for the Notion API with developer experience in mind.
5
5
  Project-URL: Homepage, https://github.com/nesalia-inc/better-notion
6
6
  Project-URL: Documentation, https://github.com/nesalia-inc/better-notion#readme
@@ -112,13 +112,13 @@ better_notion/plugins/base.py,sha256=3h9jOZzS--UqmVW3RREtcQ2h1GTWWPUryTencsJKhTM
112
112
  better_notion/plugins/loader.py,sha256=zCWsMdJyvZs1IHFm0zjEiqm_l_5jB1Uw4x30Kq8rLS4,9527
113
113
  better_notion/plugins/state.py,sha256=jH_tZWvC35hqLO4qwl2Kwq9ziWVavwCEUcCqy3s5wMY,3780
114
114
  better_notion/plugins/official/__init__.py,sha256=rPg5vdk1cEANVstMPzxcWmImtsOpdSR40JSml7h1uUk,426
115
- better_notion/plugins/official/agents.py,sha256=NZ50Fiw-x7JIgrF56QHjVPzGLDBpnpJHc834vFYH8Fo,51608
116
- better_notion/plugins/official/agents_cli.py,sha256=ybP13cfgnujPixiqiuKx8ejrzZWoWkHx8LW5dBQmXoM,52404
115
+ better_notion/plugins/official/agents.py,sha256=usvB2dH1oQ_BUePg2D1nSlHUvk58K3Eq4cdbFG5Fb7Q,54191
116
+ better_notion/plugins/official/agents_cli.py,sha256=M7bdEPzxpv1_EuC5rX_tlz9MZ3XYmnecp5EzLOBktrg,62551
117
117
  better_notion/plugins/official/agents_schema.py,sha256=NQRAJFoBAXRuxB9_9Eaf-4tVth-1OZh7GjmN56Yp9lA,39867
118
118
  better_notion/plugins/official/productivity.py,sha256=_-whP4pYA4HufE1aUFbIdhrjU-O9njI7xUO_Id2M1J8,8726
119
119
  better_notion/plugins/official/agents_sdk/__init__.py,sha256=luQBzZLsJ7fC5U0jFu8dfzMviiXj2SBZXcTohM53wkQ,725
120
- better_notion/plugins/official/agents_sdk/managers.py,sha256=I1SuF-iEQPcRbivVtGcLfowFlGdwsAdoFKgfM7f-yc4,30824
121
- better_notion/plugins/official/agents_sdk/models.py,sha256=Di3K1o5HeaEj8vGd--7mPb53bgOXf9_g3QAZ5I7QYF0,83156
120
+ better_notion/plugins/official/agents_sdk/managers.py,sha256=A1SQlozecnYrX1HLXxNWZwN0aJXoyrHIr9D1R-Jf3ZQ,40999
121
+ better_notion/plugins/official/agents_sdk/models.py,sha256=vBSqf-9Q-gX14Z8779Nacr8yTI3WMi5cLtkJjONiPoY,91967
122
122
  better_notion/plugins/official/agents_sdk/plugin.py,sha256=bs9O8Unv6SARGj4lBU5Gj9HGbLTUNqTacJ3RLUhdbI4,4479
123
123
  better_notion/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
124
124
  better_notion/utils/helpers.py,sha256=HgFuUQlG_HzBOB0z2GA9RxPLoXgwRc0DIxa9Fg6C-Jk,2337
@@ -133,8 +133,8 @@ better_notion/utils/agents/rbac.py,sha256=8ZA8Y7wbOiVZDbpjpH7iC35SZrZ0jl4fcJ3xWC
133
133
  better_notion/utils/agents/schemas.py,sha256=eHfGhY90FAPXA3E8qE6gP75dgNzn-9z5Ju1FMwBKnQQ,22120
134
134
  better_notion/utils/agents/state_machine.py,sha256=xUBEeDTbU1Xq-rsRo2sbr6AUYpYrV9DTHOtZT2cWES8,6699
135
135
  better_notion/utils/agents/workspace.py,sha256=Uy8bqLsT_VFGYAPoiQJNuCvGdjMceaSiVGbR9saMpoU,16558
136
- better_notion-2.0.0.dist-info/METADATA,sha256=TmBNSZyQKCNMhe2OZ07huw3UTg66yp8cXGOzMExSN1I,11096
137
- better_notion-2.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
138
- better_notion-2.0.0.dist-info/entry_points.txt,sha256=D0bUcP7Z00Zyjxw7r2p29T95UrwioDO0aGDoHe9I6fo,55
139
- better_notion-2.0.0.dist-info/licenses/LICENSE,sha256=BAdN3JpgMY_y_fWqZSCFSvSbC2mTHP-BKDAzF5FXQAI,1069
140
- better_notion-2.0.0.dist-info/RECORD,,
136
+ better_notion-2.1.0.dist-info/METADATA,sha256=CGwOskvV6KSl4YON4CUNgzo3W0TWLWfJ83Vd3aRa0Wk,11096
137
+ better_notion-2.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
138
+ better_notion-2.1.0.dist-info/entry_points.txt,sha256=D0bUcP7Z00Zyjxw7r2p29T95UrwioDO0aGDoHe9I6fo,55
139
+ better_notion-2.1.0.dist-info/licenses/LICENSE,sha256=BAdN3JpgMY_y_fWqZSCFSvSbC2mTHP-BKDAzF5FXQAI,1069
140
+ better_notion-2.1.0.dist-info/RECORD,,