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.
- better_notion/_sdk/client.py +106 -3
- better_notion/_sdk/plugins.py +180 -0
- better_notion/plugins/base.py +99 -1
- better_notion/plugins/loader.py +51 -1
- better_notion/plugins/official/agents.py +150 -5
- better_notion/plugins/official/agents_cli.py +1767 -0
- better_notion/plugins/official/agents_sdk/__init__.py +30 -0
- better_notion/plugins/official/agents_sdk/managers.py +973 -0
- better_notion/plugins/official/agents_sdk/models.py +2256 -0
- better_notion/plugins/official/agents_sdk/plugin.py +146 -0
- {better_notion-1.0.0.dist-info → better_notion-1.1.0.dist-info}/METADATA +2 -2
- {better_notion-1.0.0.dist-info → better_notion-1.1.0.dist-info}/RECORD +15 -9
- {better_notion-1.0.0.dist-info → better_notion-1.1.0.dist-info}/WHEEL +0 -0
- {better_notion-1.0.0.dist-info → better_notion-1.1.0.dist-info}/entry_points.txt +0 -0
- {better_notion-1.0.0.dist-info → better_notion-1.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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)
|