better-notion 1.0.0__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,973 @@
1
+ """Managers for workflow entities.
2
+
3
+ These managers provide convenience methods for working with workflow
4
+ entities through the client.plugin_manager() interface.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ if TYPE_CHECKING:
12
+ from better_notion._sdk.client import NotionClient
13
+
14
+
15
+ class OrganizationManager:
16
+ """
17
+ Manager for Organization entities.
18
+
19
+ Provides convenience methods for working with organizations.
20
+
21
+ Example:
22
+ >>> manager = client.plugin_manager("organizations")
23
+ >>> orgs = await manager.list()
24
+ >>> org = await manager.get("org_id")
25
+ """
26
+
27
+ def __init__(self, client: "NotionClient") -> None:
28
+ """Initialize organization manager.
29
+
30
+ Args:
31
+ client: NotionClient instance
32
+ """
33
+ self._client = client
34
+
35
+ async def list(self) -> list:
36
+ """
37
+ List all organizations.
38
+
39
+ Returns:
40
+ List of Organization instances
41
+ """
42
+ from better_notion.plugins.official.agents_sdk.models import Organization
43
+
44
+ # Get database ID from workspace config
45
+ database_id = self._get_database_id("Organizations")
46
+ if not database_id:
47
+ return []
48
+
49
+ # Query all pages
50
+ response = await self._client._api.databases.query(database_id=database_id)
51
+
52
+ return [
53
+ Organization(self._client, page_data)
54
+ for page_data in response.get("results", [])
55
+ ]
56
+
57
+ async def get(self, org_id: str) -> Any:
58
+ """
59
+ Get an organization by ID.
60
+
61
+ Args:
62
+ org_id: Organization page ID
63
+
64
+ Returns:
65
+ Organization instance
66
+ """
67
+ from better_notion.plugins.official.agents_sdk.models import Organization
68
+
69
+ return await Organization.get(org_id, client=self._client)
70
+
71
+ async def create(
72
+ self,
73
+ name: str,
74
+ slug: str | None = None,
75
+ description: str | None = None,
76
+ repository_url: str | None = None,
77
+ status: str = "Active",
78
+ ) -> Any:
79
+ """
80
+ Create a new organization.
81
+
82
+ Args:
83
+ name: Organization name
84
+ slug: URL-safe identifier
85
+ description: Organization description
86
+ repository_url: Code repository URL
87
+ status: Organization status
88
+
89
+ Returns:
90
+ Created Organization instance
91
+ """
92
+ from better_notion.plugins.official.agents_sdk.models import Organization
93
+
94
+ database_id = self._get_database_id("Organizations")
95
+ if not database_id:
96
+ raise ValueError("Organizations database ID not found in workspace config")
97
+
98
+ return await Organization.create(
99
+ client=self._client,
100
+ database_id=database_id,
101
+ name=name,
102
+ slug=slug,
103
+ description=description,
104
+ repository_url=repository_url,
105
+ status=status,
106
+ )
107
+
108
+ def _get_database_id(self, name: str) -> str | None:
109
+ """Get database ID from workspace config."""
110
+ return getattr(self._client, "_workspace_config", {}).get(name)
111
+
112
+
113
+ class ProjectManager:
114
+ """
115
+ Manager for Project entities.
116
+
117
+ Example:
118
+ >>> manager = client.plugin_manager("projects")
119
+ >>> projects = await manager.list()
120
+ >>> project = await manager.get("project_id")
121
+ """
122
+
123
+ def __init__(self, client: "NotionClient") -> None:
124
+ """Initialize project manager."""
125
+ self._client = client
126
+
127
+ async def list(self, organization_id: str | None = None) -> list:
128
+ """
129
+ List all projects, optionally filtered by organization.
130
+
131
+ Args:
132
+ organization_id: Filter by organization ID (optional)
133
+
134
+ Returns:
135
+ List of Project instances
136
+ """
137
+ from better_notion.plugins.official.agents_sdk.models import Project
138
+
139
+ database_id = self._get_database_id("Projects")
140
+ if not database_id:
141
+ return []
142
+
143
+ # Build filter
144
+ filter_dict: dict[str, Any] = {}
145
+ if organization_id:
146
+ filter_dict = {
147
+ "property": "Organization",
148
+ "relation": {"contains": organization_id},
149
+ }
150
+
151
+ # Query pages
152
+ response = await self._client._api.databases.query(
153
+ database_id=database_id,
154
+ filter=filter_dict if filter_dict else None,
155
+ )
156
+
157
+ return [
158
+ Project(self._client, page_data)
159
+ for page_data in response.get("results", [])
160
+ ]
161
+
162
+ async def get(self, project_id: str) -> Any:
163
+ """Get a project by ID."""
164
+ from better_notion.plugins.official.agents_sdk.models import Project
165
+
166
+ return await Project.get(project_id, client=self._client)
167
+
168
+ async def create(
169
+ self,
170
+ name: str,
171
+ organization_id: str,
172
+ slug: str | None = None,
173
+ description: str | None = None,
174
+ repository: str | None = None,
175
+ status: str = "Active",
176
+ tech_stack: list[str] | None = None,
177
+ role: str = "Developer",
178
+ ) -> Any:
179
+ """Create a new project."""
180
+ from better_notion.plugins.official.agents_sdk.models import Project
181
+
182
+ database_id = self._get_database_id("Projects")
183
+ if not database_id:
184
+ raise ValueError("Projects database ID not found in workspace config")
185
+
186
+ return await Project.create(
187
+ client=self._client,
188
+ database_id=database_id,
189
+ name=name,
190
+ organization_id=organization_id,
191
+ slug=slug,
192
+ description=description,
193
+ repository=repository,
194
+ status=status,
195
+ tech_stack=tech_stack,
196
+ role=role,
197
+ )
198
+
199
+ def _get_database_id(self, name: str) -> str | None:
200
+ """Get database ID from workspace config."""
201
+ return getattr(self._client, "_workspace_config", {}).get(name)
202
+
203
+
204
+ class VersionManager:
205
+ """
206
+ Manager for Version entities.
207
+
208
+ Example:
209
+ >>> manager = client.plugin_manager("versions")
210
+ >>> versions = await manager.list()
211
+ >>> version = await manager.get("version_id")
212
+ """
213
+
214
+ def __init__(self, client: "NotionClient") -> None:
215
+ """Initialize version manager."""
216
+ self._client = client
217
+
218
+ async def list(self, project_id: str | None = None) -> list:
219
+ """
220
+ List all versions, optionally filtered by project.
221
+
222
+ Args:
223
+ project_id: Filter by project ID (optional)
224
+
225
+ Returns:
226
+ List of Version instances
227
+ """
228
+ from better_notion.plugins.official.agents_sdk.models import Version
229
+
230
+ database_id = self._get_database_id("Versions")
231
+ if not database_id:
232
+ return []
233
+
234
+ # Build filter
235
+ filter_dict: dict[str, Any] = {}
236
+ if project_id:
237
+ filter_dict = {
238
+ "property": "Project",
239
+ "relation": {"contains": project_id},
240
+ }
241
+
242
+ # Query pages
243
+ response = await self._client._api.databases.query(
244
+ database_id=database_id,
245
+ filter=filter_dict if filter_dict else None,
246
+ )
247
+
248
+ return [
249
+ Version(self._client, page_data)
250
+ for page_data in response.get("results", [])
251
+ ]
252
+
253
+ async def get(self, version_id: str) -> Any:
254
+ """Get a version by ID."""
255
+ from better_notion.plugins.official.agents_sdk.models import Version
256
+
257
+ return await Version.get(version_id, client=self._client)
258
+
259
+ async def create(
260
+ self,
261
+ name: str,
262
+ project_id: str,
263
+ status: str = "Planning",
264
+ version_type: str = "Minor",
265
+ branch_name: str | None = None,
266
+ progress: int = 0,
267
+ ) -> Any:
268
+ """Create a new version."""
269
+ from better_notion.plugins.official.agents_sdk.models import Version
270
+
271
+ database_id = self._get_database_id("Versions")
272
+ if not database_id:
273
+ raise ValueError("Versions database ID not found in workspace config")
274
+
275
+ return await Version.create(
276
+ client=self._client,
277
+ database_id=database_id,
278
+ name=name,
279
+ project_id=project_id,
280
+ status=status,
281
+ version_type=version_type,
282
+ branch_name=branch_name,
283
+ progress=progress,
284
+ )
285
+
286
+ def _get_database_id(self, name: str) -> str | None:
287
+ """Get database ID from workspace config."""
288
+ return getattr(self._client, "_workspace_config", {}).get(name)
289
+
290
+
291
+ class TaskManager:
292
+ """
293
+ Manager for Task entities.
294
+
295
+ Provides task discovery and workflow management methods.
296
+
297
+ Example:
298
+ >>> manager = client.plugin_manager("tasks")
299
+ >>> tasks = await manager.list()
300
+ >>> task = await manager.next()
301
+ >>> await task.claim()
302
+ """
303
+
304
+ def __init__(self, client: "NotionClient") -> None:
305
+ """Initialize task manager."""
306
+ self._client = client
307
+
308
+ async def list(
309
+ self,
310
+ version_id: str | None = None,
311
+ status: str | None = None,
312
+ ) -> list:
313
+ """
314
+ List all tasks, optionally filtered.
315
+
316
+ Args:
317
+ version_id: Filter by version ID (optional)
318
+ status: Filter by status (optional)
319
+
320
+ Returns:
321
+ List of Task instances
322
+ """
323
+ from better_notion.plugins.official.agents_sdk.models import Task
324
+
325
+ database_id = self._get_database_id("Tasks")
326
+ if not database_id:
327
+ return []
328
+
329
+ # Build filter
330
+ filters: list[dict[str, Any]] = []
331
+
332
+ if version_id:
333
+ filters.append({
334
+ "property": "Version",
335
+ "relation": {"contains": version_id},
336
+ })
337
+
338
+ if status:
339
+ filters.append({
340
+ "property": "Status",
341
+ "select": {"equals": status},
342
+ })
343
+
344
+ # Query pages
345
+ response = await self._client._api.databases.query(
346
+ database_id=database_id,
347
+ filter={"and": filters} if len(filters) > 1 else (filters[0] if filters else None),
348
+ )
349
+
350
+ return [
351
+ Task(self._client, page_data)
352
+ for page_data in response.get("results", [])
353
+ ]
354
+
355
+ async def get(self, task_id: str) -> Any:
356
+ """Get a task by ID."""
357
+ from better_notion.plugins.official.agents_sdk.models import Task
358
+
359
+ return await Task.get(task_id, client=self._client)
360
+
361
+ async def create(
362
+ self,
363
+ title: str,
364
+ version_id: str,
365
+ status: str = "Backlog",
366
+ task_type: str = "New Feature",
367
+ priority: str = "Medium",
368
+ dependency_ids: list[str] | None = None,
369
+ estimated_hours: int | None = None,
370
+ ) -> Any:
371
+ """Create a new task."""
372
+ from better_notion.plugins.official.agents_sdk.models import Task
373
+
374
+ database_id = self._get_database_id("Tasks")
375
+ if not database_id:
376
+ raise ValueError("Tasks database ID not found in workspace config")
377
+
378
+ return await Task.create(
379
+ client=self._client,
380
+ database_id=database_id,
381
+ title=title,
382
+ version_id=version_id,
383
+ status=status,
384
+ task_type=task_type,
385
+ priority=priority,
386
+ dependency_ids=dependency_ids,
387
+ estimated_hours=estimated_hours,
388
+ )
389
+
390
+ async def next(self, project_id: str | None = None) -> Any | None:
391
+ """
392
+ Find the next available task to work on.
393
+
394
+ Tasks are considered available if:
395
+ - Status is Backlog or Claimed
396
+ - All dependencies are completed
397
+
398
+ Args:
399
+ project_id: Filter by project ID (optional)
400
+
401
+ Returns:
402
+ Task instance or None if no tasks available
403
+ """
404
+ from better_notion.plugins.official.agents_sdk.models import Task
405
+
406
+ database_id = self._get_database_id("Tasks")
407
+ if not database_id:
408
+ return None
409
+
410
+ # Filter for backlog/claimed tasks
411
+ response = await self._client._api.databases.query(
412
+ database_id=database_id,
413
+ filter={
414
+ "or": [
415
+ {"property": "Status", "select": {"equals": "Backlog"}},
416
+ {"property": "Status", "select": {"equals": "Claimed"}},
417
+ ]
418
+ },
419
+ )
420
+
421
+ # Check each task for completed dependencies
422
+ for page_data in response.get("results", []):
423
+ task = Task(self._client, page_data)
424
+
425
+ # Filter by project if specified
426
+ if project_id:
427
+ version = await task.version()
428
+ if version:
429
+ project = await version.project()
430
+ if project and project.id != project_id:
431
+ continue
432
+
433
+ # Check if can start
434
+ if await task.can_start():
435
+ return task
436
+
437
+ return None
438
+
439
+ async def find_ready(self, version_id: str | None = None) -> list:
440
+ """
441
+ Find all tasks that are ready to start (dependencies completed).
442
+
443
+ Args:
444
+ version_id: Filter by version ID (optional)
445
+
446
+ Returns:
447
+ List of Task instances ready to start
448
+ """
449
+ ready_tasks = []
450
+
451
+ database_id = self._get_database_id("Tasks")
452
+ if not database_id:
453
+ return ready_tasks
454
+
455
+ # Get all backlog/claimed tasks
456
+ tasks = await self.list(status=None)
457
+
458
+ for task in tasks:
459
+ # Filter by version if specified
460
+ if version_id and task.version_id != version_id:
461
+ continue
462
+
463
+ # Check status and dependencies
464
+ if task.status in ("Backlog", "Claimed") and await task.can_start():
465
+ ready_tasks.append(task)
466
+
467
+ return ready_tasks
468
+
469
+ async def find_blocked(self, version_id: str | None = None) -> list:
470
+ """
471
+ Find all tasks that are blocked by incomplete dependencies.
472
+
473
+ Args:
474
+ version_id: Filter by version ID (optional)
475
+
476
+ Returns:
477
+ List of Task instances that are blocked
478
+ """
479
+ blocked_tasks = []
480
+
481
+ database_id = self._get_database_id("Tasks")
482
+ if not database_id:
483
+ return blocked_tasks
484
+
485
+ # Get all backlog/claimed/in-progress tasks
486
+ tasks = await self.list(status=None)
487
+
488
+ for task in tasks:
489
+ # Filter by version if specified
490
+ if version_id and task.version_id != version_id:
491
+ continue
492
+
493
+ # Check status and dependencies
494
+ if task.status in ("Backlog", "Claimed", "In Progress"):
495
+ if not await task.can_start():
496
+ blocked_tasks.append(task)
497
+
498
+ return blocked_tasks
499
+
500
+ def _get_database_id(self, name: str) -> str | None:
501
+ """Get database ID from workspace config."""
502
+ return getattr(self._client, "_workspace_config", {}).get(name)
503
+
504
+
505
+ class IdeaManager:
506
+ """
507
+ Manager for Idea entities.
508
+
509
+ Provides convenience methods for working with Ideas,
510
+ including filtering by project, category, and status.
511
+ """
512
+
513
+ def __init__(self, client: "NotionClient") -> None:
514
+ """Initialize manager with NotionClient instance."""
515
+ self._client = client
516
+
517
+ async def list(
518
+ self,
519
+ project_id: str | None = None,
520
+ category: str | None = None,
521
+ status: str | None = None,
522
+ effort_estimate: str | None = None,
523
+ ) -> list:
524
+ """
525
+ List Ideas with optional filtering.
526
+
527
+ Args:
528
+ project_id: Filter by project ID
529
+ category: Filter by category (enhancement, feature, bugfix, optimization, research)
530
+ status: Filter by status
531
+ effort_estimate: Filter by effort estimate
532
+
533
+ Returns:
534
+ List of Idea instances
535
+ """
536
+ from better_notion.plugins.official.agents_sdk.models import Idea
537
+
538
+ database_id = self._get_database_id("Ideas")
539
+ if not database_id:
540
+ return []
541
+
542
+ # Build filters
543
+ filters = []
544
+
545
+ if project_id:
546
+ filters.append({
547
+ "property": "project_id",
548
+ "relation": {"contains": project_id}
549
+ })
550
+
551
+ if category:
552
+ filters.append({
553
+ "property": "category",
554
+ "select": {"equals": category}
555
+ })
556
+
557
+ if status:
558
+ filters.append({
559
+ "property": "status",
560
+ "select": {"equals": status}
561
+ })
562
+
563
+ if effort_estimate:
564
+ filters.append({
565
+ "property": "effort_estimate",
566
+ "select": {"equals": effort_estimate}
567
+ })
568
+
569
+ # Query database
570
+ query = {}
571
+ if filters:
572
+ if len(filters) == 1:
573
+ query["filter"] = filters[0]
574
+ else:
575
+ query["filter"] = {
576
+ "and": filters
577
+ }
578
+
579
+ response = await self._client._api.request(
580
+ method="POST",
581
+ path=f"databases/{database_id}/query",
582
+ json=query,
583
+ )
584
+
585
+ # Create Idea instances
586
+ ideas = []
587
+ for page_data in response.get("results", []):
588
+ idea = Idea(data=page_data, client=self._client, cache=self._client._plugin_caches.get("ideas"))
589
+ ideas.append(idea)
590
+
591
+ return ideas
592
+
593
+ async def review_batch(self, count: int = 10) -> list:
594
+ """
595
+ Get a batch of ideas for review, prioritized by effort.
596
+
597
+ Args:
598
+ count: Maximum number of ideas to return
599
+
600
+ Returns:
601
+ List of Idea instances ready for review
602
+ """
603
+ ideas = await self.list(status="Proposed")
604
+
605
+ # Sort by effort (XS < S < M < L < XL)
606
+ effort_order = {"XS": 0, "S": 1, "M": 2, "L": 3, "XL": 4}
607
+ ideas.sort(key=lambda i: effort_order.get(i.effort_estimate or "M", 2))
608
+
609
+ return ideas[:count]
610
+
611
+ async def get_accepted_without_tasks(self) -> list:
612
+ """
613
+ Get all accepted ideas that don't have related tasks yet.
614
+
615
+ Returns:
616
+ List of Idea instances that should have tasks created
617
+ """
618
+ ideas = await self.list(status="Accepted")
619
+ return [idea for idea in ideas if not idea.related_task_id]
620
+
621
+ def _get_database_id(self, name: str) -> str | None:
622
+ """Get database ID from workspace config."""
623
+ return getattr(self._client, "_workspace_config", {}).get(name)
624
+
625
+
626
+ class WorkIssueManager:
627
+ """
628
+ Manager for Work Issue entities.
629
+
630
+ Provides convenience methods for working with work issues,
631
+ including finding blockers and creating from exceptions.
632
+ """
633
+
634
+ def __init__(self, client: "NotionClient") -> None:
635
+ """Initialize manager with NotionClient instance."""
636
+ self._client = client
637
+
638
+ async def list(
639
+ self,
640
+ project_id: str | None = None,
641
+ task_id: str | None = None,
642
+ type_: str | None = None,
643
+ severity: str | None = None,
644
+ status: str | None = None,
645
+ ) -> list:
646
+ """
647
+ List Work Issues with optional filtering.
648
+
649
+ Args:
650
+ project_id: Filter by project ID
651
+ task_id: Filter by task ID
652
+ type_: Filter by type
653
+ severity: Filter by severity
654
+ status: Filter by status
655
+
656
+ Returns:
657
+ List of WorkIssue instances
658
+ """
659
+ from better_notion.plugins.official.agents_sdk.models import WorkIssue
660
+
661
+ database_id = self._get_database_id("Work Issues")
662
+ if not database_id:
663
+ return []
664
+
665
+ # Build filters
666
+ filters = []
667
+
668
+ if project_id:
669
+ filters.append({
670
+ "property": "project_id",
671
+ "relation": {"contains": project_id}
672
+ })
673
+
674
+ if task_id:
675
+ filters.append({
676
+ "property": "task_id",
677
+ "relation": {"contains": task_id}
678
+ })
679
+
680
+ if type_:
681
+ filters.append({
682
+ "property": "type",
683
+ "select": {"equals": type_}
684
+ })
685
+
686
+ if severity:
687
+ filters.append({
688
+ "property": "severity",
689
+ "select": {"equals": severity}
690
+ })
691
+
692
+ if status:
693
+ filters.append({
694
+ "property": "status",
695
+ "select": {"equals": status}
696
+ })
697
+
698
+ # Query database
699
+ query = {}
700
+ if filters:
701
+ if len(filters) == 1:
702
+ query["filter"] = filters[0]
703
+ else:
704
+ query["filter"] = {
705
+ "and": filters
706
+ }
707
+
708
+ response = await self._client._api.request(
709
+ method="POST",
710
+ path=f"databases/{database_id}/query",
711
+ json=query,
712
+ )
713
+
714
+ # Create WorkIssue instances
715
+ issues = []
716
+ for page_data in response.get("results", []):
717
+ issue = WorkIssue(data=page_data, client=self._client, cache=self._client._plugin_caches.get("work_issues"))
718
+ issues.append(issue)
719
+
720
+ return issues
721
+
722
+ async def find_blockers(self, project_id: str) -> list:
723
+ """
724
+ Find all open work issues that are blocking development.
725
+
726
+ Args:
727
+ project_id: Project ID to search within
728
+
729
+ Returns:
730
+ List of WorkIssue instances with High/Critical severity
731
+ """
732
+ issues = await self.list(
733
+ project_id=project_id,
734
+ status="Open"
735
+ )
736
+
737
+ return [issue for issue in issues if issue.severity in ("High", "Critical")]
738
+
739
+ async def create_from_exception(
740
+ self,
741
+ title: str,
742
+ exception: Exception,
743
+ project_id: str,
744
+ task_id: str | None = None,
745
+ context: str | None = None,
746
+ ) -> "WorkIssue":
747
+ """
748
+ Create a work issue from an exception.
749
+
750
+ Args:
751
+ title: Issue title
752
+ exception: Exception object
753
+ project_id: Project ID
754
+ task_id: Related task ID (optional)
755
+ context: Additional context
756
+
757
+ Returns:
758
+ Created WorkIssue instance
759
+ """
760
+ from better_notion.plugins.official.agents_sdk.models import WorkIssue
761
+
762
+ database_id = self._get_database_id("Work Issues")
763
+ if not database_id:
764
+ raise ValueError("Work Issues database not configured")
765
+
766
+ # Create issue in Notion
767
+ properties = {
768
+ "title": {"title": [{"text": {"content": title}}]},
769
+ "project_id": {"relation": [{"id": project_id}]},
770
+ "type": {"select": {"name": "Technical"}},
771
+ "severity": {"select": {"name": "Medium"}},
772
+ "status": {"select": {"name": "Open"}},
773
+ "description": {
774
+ "rich_text": [{
775
+ "text": {
776
+ "content": f"Exception: {type(exception).__name__}: {str(exception)}"
777
+ }
778
+ }]
779
+ },
780
+ }
781
+
782
+ if task_id:
783
+ properties["task_id"] = {"relation": [{"id": task_id}]}
784
+
785
+ if context:
786
+ properties["context"] = {
787
+ "rich_text": [{"text": {"content": context}}]
788
+ }
789
+
790
+ response = await self._client._api.request(
791
+ method="POST",
792
+ path=f"databases/{database_id}",
793
+ json={"properties": properties},
794
+ )
795
+
796
+ # Create WorkIssue instance
797
+ issue = WorkIssue(
798
+ data=response,
799
+ client=self._client,
800
+ cache=self._client._plugin_caches.get("work_issues"),
801
+ )
802
+
803
+ return issue
804
+
805
+ def _get_database_id(self, name: str) -> str | None:
806
+ """Get database ID from workspace config."""
807
+ return getattr(self._client, "_workspace_config", {}).get(name)
808
+
809
+
810
+ class IncidentManager:
811
+ """
812
+ Manager for Incident entities.
813
+
814
+ Provides convenience methods for working with incidents,
815
+ including finding SLA violations and calculating MTTR.
816
+ """
817
+
818
+ def __init__(self, client: "NotionClient") -> None:
819
+ """Initialize manager with NotionClient instance."""
820
+ self._client = client
821
+
822
+ async def list(
823
+ self,
824
+ project_id: str | None = None,
825
+ version_id: str | None = None,
826
+ severity: str | None = None,
827
+ status: str | None = None,
828
+ ) -> list:
829
+ """
830
+ List Incidents with optional filtering.
831
+
832
+ Args:
833
+ project_id: Filter by project ID
834
+ version_id: Filter by affected version ID
835
+ severity: Filter by severity
836
+ status: Filter by status
837
+
838
+ Returns:
839
+ List of Incident instances
840
+ """
841
+ from better_notion.plugins.official.agents_sdk.models import Incident
842
+
843
+ database_id = self._get_database_id("Incidents")
844
+ if not database_id:
845
+ return []
846
+
847
+ # Build filters
848
+ filters = []
849
+
850
+ if project_id:
851
+ filters.append({
852
+ "property": "project_id",
853
+ "relation": {"contains": project_id}
854
+ })
855
+
856
+ if version_id:
857
+ filters.append({
858
+ "property": "affected_version_id",
859
+ "relation": {"contains": version_id}
860
+ })
861
+
862
+ if severity:
863
+ filters.append({
864
+ "property": "severity",
865
+ "select": {"equals": severity}
866
+ })
867
+
868
+ if status:
869
+ filters.append({
870
+ "property": "status",
871
+ "select": {"equals": status}
872
+ })
873
+
874
+ # Query database
875
+ query = {}
876
+ if filters:
877
+ if len(filters) == 1:
878
+ query["filter"] = filters[0]
879
+ else:
880
+ query["filter"] = {
881
+ "and": filters
882
+ }
883
+
884
+ response = await self._client._api.request(
885
+ method="POST",
886
+ path=f"databases/{database_id}/query",
887
+ json=query,
888
+ )
889
+
890
+ # Create Incident instances
891
+ incidents = []
892
+ for page_data in response.get("results", []):
893
+ incident = Incident(data=page_data, client=self._client, cache=self._client._plugin_caches.get("incidents"))
894
+ incidents.append(incident)
895
+
896
+ return incidents
897
+
898
+ async def find_sla_violations(self) -> list:
899
+ """
900
+ Find all incidents that violated SLA.
901
+
902
+ SLA: Critical incidents should be resolved within 4 hours.
903
+
904
+ Returns:
905
+ List of Incident instances with SLA violations
906
+ """
907
+ from datetime import timedelta
908
+
909
+ incidents = await self.list()
910
+
911
+ violations = []
912
+ for incident in incidents:
913
+ if incident.severity == "Critical" and incident.resolved_date:
914
+ # SLA: 4 hours for Critical
915
+ sla_hours = 4
916
+ sla_duration = timedelta(hours=sla_hours)
917
+
918
+ if incident.discovery_date:
919
+ resolution_time = incident.resolved_date - incident.discovery_date
920
+ if resolution_time > sla_duration:
921
+ violations.append(incident)
922
+
923
+ return violations
924
+
925
+ async def calculate_mttr(
926
+ self, project_id: str | None = None, within_days: int = 30
927
+ ) -> dict[str, float]:
928
+ """
929
+ Calculate Mean Time To Resolve (MTTR) for incidents.
930
+
931
+ Args:
932
+ project_id: Filter by project ID (optional)
933
+ within_days: Only consider incidents from last N days
934
+
935
+ Returns:
936
+ Dictionary mapping severity to MTTR in hours
937
+ """
938
+ from datetime import datetime, timedelta, timezone
939
+
940
+ cutoff_date = datetime.now(timezone.utc) - timedelta(days=within_days)
941
+ incidents = await self.list(project_id=project_id)
942
+
943
+ # Filter by date and resolved status
944
+ resolved_incidents = [
945
+ i for i in incidents
946
+ if i.status == "Resolved"
947
+ and i.discovery_date
948
+ and i.resolved_date
949
+ and i.discovery_date >= cutoff_date
950
+ ]
951
+
952
+ # Group by severity
953
+ by_severity = {}
954
+ for incident in resolved_incidents:
955
+ severity = incident.severity or "Unknown"
956
+ if severity not in by_severity:
957
+ by_severity[severity] = []
958
+
959
+ # Calculate resolution time in hours
960
+ resolution_time = incident.resolved_date - incident.discovery_date
961
+ hours = resolution_time.total_seconds() / 3600
962
+ by_severity[severity].append(hours)
963
+
964
+ # Calculate averages
965
+ mttr = {}
966
+ for severity, times in by_severity.items():
967
+ mttr[severity] = sum(times) / len(times) if times else 0.0
968
+
969
+ return mttr
970
+
971
+ def _get_database_id(self, name: str) -> str | None:
972
+ """Get database ID from workspace config."""
973
+ return getattr(self._client, "_workspace_config", {}).get(name)