better-notion 2.1.9__py3-none-any.whl → 2.3.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.
@@ -341,10 +341,19 @@ class TaskManager:
341
341
  "select": {"equals": status},
342
342
  })
343
343
 
344
+ # Build query payload
345
+ query_payload = {}
346
+ if filters:
347
+ # Apply filter only if there are filters
348
+ if len(filters) == 1:
349
+ query_payload["filter"] = filters[0]
350
+ else:
351
+ query_payload["filter"] = {"and": filters}
352
+
344
353
  # Query pages
345
354
  response = await self._client._api.databases.query(
346
355
  database_id=database_id,
347
- filter={"and": filters} if len(filters) > 1 else (filters[0] if filters else None),
356
+ **query_payload
348
357
  )
349
358
 
350
359
  return [
@@ -760,6 +769,235 @@ class TaskManager:
760
769
  """
761
770
  return await self.find_ready(version_id)
762
771
 
772
+ async def search(
773
+ self,
774
+ query: str,
775
+ status: str | None = None,
776
+ priority: str | None = None,
777
+ assignee: str | None = None,
778
+ project_id: str | None = None,
779
+ version_id: str | None = None,
780
+ limit: int = 50,
781
+ ) -> list[dict]:
782
+ """Search tasks by text query with optional filters.
783
+
784
+ Args:
785
+ query: Text search query
786
+ status: Optional status filter
787
+ priority: Optional priority filter
788
+ assignee: Optional assignee filter
789
+ project_id: Optional project ID filter
790
+ version_id: Optional version ID filter
791
+ limit: Maximum results to return
792
+
793
+ Returns:
794
+ List of search result dictionaries
795
+ """
796
+ from better_notion.plugins.official.agents_sdk.search import TextSearcher
797
+
798
+ database_id = self._get_database_id("Tasks")
799
+ if not database_id:
800
+ return []
801
+
802
+ # Build filters
803
+ filters: list[dict[str, Any]] = []
804
+
805
+ if status:
806
+ filters.append({"property": "Status", "select": {"equals": status}})
807
+
808
+ if priority:
809
+ filters.append({"property": "Priority", "select": {"equals": priority}})
810
+
811
+ if assignee:
812
+ filters.append({"property": "Assignee", "select": {"equals": assignee}})
813
+
814
+ # Combine filters
815
+ filter_dict = None
816
+ if filters:
817
+ if len(filters) == 1:
818
+ filter_dict = filters[0]
819
+ else:
820
+ filter_dict = {"and": filters}
821
+
822
+ searcher = TextSearcher(self._client)
823
+ results = await searcher.search_tasks(query, database_id, filter_dict, limit)
824
+
825
+ # Filter by project/version if specified (post-filter due to relation filtering)
826
+ if project_id or version_id:
827
+ filtered_results = []
828
+ for result in results:
829
+ task = result.entity
830
+ # Filter by version
831
+ if version_id:
832
+ task_version = getattr(task, "version_id", None)
833
+ if task_version != version_id:
834
+ continue
835
+
836
+ # Filter by project
837
+ if project_id:
838
+ version = await task.version() if hasattr(task, "version") else None
839
+ if version:
840
+ project = await version.project() if hasattr(version, "project") else None
841
+ if project and project.id != project_id:
842
+ continue
843
+
844
+ filtered_results.append(result)
845
+
846
+ results = filtered_results
847
+
848
+ return [r.to_dict() for r in results]
849
+
850
+ async def pick(
851
+ self,
852
+ skills: list[str] | None = None,
853
+ max_priority: str | None = None,
854
+ exclude_patterns: list[str] | None = None,
855
+ count: int = 5,
856
+ project_id: str | None = None,
857
+ version_id: str | None = None,
858
+ ) -> list[dict]:
859
+ """Pick the best tasks for an agent to work on.
860
+
861
+ Args:
862
+ skills: List of agent skills (e.g., ["python", "database"])
863
+ max_priority: Maximum priority to consider
864
+ exclude_patterns: Regex patterns to exclude
865
+ count: Maximum number of recommendations
866
+ project_id: Optional project filter
867
+ version_id: Optional version filter
868
+
869
+ Returns:
870
+ List of task recommendation dictionaries
871
+ """
872
+ from better_notion.plugins.official.agents_sdk.agent import TaskSelector
873
+
874
+ selector = TaskSelector(self._client)
875
+ recommendations = await selector.pick_best_tasks(
876
+ skills=skills,
877
+ max_priority=max_priority,
878
+ exclude_patterns=exclude_patterns,
879
+ count=count,
880
+ project_id=project_id,
881
+ version_id=version_id,
882
+ )
883
+
884
+ return [r.to_dict() for r in recommendations]
885
+
886
+ async def suggest(
887
+ self,
888
+ unassigned: bool = False,
889
+ priority: list[str] | None = None,
890
+ count: int = 5,
891
+ ) -> list[dict]:
892
+ """Suggest tasks based on criteria.
893
+
894
+ Args:
895
+ unassigned: Only suggest unassigned tasks
896
+ priority: List of priority levels to include
897
+ count: Maximum number of suggestions
898
+
899
+ Returns:
900
+ List of task dictionaries
901
+ """
902
+ # Get candidate tasks
903
+ filters = []
904
+
905
+ if unassigned:
906
+ filters.append({"property": "Assignee", "select": {"is_empty": True}})
907
+
908
+ if priority:
909
+ or_filter = {"or": [{"property": "Priority", "select": {"equals": p}} for p in priority]}
910
+ filters.append(or_filter)
911
+
912
+ # Query with filters
913
+ database_id = self._get_database_id("Tasks")
914
+ if not database_id:
915
+ return []
916
+
917
+ query_obj: dict[str, Any] = {}
918
+ if filters:
919
+ if len(filters) == 1:
920
+ query_obj["filter"] = filters[0]
921
+ else:
922
+ query_obj["filter"] = {"and": filters}
923
+
924
+ from better_notion.plugins.official.agents_sdk.models import Task
925
+
926
+ response = await self._client._api._request(
927
+ "POST",
928
+ f"/databases/{database_id}/query",
929
+ json=query_obj,
930
+ )
931
+
932
+ tasks = [Task(self._client, page_data) for page_data in response.get("results", [])]
933
+
934
+ # Sort by priority (Critical > High > Medium > Low)
935
+ priority_order = {"Critical": 0, "High": 1, "Medium": 2, "Low": 3}
936
+ tasks.sort(key=lambda t: priority_order.get(getattr(t, "priority", "Low"), 3))
937
+
938
+ # Get top N and convert to dicts
939
+ suggestions = []
940
+ for task in tasks[:count]:
941
+ suggestions.append({
942
+ "id": task.id,
943
+ "title": task.title,
944
+ "priority": getattr(task, "priority", None),
945
+ "status": getattr(task, "status", None),
946
+ "assignee": getattr(task, "assignee", None),
947
+ "type": getattr(task, "task_type", None),
948
+ })
949
+
950
+ return suggestions
951
+
952
+ async def claim(self, task_id: str) -> dict:
953
+ """Claim a task (set status to In Progress).
954
+
955
+ Args:
956
+ task_id: Task ID to claim
957
+
958
+ Returns:
959
+ Dict with task_id and new status
960
+ """
961
+ from better_notion.plugins.official.agents_sdk.models import Task
962
+
963
+ task = await Task.get(task_id, client=self._client)
964
+ await task.update(status="In Progress")
965
+
966
+ return {
967
+ "task_id": task_id,
968
+ "status": "In Progress",
969
+ }
970
+
971
+ async def start(self, task_id: str) -> dict:
972
+ """Start a task (alias for claim).
973
+
974
+ Args:
975
+ task_id: Task ID to start
976
+
977
+ Returns:
978
+ Dict with task_id and new status
979
+ """
980
+ return await self.claim(task_id)
981
+
982
+ async def complete(self, task_id: str) -> dict:
983
+ """Complete a task (set status to Completed).
984
+
985
+ Args:
986
+ task_id: Task ID to complete
987
+
988
+ Returns:
989
+ Dict with task_id and new status
990
+ """
991
+ from better_notion.plugins.official.agents_sdk.models import Task
992
+
993
+ task = await Task.get(task_id, client=self._client)
994
+ await task.update(status="Completed")
995
+
996
+ return {
997
+ "task_id": task_id,
998
+ "status": "Completed",
999
+ }
1000
+
763
1001
  def _get_database_id(self, name: str) -> str | None:
764
1002
  """Get database ID from workspace config."""
765
1003
  return getattr(self._client, "_workspace_config", {}).get(name)
@@ -881,6 +1119,57 @@ class IdeaManager:
881
1119
  ideas = await self.list(status="Accepted")
882
1120
  return [idea for idea in ideas if not idea.related_task_id]
883
1121
 
1122
+ async def search(
1123
+ self,
1124
+ query: str,
1125
+ status: str | None = None,
1126
+ category: str | None = None,
1127
+ project_id: str | None = None,
1128
+ limit: int = 50,
1129
+ ) -> list[dict]:
1130
+ """Search ideas by text query with optional filters.
1131
+
1132
+ Args:
1133
+ query: Text search query
1134
+ status: Optional status filter
1135
+ category: Optional category filter
1136
+ project_id: Optional project ID filter
1137
+ limit: Maximum results to return
1138
+
1139
+ Returns:
1140
+ List of search result dictionaries
1141
+ """
1142
+ from better_notion.plugins.official.agents_sdk.search import TextSearcher
1143
+
1144
+ database_id = self._get_database_id("Ideas")
1145
+ if not database_id:
1146
+ return []
1147
+
1148
+ # Build filters
1149
+ filters: list[dict[str, Any]] = []
1150
+
1151
+ if status:
1152
+ filters.append({"property": "Status", "select": {"equals": status}})
1153
+
1154
+ if category:
1155
+ filters.append({"property": "Category", "select": {"equals": category}})
1156
+
1157
+ if project_id:
1158
+ filters.append({"property": "Project", "relation": {"contains": project_id}})
1159
+
1160
+ # Combine filters
1161
+ filter_dict = None
1162
+ if filters:
1163
+ if len(filters) == 1:
1164
+ filter_dict = filters[0]
1165
+ else:
1166
+ filter_dict = {"and": filters}
1167
+
1168
+ searcher = TextSearcher(self._client)
1169
+ results = await searcher.search_ideas(query, database_id, filter_dict, limit)
1170
+
1171
+ return [r.to_dict() for r in results]
1172
+
884
1173
  def _get_database_id(self, name: str) -> str | None:
885
1174
  """Get database ID from workspace config."""
886
1175
  return getattr(self._client, "_workspace_config", {}).get(name)
@@ -1096,6 +1385,62 @@ class WorkIssueManager:
1096
1385
  for page_data in response.get("results", [])
1097
1386
  ]
1098
1387
 
1388
+ async def search(
1389
+ self,
1390
+ query: str,
1391
+ status: str | None = None,
1392
+ type_: str | None = None,
1393
+ severity: str | None = None,
1394
+ project_id: str | None = None,
1395
+ limit: int = 50,
1396
+ ) -> list[dict]:
1397
+ """Search work issues by text query with optional filters.
1398
+
1399
+ Args:
1400
+ query: Text search query
1401
+ status: Optional status filter
1402
+ type_: Optional type filter
1403
+ severity: Optional severity filter
1404
+ project_id: Optional project ID filter
1405
+ limit: Maximum results to return
1406
+
1407
+ Returns:
1408
+ List of search result dictionaries
1409
+ """
1410
+ from better_notion.plugins.official.agents_sdk.search import TextSearcher
1411
+
1412
+ database_id = self._get_database_id("Work_issues")
1413
+ if not database_id:
1414
+ return []
1415
+
1416
+ # Build filters
1417
+ filters: list[dict[str, Any]] = []
1418
+
1419
+ if status:
1420
+ filters.append({"property": "Status", "select": {"equals": status}})
1421
+
1422
+ if type_:
1423
+ filters.append({"property": "Type", "select": {"equals": type_}})
1424
+
1425
+ if severity:
1426
+ filters.append({"property": "Severity", "select": {"equals": severity}})
1427
+
1428
+ if project_id:
1429
+ filters.append({"property": "Project", "relation": {"contains": project_id}})
1430
+
1431
+ # Combine filters
1432
+ filter_dict = None
1433
+ if filters:
1434
+ if len(filters) == 1:
1435
+ filter_dict = filters[0]
1436
+ else:
1437
+ filter_dict = {"and": filters}
1438
+
1439
+ searcher = TextSearcher(self._client)
1440
+ results = await searcher.search_work_issues(query, database_id, filter_dict, limit)
1441
+
1442
+ return [r.to_dict() for r in results]
1443
+
1099
1444
  def _get_database_id(self, name: str) -> str | None:
1100
1445
  """Get database ID from workspace config."""
1101
1446
  return getattr(self._client, "_workspace_config", {}).get(name)
@@ -1189,6 +1534,79 @@ class IncidentManager:
1189
1534
 
1190
1535
  return incidents
1191
1536
 
1537
+ async def search(
1538
+ self,
1539
+ query: str,
1540
+ status: str | None = None,
1541
+ severity: str | None = None,
1542
+ incident_type: str | None = None,
1543
+ project_id: str | None = None,
1544
+ limit: int = 50,
1545
+ ) -> list[dict]:
1546
+ """Search incidents by text query with optional filters.
1547
+
1548
+ Args:
1549
+ query: Text search query
1550
+ status: Optional status filter
1551
+ severity: Optional severity filter
1552
+ incident_type: Optional incident type filter
1553
+ project_id: Optional project ID filter
1554
+ limit: Maximum results to return
1555
+
1556
+ Returns:
1557
+ List of search result dictionaries
1558
+ """
1559
+ from better_notion.plugins.official.agents_sdk.search import TextSearcher
1560
+
1561
+ database_id = self._get_database_id("Incidents")
1562
+ if not database_id:
1563
+ return []
1564
+
1565
+ # Build filters
1566
+ filters: list[dict[str, Any]] = []
1567
+
1568
+ if status:
1569
+ filters.append({"property": "Status", "select": {"equals": status}})
1570
+
1571
+ if severity:
1572
+ filters.append({"property": "Severity", "select": {"equals": severity}})
1573
+
1574
+ if incident_type:
1575
+ filters.append({"property": "Type", "select": {"equals": incident_type}})
1576
+
1577
+ if project_id:
1578
+ filters.append({"property": "Project", "relation": {"contains": project_id}})
1579
+
1580
+ # Combine filters
1581
+ filter_dict = None
1582
+ if filters:
1583
+ if len(filters) == 1:
1584
+ filter_dict = filters[0]
1585
+ else:
1586
+ filter_dict = {"and": filters}
1587
+
1588
+ searcher = TextSearcher(self._client)
1589
+ results = await searcher.search_incidents(query, database_id, filter_dict, limit)
1590
+
1591
+ return [r.to_dict() for r in results]
1592
+
1593
+ async def triage(self, incident_id: str) -> dict:
1594
+ """Triage and classify an incident.
1595
+
1596
+ Args:
1597
+ incident_id: Incident ID to triage
1598
+
1599
+ Returns:
1600
+ Dictionary with triage results including severity, type, and suggested assignment
1601
+ """
1602
+ from better_notion.plugins.official.agents_sdk.agent import IncidentTriager
1603
+ from better_notion.plugins.official.agents_sdk.models import Incident
1604
+
1605
+ incident = await Incident.get(incident_id, client=self._client)
1606
+ triager = IncidentTriager(self._client)
1607
+
1608
+ return await triager.triage_incident(incident)
1609
+
1192
1610
  async def find_sla_violations(self) -> list:
1193
1611
  """
1194
1612
  Find all incidents that violated SLA.