better-notion 0.9.9__py3-none-any.whl → 1.0.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.
- better_notion/plugins/official/__init__.py +3 -1
- better_notion/plugins/official/agents.py +356 -0
- better_notion/utils/agents/__init__.py +65 -0
- better_notion/utils/agents/auth.py +235 -0
- better_notion/utils/agents/dependency_resolver.py +368 -0
- better_notion/utils/agents/project_context.py +232 -0
- better_notion/utils/agents/rbac.py +371 -0
- better_notion/utils/agents/schemas.py +614 -0
- better_notion/utils/agents/state_machine.py +216 -0
- better_notion/utils/agents/workspace.py +371 -0
- {better_notion-0.9.9.dist-info → better_notion-1.0.1.dist-info}/METADATA +2 -2
- {better_notion-0.9.9.dist-info → better_notion-1.0.1.dist-info}/RECORD +15 -6
- {better_notion-0.9.9.dist-info → better_notion-1.0.1.dist-info}/WHEEL +0 -0
- {better_notion-0.9.9.dist-info → better_notion-1.0.1.dist-info}/entry_points.txt +0 -0
- {better_notion-0.9.9.dist-info → better_notion-1.0.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""State machine for managing task status transitions.
|
|
2
|
+
|
|
3
|
+
This module provides the state machine logic for task workflow transitions,
|
|
4
|
+
ensuring that tasks only move through valid states.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Dict, List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TaskStatus(Enum):
|
|
12
|
+
"""Task status values following the workflow.
|
|
13
|
+
|
|
14
|
+
The typical workflow is:
|
|
15
|
+
Backlog → Claimed → In Progress → In Review → Completed
|
|
16
|
+
|
|
17
|
+
Tasks can be Cancelled from most states.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
BACKLOG = "Backlog"
|
|
21
|
+
CLAIMED = "Claimed"
|
|
22
|
+
IN_PROGRESS = "In Progress"
|
|
23
|
+
IN_REVIEW = "In Review"
|
|
24
|
+
COMPLETED = "Completed"
|
|
25
|
+
CANCELLED = "Cancelled"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TaskStateMachine:
|
|
29
|
+
"""Manages valid task status transitions.
|
|
30
|
+
|
|
31
|
+
This state machine enforces the workflow rules for task status changes.
|
|
32
|
+
It prevents invalid transitions and provides helpers for querying
|
|
33
|
+
possible next states.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
>>> # Check if a transition is valid
|
|
37
|
+
>>> is_valid = TaskStateMachine.can_transition(
|
|
38
|
+
... TaskStatus.BACKLOG,
|
|
39
|
+
... TaskStatus.CLAIMED
|
|
40
|
+
... )
|
|
41
|
+
>>> print(is_valid) # True
|
|
42
|
+
|
|
43
|
+
>>> # Get next possible states
|
|
44
|
+
>>> next_states = TaskStateMachine.get_next_statuses("Backlog")
|
|
45
|
+
>>> print(next_states) # ["Claimed", "Cancelled"]
|
|
46
|
+
|
|
47
|
+
>>> # Validate transition
|
|
48
|
+
>>> is_valid, error = TaskStateMachine.validate_transition(
|
|
49
|
+
... "Backlog",
|
|
50
|
+
... "Completed"
|
|
51
|
+
... )
|
|
52
|
+
>>> print(error) # "Invalid transition: Backlog → Completed"
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
# Define valid state transitions
|
|
56
|
+
TRANSITIONS: Dict[TaskStatus, List[TaskStatus]] = {
|
|
57
|
+
TaskStatus.BACKLOG: [
|
|
58
|
+
TaskStatus.CLAIMED,
|
|
59
|
+
TaskStatus.CANCELLED,
|
|
60
|
+
],
|
|
61
|
+
TaskStatus.CLAIMED: [
|
|
62
|
+
TaskStatus.IN_PROGRESS,
|
|
63
|
+
TaskStatus.BACKLOG, # Unclaim and return to backlog
|
|
64
|
+
TaskStatus.CANCELLED,
|
|
65
|
+
],
|
|
66
|
+
TaskStatus.IN_PROGRESS: [
|
|
67
|
+
TaskStatus.IN_REVIEW,
|
|
68
|
+
TaskStatus.COMPLETED, # Skip review if not needed
|
|
69
|
+
TaskStatus.CANCELLED,
|
|
70
|
+
],
|
|
71
|
+
TaskStatus.IN_REVIEW: [
|
|
72
|
+
TaskStatus.COMPLETED,
|
|
73
|
+
TaskStatus.IN_PROGRESS, # Request changes during review
|
|
74
|
+
TaskStatus.CANCELLED,
|
|
75
|
+
],
|
|
76
|
+
TaskStatus.COMPLETED: [
|
|
77
|
+
# Terminal state - no transitions out
|
|
78
|
+
],
|
|
79
|
+
TaskStatus.CANCELLED: [
|
|
80
|
+
# Terminal state - no transitions out
|
|
81
|
+
],
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def can_transition(
|
|
86
|
+
cls,
|
|
87
|
+
from_status: TaskStatus,
|
|
88
|
+
to_status: TaskStatus,
|
|
89
|
+
) -> bool:
|
|
90
|
+
"""Check if a transition between two states is valid.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
from_status: Current state
|
|
94
|
+
to_status: Desired next state
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if the transition is valid, False otherwise
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
>>> TaskStateMachine.can_transition(
|
|
101
|
+
... TaskStatus.BACKLOG,
|
|
102
|
+
... TaskStatus.CLAIMED
|
|
103
|
+
... )
|
|
104
|
+
True
|
|
105
|
+
"""
|
|
106
|
+
allowed = cls.TRANSITIONS.get(from_status, [])
|
|
107
|
+
return to_status in allowed
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def validate_transition(
|
|
111
|
+
cls,
|
|
112
|
+
from_status: str,
|
|
113
|
+
to_status: str,
|
|
114
|
+
) -> Tuple[bool, Optional[str]]:
|
|
115
|
+
"""Validate a transition and return (is_valid, error_message).
|
|
116
|
+
|
|
117
|
+
This is a convenience method that handles string status values
|
|
118
|
+
and returns a detailed error message if the transition is invalid.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
from_status: Current status as string
|
|
122
|
+
to_status: Desired next status as string
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Tuple of (is_valid, error_message):
|
|
126
|
+
- is_valid: True if transition is valid
|
|
127
|
+
- error_message: None if valid, error description if invalid
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> is_valid, error = TaskStateMachine.validate_transition(
|
|
131
|
+
... "Backlog",
|
|
132
|
+
... "Claimed"
|
|
133
|
+
... )
|
|
134
|
+
>>> print(is_valid) # True
|
|
135
|
+
>>> print(error) # None
|
|
136
|
+
|
|
137
|
+
>>> is_valid, error = TaskStateMachine.validate_transition(
|
|
138
|
+
... "Completed",
|
|
139
|
+
... "In Progress"
|
|
140
|
+
... )
|
|
141
|
+
>>> print(is_valid) # False
|
|
142
|
+
>>> print(error) # "Invalid transition: Completed → In Progress"
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
from_enum = TaskStatus(from_status)
|
|
146
|
+
to_enum = TaskStatus(to_status)
|
|
147
|
+
except ValueError as e:
|
|
148
|
+
return False, f"Invalid status values: {from_status} → {to_status}"
|
|
149
|
+
|
|
150
|
+
if not cls.can_transition(from_enum, to_enum):
|
|
151
|
+
return False, f"Invalid transition: {from_status} → {to_status}"
|
|
152
|
+
|
|
153
|
+
return True, None
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def get_next_statuses(cls, current_status: str) -> List[str]:
|
|
157
|
+
"""Get list of valid next statuses from current status.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
current_status: Current status as string
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of valid next status values as strings
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
>>> TaskStateMachine.get_next_statuses("Backlog")
|
|
167
|
+
["Claimed", "Cancelled"]
|
|
168
|
+
|
|
169
|
+
>>> TaskStateMachine.get_next_statuses("Completed")
|
|
170
|
+
[]
|
|
171
|
+
"""
|
|
172
|
+
try:
|
|
173
|
+
current_enum = TaskStatus(current_status)
|
|
174
|
+
next_enums = cls.TRANSITIONS.get(current_enum, [])
|
|
175
|
+
return [e.value for e in next_enums]
|
|
176
|
+
except ValueError:
|
|
177
|
+
return []
|
|
178
|
+
|
|
179
|
+
@classmethod
|
|
180
|
+
def is_terminal_state(cls, status: str) -> bool:
|
|
181
|
+
"""Check if a status is a terminal state (no transitions out).
|
|
182
|
+
|
|
183
|
+
Terminal states are Completed and Cancelled.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
status: Status to check
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
True if status is terminal, False otherwise
|
|
190
|
+
|
|
191
|
+
Example:
|
|
192
|
+
>>> TaskStateMachine.is_terminal_state("Completed")
|
|
193
|
+
True
|
|
194
|
+
|
|
195
|
+
>>> TaskStateMachine.is_terminal_state("In Progress")
|
|
196
|
+
False
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
status_enum = TaskStatus(status)
|
|
200
|
+
allowed = cls.TRANSITIONS.get(status_enum, [])
|
|
201
|
+
return len(allowed) == 0
|
|
202
|
+
except ValueError:
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def get_initial_state(cls) -> str:
|
|
207
|
+
"""Get the initial state for new tasks.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
The initial task status (Backlog)
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
>>> TaskStateMachine.get_initial_state()
|
|
214
|
+
"Backlog"
|
|
215
|
+
"""
|
|
216
|
+
return TaskStatus.BACKLOG.value
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""Workspace initializer for the agents workflow system.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to initialize a new workspace with all
|
|
4
|
+
required databases for the workflow management system.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
from better_notion._cli.config import Config
|
|
12
|
+
from better_notion._sdk.client import NotionClient
|
|
13
|
+
from better_notion._sdk.models.page import Page
|
|
14
|
+
from better_notion.utils.agents.schemas import (
|
|
15
|
+
IncidentSchema,
|
|
16
|
+
IdeaSchema,
|
|
17
|
+
OrganizationSchema,
|
|
18
|
+
ProjectSchema,
|
|
19
|
+
TagSchema,
|
|
20
|
+
TaskSchema,
|
|
21
|
+
VersionSchema,
|
|
22
|
+
WorkIssueSchema,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class WorkspaceInitializer:
|
|
27
|
+
"""Initialize a new workspace with workflow databases.
|
|
28
|
+
|
|
29
|
+
This class handles the creation of all required databases and their
|
|
30
|
+
relationships for the agents workflow system.
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
>>> initializer = WorkspaceInitializer(client)
|
|
34
|
+
>>> database_ids = await initializer.initialize_workspace(
|
|
35
|
+
... parent_page_id="page123",
|
|
36
|
+
... workspace_name="My Workspace"
|
|
37
|
+
... )
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, client: NotionClient) -> None:
|
|
41
|
+
"""Initialize the workspace initializer.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
client: Authenticated NotionClient instance
|
|
45
|
+
"""
|
|
46
|
+
self._client = client
|
|
47
|
+
self._database_ids: Dict[str, str] = {}
|
|
48
|
+
|
|
49
|
+
async def initialize_workspace(
|
|
50
|
+
self,
|
|
51
|
+
parent_page_id: str,
|
|
52
|
+
workspace_name: str = "Agents Workspace",
|
|
53
|
+
) -> Dict[str, str]:
|
|
54
|
+
"""Initialize a complete workspace with all databases.
|
|
55
|
+
|
|
56
|
+
Creates all 8 databases in the correct order, establishing
|
|
57
|
+
relationships between them.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
parent_page_id: ID of the parent page where databases will be created
|
|
61
|
+
workspace_name: Name for the workspace (used for database titles)
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Dict mapping database names to their IDs
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
Exception: If database creation fails
|
|
68
|
+
"""
|
|
69
|
+
# Get parent page
|
|
70
|
+
parent = await Page.get(parent_page_id, client=self._client)
|
|
71
|
+
|
|
72
|
+
# Create databases in order (independent first, then dependent)
|
|
73
|
+
self._database_ids = {}
|
|
74
|
+
|
|
75
|
+
# 1. Organizations (independent)
|
|
76
|
+
await self._create_organizations_db(parent)
|
|
77
|
+
|
|
78
|
+
# 2. Tags (independent)
|
|
79
|
+
await self._create_tags_db(parent)
|
|
80
|
+
|
|
81
|
+
# 3. Projects (depends on Organizations)
|
|
82
|
+
await self._create_projects_db(parent)
|
|
83
|
+
|
|
84
|
+
# 4. Versions (depends on Projects)
|
|
85
|
+
await self._create_versions_db(parent)
|
|
86
|
+
|
|
87
|
+
# 5. Tasks (depends on Versions)
|
|
88
|
+
await self._create_tasks_db(parent)
|
|
89
|
+
|
|
90
|
+
# 6. Ideas (depends on Projects)
|
|
91
|
+
await self._create_ideas_db(parent)
|
|
92
|
+
|
|
93
|
+
# 7. Work Issues (depends on Projects, Tasks, Ideas)
|
|
94
|
+
await self._create_work_issues_db(parent)
|
|
95
|
+
|
|
96
|
+
# 8. Incidents (depends on Projects, Versions, Tasks)
|
|
97
|
+
await self._create_incidents_db(parent)
|
|
98
|
+
|
|
99
|
+
return self._database_ids
|
|
100
|
+
|
|
101
|
+
async def _create_organizations_db(self, parent: Page) -> None:
|
|
102
|
+
"""Create Organizations database."""
|
|
103
|
+
schema = OrganizationSchema.get_schema()
|
|
104
|
+
|
|
105
|
+
db = await self._client.databases.create(
|
|
106
|
+
parent=parent,
|
|
107
|
+
title="Organizations",
|
|
108
|
+
schema=schema,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
self._database_ids["organizations"] = db.id
|
|
112
|
+
|
|
113
|
+
async def _create_tags_db(self, parent: Page) -> None:
|
|
114
|
+
"""Create Tags database."""
|
|
115
|
+
schema = TagSchema.get_schema()
|
|
116
|
+
|
|
117
|
+
db = await self._client.databases.create(
|
|
118
|
+
parent=parent,
|
|
119
|
+
title="Tags",
|
|
120
|
+
schema=schema,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
self._database_ids["tags"] = db.id
|
|
124
|
+
|
|
125
|
+
async def _create_projects_db(self, parent: Page) -> None:
|
|
126
|
+
"""Create Projects database with relation to Organizations."""
|
|
127
|
+
schema = ProjectSchema.get_schema()
|
|
128
|
+
|
|
129
|
+
# Update relation with Organizations database ID
|
|
130
|
+
if "organizations" in self._database_ids:
|
|
131
|
+
schema["Organization"]["relation"]["database_id"] = self._database_ids[
|
|
132
|
+
"organizations"
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
db = await self._client.databases.create(
|
|
136
|
+
parent=parent,
|
|
137
|
+
title="Projects",
|
|
138
|
+
schema=schema,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
self._database_ids["projects"] = db.id
|
|
142
|
+
|
|
143
|
+
# Update Organizations database with reverse relation
|
|
144
|
+
await self._add_reverse_relation(
|
|
145
|
+
self._database_ids["organizations"],
|
|
146
|
+
db.id,
|
|
147
|
+
"Projects",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
async def _create_versions_db(self, parent: Page) -> None:
|
|
151
|
+
"""Create Versions database with relation to Projects."""
|
|
152
|
+
schema = VersionSchema.get_schema()
|
|
153
|
+
|
|
154
|
+
# Update relation with Projects database ID
|
|
155
|
+
if "projects" in self._database_ids:
|
|
156
|
+
schema["Project"]["relation"]["database_id"] = self._database_ids["projects"]
|
|
157
|
+
|
|
158
|
+
db = await self._client.databases.create(
|
|
159
|
+
parent=parent,
|
|
160
|
+
title="Versions",
|
|
161
|
+
schema=schema,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
self._database_ids["versions"] = db.id
|
|
165
|
+
|
|
166
|
+
# Update Projects database with reverse relation
|
|
167
|
+
await self._add_reverse_relation(
|
|
168
|
+
self._database_ids["projects"],
|
|
169
|
+
db.id,
|
|
170
|
+
"Versions",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
async def _create_tasks_db(self, parent: Page) -> None:
|
|
174
|
+
"""Create Tasks database with relations to Versions (self-referencing for dependencies)."""
|
|
175
|
+
schema = TaskSchema.get_schema()
|
|
176
|
+
|
|
177
|
+
# Update relations with Versions database ID
|
|
178
|
+
if "versions" in self._database_ids:
|
|
179
|
+
schema["Version"]["relation"]["database_id"] = self._database_ids["versions"]
|
|
180
|
+
schema["Target Version"]["relation"]["database_id"] = self._database_ids[
|
|
181
|
+
"versions"
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
# Tasks reference themselves for dependencies
|
|
185
|
+
# Will be updated after database creation
|
|
186
|
+
db = await self._client.databases.create(
|
|
187
|
+
parent=parent,
|
|
188
|
+
title="Tasks",
|
|
189
|
+
schema=schema,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
self._database_ids["tasks"] = db.id
|
|
193
|
+
|
|
194
|
+
# Update self-referential relations
|
|
195
|
+
await self._update_self_relations(db.id)
|
|
196
|
+
|
|
197
|
+
# Update Versions database with reverse relation
|
|
198
|
+
await self._add_reverse_relation(
|
|
199
|
+
self._database_ids["versions"],
|
|
200
|
+
db.id,
|
|
201
|
+
"Tasks",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
async def _create_ideas_db(self, parent: Page) -> None:
|
|
205
|
+
"""Create Ideas database with relations to Projects and Tasks."""
|
|
206
|
+
schema = IdeaSchema.get_schema()
|
|
207
|
+
|
|
208
|
+
# Update relations
|
|
209
|
+
if "projects" in self._database_ids:
|
|
210
|
+
schema["Project"]["relation"]["database_id"] = self._database_ids["projects"]
|
|
211
|
+
|
|
212
|
+
# Related Task will be updated after Tasks DB creation
|
|
213
|
+
db = await self._client.databases.create(
|
|
214
|
+
parent=parent,
|
|
215
|
+
title="Ideas",
|
|
216
|
+
schema=schema,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
self._database_ids["ideas"] = db.id
|
|
220
|
+
|
|
221
|
+
# Update Projects database with reverse relation
|
|
222
|
+
await self._add_reverse_relation(
|
|
223
|
+
self._database_ids["projects"],
|
|
224
|
+
db.id,
|
|
225
|
+
"Ideas",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
async def _create_work_issues_db(self, parent: Page) -> None:
|
|
229
|
+
"""Create Work Issues database with relations."""
|
|
230
|
+
schema = WorkIssueSchema.get_schema()
|
|
231
|
+
|
|
232
|
+
# Update relations
|
|
233
|
+
if "projects" in self._database_ids:
|
|
234
|
+
schema["Project"]["relation"]["database_id"] = self._database_ids["projects"]
|
|
235
|
+
if "tasks" in self._database_ids:
|
|
236
|
+
schema["Task"]["relation"]["database_id"] = self._database_ids["tasks"]
|
|
237
|
+
if "ideas" in self._database_ids:
|
|
238
|
+
schema["Related Idea"]["relation"]["database_id"] = self._database_ids["ideas"]
|
|
239
|
+
|
|
240
|
+
db = await self._client.databases.create(
|
|
241
|
+
parent=parent,
|
|
242
|
+
title="Work Issues",
|
|
243
|
+
schema=schema,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
self._database_ids["work_issues"] = db.id
|
|
247
|
+
|
|
248
|
+
async def _create_incidents_db(self, parent: Page) -> None:
|
|
249
|
+
"""Create Incidents database with relations."""
|
|
250
|
+
schema = IncidentSchema.get_schema()
|
|
251
|
+
|
|
252
|
+
# Update relations
|
|
253
|
+
if "projects" in self._database_ids:
|
|
254
|
+
schema["Project"]["relation"]["database_id"] = self._database_ids["projects"]
|
|
255
|
+
if "versions" in self._database_ids:
|
|
256
|
+
schema["Affected Version"]["relation"][
|
|
257
|
+
"database_id"
|
|
258
|
+
] = self._database_ids["versions"]
|
|
259
|
+
if "tasks" in self._database_ids:
|
|
260
|
+
schema["Fix Task"]["relation"]["database_id"] = self._database_ids["tasks"]
|
|
261
|
+
|
|
262
|
+
db = await self._client.databases.create(
|
|
263
|
+
parent=parent,
|
|
264
|
+
title="Incidents",
|
|
265
|
+
schema=schema,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
self._database_ids["incidents"] = db.id
|
|
269
|
+
|
|
270
|
+
async def _add_reverse_relation(
|
|
271
|
+
self,
|
|
272
|
+
database_id: str,
|
|
273
|
+
related_db_id: str,
|
|
274
|
+
property_name: str,
|
|
275
|
+
) -> None:
|
|
276
|
+
"""Add reverse relation to a database.
|
|
277
|
+
|
|
278
|
+
Note: This is a placeholder. The actual Notion API creates
|
|
279
|
+
dual_property relations automatically. This method exists
|
|
280
|
+
for documentation purposes.
|
|
281
|
+
"""
|
|
282
|
+
# Notion API handles dual_property relations automatically
|
|
283
|
+
# This is a no-op but documents the intent
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
async def _update_self_relations(self, database_id: str) -> None:
|
|
287
|
+
"""Update self-referential relations in Tasks database.
|
|
288
|
+
|
|
289
|
+
Note: The database was created with placeholder relations.
|
|
290
|
+
This method updates them to point to themselves.
|
|
291
|
+
"""
|
|
292
|
+
# Get the database
|
|
293
|
+
db = await self._client.databases.get(database_id)
|
|
294
|
+
|
|
295
|
+
# Update Dependencies and Dependent Tasks to point to self
|
|
296
|
+
# Note: This would require updating the database schema
|
|
297
|
+
# via the Notion API, which may not support all updates
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
def save_database_ids(self, path: Optional[Path] = None) -> None:
|
|
301
|
+
"""Save database IDs to a config file.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
path: Path to save config file (default: ~/.notion/workspace.json)
|
|
305
|
+
"""
|
|
306
|
+
if path is None:
|
|
307
|
+
path = Path.home() / ".notion" / "workspace.json"
|
|
308
|
+
|
|
309
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
310
|
+
|
|
311
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
312
|
+
json.dump(self._database_ids, f, indent=2)
|
|
313
|
+
|
|
314
|
+
@classmethod
|
|
315
|
+
def load_database_ids(cls, path: Optional[Path] = None) -> Dict[str, str]:
|
|
316
|
+
"""Load database IDs from config file.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
path: Path to config file (default: ~/.notion/workspace.json)
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Dict mapping database names to IDs
|
|
323
|
+
|
|
324
|
+
Raises:
|
|
325
|
+
FileNotFoundError: If config file doesn't exist
|
|
326
|
+
"""
|
|
327
|
+
if path is None:
|
|
328
|
+
path = Path.home() / ".notion" / "workspace.json"
|
|
329
|
+
|
|
330
|
+
with open(path, encoding="utf-8") as f:
|
|
331
|
+
return json.load(f)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
async def initialize_workspace_command(
|
|
335
|
+
parent_page_id: str,
|
|
336
|
+
workspace_name: str = "Agents Workspace",
|
|
337
|
+
) -> Dict[str, str]:
|
|
338
|
+
"""Convenience function to initialize a workspace.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
parent_page_id: ID of parent page
|
|
342
|
+
workspace_name: Name for the workspace
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Dict mapping database names to IDs
|
|
346
|
+
|
|
347
|
+
Example:
|
|
348
|
+
>>> from better_notion._cli.config import Config
|
|
349
|
+
>>> from better_notion._sdk.client import NotionClient
|
|
350
|
+
>>>
|
|
351
|
+
>>> config = Config.load()
|
|
352
|
+
>>> client = NotionClient(auth=config.token)
|
|
353
|
+
>>>
|
|
354
|
+
>>> db_ids = await initialize_workspace_command(
|
|
355
|
+
... parent_page_id="page123",
|
|
356
|
+
... workspace_name="My Workspace"
|
|
357
|
+
... )
|
|
358
|
+
"""
|
|
359
|
+
from better_notion._cli.config import Config
|
|
360
|
+
from better_notion._sdk.client import NotionClient
|
|
361
|
+
|
|
362
|
+
config = Config.load()
|
|
363
|
+
client = NotionClient(auth=config.token)
|
|
364
|
+
|
|
365
|
+
initializer = WorkspaceInitializer(client)
|
|
366
|
+
database_ids = await initializer.initialize_workspace(parent_page_id, workspace_name)
|
|
367
|
+
|
|
368
|
+
# Save to config file
|
|
369
|
+
initializer.save_database_ids()
|
|
370
|
+
|
|
371
|
+
return database_ids
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: better-notion
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: A high-level Python SDK for the Notion API with developer experience in mind.
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: A high-level Python SDK for the Notion API with developer experience in mind. Now with AI agents workflow management system!
|
|
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
|
|
7
7
|
Project-URL: Repository, https://github.com/nesalia-inc/better-notion
|
|
@@ -105,13 +105,22 @@ better_notion/plugins/__init__.py,sha256=ZbaLy0BS4_zWfEyCW0Xv9EhK427cEhrKIap_8DR
|
|
|
105
105
|
better_notion/plugins/base.py,sha256=G2asUEvQXWIiyWvNtB1hgg9JXZhDayi7Jvfq15FeQWY,5255
|
|
106
106
|
better_notion/plugins/loader.py,sha256=ewCG_Thv7Khh8IsHLB17U1Hi0cWpq0YG3w37hrpbeRk,7710
|
|
107
107
|
better_notion/plugins/state.py,sha256=jH_tZWvC35hqLO4qwl2Kwq9ziWVavwCEUcCqy3s5wMY,3780
|
|
108
|
-
better_notion/plugins/official/__init__.py,sha256=
|
|
108
|
+
better_notion/plugins/official/__init__.py,sha256=rPg5vdk1cEANVstMPzxcWmImtsOpdSR40JSml7h1uUk,426
|
|
109
|
+
better_notion/plugins/official/agents.py,sha256=8Cwx5fkNMaYc1bavS48jyvaXCTMYWgCaIMZH0y9YSbM,12696
|
|
109
110
|
better_notion/plugins/official/productivity.py,sha256=_-whP4pYA4HufE1aUFbIdhrjU-O9njI7xUO_Id2M1J8,8726
|
|
110
111
|
better_notion/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
111
112
|
better_notion/utils/helpers.py,sha256=HgFuUQlG_HzBOB0z2GA9RxPLoXgwRc0DIxa9Fg6C-Jk,2337
|
|
112
113
|
better_notion/utils/retry.py,sha256=9WJDNitiIfVTL18hIlipvOKn41ukePrOwtAwx-LevpQ,7479
|
|
113
|
-
better_notion
|
|
114
|
-
better_notion
|
|
115
|
-
better_notion
|
|
116
|
-
better_notion
|
|
117
|
-
better_notion
|
|
114
|
+
better_notion/utils/agents/__init__.py,sha256=Zu32q0abbimrJY5dczjWNEastE-IrtGPQjpxD4JS4IU,1715
|
|
115
|
+
better_notion/utils/agents/auth.py,sha256=_SBcqBjXmX8CJMCPpRWM-UuaDg7-OOtMWbhnYEiIBTs,6568
|
|
116
|
+
better_notion/utils/agents/dependency_resolver.py,sha256=PfHHDIQztGih4LwylMb0_MyhDFbOYPjvUxcxY52mSEs,12033
|
|
117
|
+
better_notion/utils/agents/project_context.py,sha256=aJlzy5H2rL4sAfW2jHL_3K2VkBJ4ihUhCRVolkpuO78,7477
|
|
118
|
+
better_notion/utils/agents/rbac.py,sha256=8ZA8Y7wbOiVZDbpjpH7iC35SZrZ0jl4fcJ3xWCm3SsE,11820
|
|
119
|
+
better_notion/utils/agents/schemas.py,sha256=e_lpGGO12FXtfqFyI91edj9xc5RUtuA6pU7Sk6ip7xg,21784
|
|
120
|
+
better_notion/utils/agents/state_machine.py,sha256=xUBEeDTbU1Xq-rsRo2sbr6AUYpYrV9DTHOtZT2cWES8,6699
|
|
121
|
+
better_notion/utils/agents/workspace.py,sha256=T8mP_DNIWhI1-k6UydJINsEJ6kQxQ6_Pa44ul58k088,12370
|
|
122
|
+
better_notion-1.0.1.dist-info/METADATA,sha256=twwihoncbT6FKJ_VmFtC_5g2zXSY12q-YAWFm4hLaqQ,11143
|
|
123
|
+
better_notion-1.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
124
|
+
better_notion-1.0.1.dist-info/entry_points.txt,sha256=D0bUcP7Z00Zyjxw7r2p29T95UrwioDO0aGDoHe9I6fo,55
|
|
125
|
+
better_notion-1.0.1.dist-info/licenses/LICENSE,sha256=BAdN3JpgMY_y_fWqZSCFSvSbC2mTHP-BKDAzF5FXQAI,1069
|
|
126
|
+
better_notion-1.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|