better-notion 1.5.4__py3-none-any.whl → 1.5.5__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.
@@ -86,6 +86,18 @@ class AgentsPlugin(CombinedPluginInterface):
86
86
  "-n",
87
87
  help="Name for the workspace",
88
88
  ),
89
+ reset: bool = typer.Option(
90
+ False,
91
+ "--reset",
92
+ "-r",
93
+ help="Reset workspace (delete existing databases and recreate)",
94
+ ),
95
+ skip: bool = typer.Option(
96
+ False,
97
+ "--skip",
98
+ "-s",
99
+ help="Skip if workspace already exists (return existing databases)",
100
+ ),
89
101
  debug: bool = typer.Option(
90
102
  False,
91
103
  "--debug",
@@ -106,8 +118,13 @@ class AgentsPlugin(CombinedPluginInterface):
106
118
  - Incidents
107
119
  - Tags
108
120
 
121
+ If a workspace already exists in the page, use --reset to recreate
122
+ or --skip to keep the existing one.
123
+
109
124
  Example:
110
125
  $ notion agents init --parent-page page123 --name "My Workspace"
126
+ $ notion agents init --parent-page page123 --reset # Recreate
127
+ $ notion agents init --parent-page page123 --skip # Keep existing
111
128
  """
112
129
  import asyncio
113
130
  import logging
@@ -128,17 +145,40 @@ class AgentsPlugin(CombinedPluginInterface):
128
145
  client = get_client()
129
146
  initializer = WorkspaceInitializer(client)
130
147
 
148
+ # Handle --skip flag
149
+ if skip:
150
+ from better_notion._sdk.models.page import Page
151
+ from better_notion.utils.agents.metadata import WorkspaceMetadata
152
+
153
+ try:
154
+ page = await Page.get(parent_page_id, client=client)
155
+ existing = await WorkspaceMetadata.detect_workspace(page, client)
156
+
157
+ if existing:
158
+ database_ids = existing.get("database_ids", {})
159
+ return format_success(
160
+ {
161
+ "message": "Workspace already exists, skipping initialization",
162
+ "workspace_id": existing.get("workspace_id"),
163
+ "databases_found": len(database_ids),
164
+ "database_ids": database_ids,
165
+ }
166
+ )
167
+ except Exception:
168
+ # If detection fails, proceed with normal init
169
+ pass
170
+
171
+ # Initialize workspace (with skip_detection=True if --reset)
131
172
  database_ids = await initializer.initialize_workspace(
132
173
  parent_page_id=parent_page_id,
133
174
  workspace_name=workspace_name,
175
+ skip_detection=reset, # Skip detection if resetting
134
176
  )
135
177
 
136
- # Save database IDs
137
- initializer.save_database_ids()
138
-
139
178
  return format_success(
140
179
  {
141
180
  "message": "Workspace initialized successfully",
181
+ "workspace_id": initializer._workspace_id,
142
182
  "databases_created": len(database_ids),
143
183
  "database_ids": database_ids,
144
184
  }
@@ -151,6 +191,81 @@ class AgentsPlugin(CombinedPluginInterface):
151
191
  result = asyncio.run(_init())
152
192
  typer.echo(result)
153
193
 
194
+ @agents_app.command("info")
195
+ def workspace_info(
196
+ parent_page_id: str = typer.Option(
197
+ ...,
198
+ "--parent-page",
199
+ "-p",
200
+ help="ID of the parent page to check",
201
+ ),
202
+ ) -> None:
203
+ """
204
+ Show workspace information for a page.
205
+
206
+ Displays whether a workspace is initialized in the given page,
207
+ along with workspace metadata and database information.
208
+
209
+ Example:
210
+ $ notion agents info --parent-page page123
211
+ """
212
+ import asyncio
213
+
214
+ async def _info() -> str:
215
+ try:
216
+ client = get_client()
217
+ from better_notion._sdk.models.page import Page
218
+ from better_notion.utils.agents.metadata import WorkspaceMetadata
219
+
220
+ page = await Page.get(parent_page_id, client=client)
221
+ existing = await WorkspaceMetadata.detect_workspace(page, client)
222
+
223
+ if existing:
224
+ database_ids = existing.get("database_ids", {})
225
+
226
+ return format_success(
227
+ {
228
+ "message": "Workspace found in this page",
229
+ "parent_page": parent_page_id,
230
+ "parent_title": page.title,
231
+ "workspace_id": existing.get("workspace_id"),
232
+ "workspace_name": existing.get("workspace_name"),
233
+ "initialized_at": existing.get("initialized_at"),
234
+ "databases_count": len(database_ids),
235
+ "database_ids": database_ids,
236
+ "detection_method": existing.get("detection_method", "config_file"),
237
+ }
238
+ )
239
+ else:
240
+ # Search for any databases in the page
241
+ results = await client.search(
242
+ query="",
243
+ filter={"value": "database", "property": "object"}
244
+ )
245
+
246
+ # Count databases in this page
247
+ databases_in_page = []
248
+ for result in results:
249
+ if hasattr(result, 'title'):
250
+ databases_in_page.append(result.title)
251
+
252
+ return format_success(
253
+ {
254
+ "message": "No agents workspace found in this page",
255
+ "parent_page": parent_page_id,
256
+ "parent_title": page.title,
257
+ "workspace_initialized": False,
258
+ "other_databases": len(databases_in_page),
259
+ }
260
+ )
261
+
262
+ except Exception as e:
263
+ result = format_error("INFO_ERROR", str(e), retry=False)
264
+ return result
265
+
266
+ result = asyncio.run(_info())
267
+ typer.echo(result)
268
+
154
269
  @agents_app.command("init-project")
155
270
  def init_project(
156
271
  project_id: str = typer.Option(
@@ -0,0 +1,185 @@
1
+ """Workspace metadata management for agents plugin.
2
+
3
+ This module provides functionality to manage workspace metadata,
4
+ including detecting duplicate workspaces and storing workspace information.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+ from typing import Any, Optional
12
+ from uuid import uuid4
13
+
14
+ from better_notion._sdk.client import NotionClient
15
+ from better_notion._sdk.models.page import Page
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class WorkspaceMetadata:
21
+ """Manage workspace metadata for duplicate detection."""
22
+
23
+ # Property names used in Notion page
24
+ PROP_WORKSPACE_ID = "agents_workspace_id"
25
+ PROP_WORKSPACE_NAME = "agents_workspace_name"
26
+ PROP_INITIALIZED_AT = "agents_workspace_created"
27
+ PROP_VERSION = "agents_workspace_version"
28
+ PROP_DATABASE_IDS = "agents_workspace_databases"
29
+
30
+ @staticmethod
31
+ def generate_workspace_id() -> str:
32
+ """Generate a unique workspace ID.
33
+
34
+ Returns:
35
+ Unique workspace ID (UUID without hyphens for compactness)
36
+ """
37
+ return str(uuid4()).replace("-", "")
38
+
39
+ @staticmethod
40
+ def extract_metadata_from_page(page: Page) -> dict[str, Any]:
41
+ """Extract workspace metadata from page properties.
42
+
43
+ Args:
44
+ page: Page object to extract metadata from
45
+
46
+ Returns:
47
+ Dict with workspace metadata (empty if not initialized)
48
+
49
+ Example:
50
+ >>> metadata = WorkspaceMetadata.extract_metadata_from_page(page)
51
+ >>> if metadata.get("workspace_id"):
52
+ ... print(f"Workspace: {metadata['workspace_name']}")
53
+ """
54
+ metadata = {}
55
+
56
+ # Try to get workspace ID from icon (stored as emoji)
57
+ # Notion doesn't support custom properties, so we use creative approaches
58
+
59
+ # Check if page icon contains our metadata marker
60
+ icon = page.icon
61
+ if icon and icon.startswith("🤖"):
62
+ metadata["is_agents_workspace"] = True
63
+ # Could encode data in icon, but keep it simple for now
64
+
65
+ return metadata
66
+
67
+ @staticmethod
68
+ async def detect_workspace(
69
+ page: Page,
70
+ client: NotionClient
71
+ ) -> dict[str, Any] | None:
72
+ """Detect if page already has an agents workspace.
73
+
74
+ Args:
75
+ page: Parent page to check
76
+ client: NotionClient instance
77
+
78
+ Returns:
79
+ Workspace metadata dict if found, None otherwise
80
+ """
81
+ # Primary detection: Scan for databases with expected names
82
+ expected_databases = [
83
+ "Organizations",
84
+ "Tags",
85
+ "Projects",
86
+ "Versions",
87
+ "Tasks",
88
+ "Ideas",
89
+ "Work Issues",
90
+ "Incidents"
91
+ ]
92
+
93
+ try:
94
+ # Search for databases in this page
95
+ results = await client.search(
96
+ query="",
97
+ filter={"value": "database", "property": "object"}
98
+ )
99
+
100
+ databases_found = []
101
+ database_ids = {}
102
+
103
+ for result in results:
104
+ if hasattr(result, 'title') and result.title in expected_databases:
105
+ databases_found.append(result.title)
106
+ # Extract the key name (lowercase with underscores)
107
+ key = result.title.lower().replace(" ", "_")
108
+ database_ids[key] = result.id
109
+
110
+ # Check if we have at least 5 of the expected databases
111
+ matches = len(databases_found)
112
+ if matches >= 5:
113
+ logger.info(f"Detected existing workspace with {matches}/{len(expected_databases)} databases in page {page.id}")
114
+
115
+ # Try to load workspace metadata from local config to get workspace_id
116
+ workspace_id = None
117
+ workspace_name = None
118
+ initialized_at = None
119
+
120
+ try:
121
+ config_path = Path.home() / ".notion" / "workspace.json"
122
+ if config_path.exists():
123
+ with open(config_path, "r", encoding="utf-8") as f:
124
+ config = json.load(f)
125
+ # Only use config if it matches this page
126
+ if config.get("parent_page") == page.id:
127
+ workspace_id = config.get("workspace_id")
128
+ workspace_name = config.get("workspace_name")
129
+ initialized_at = config.get("initialized_at")
130
+ logger.info(f"Config file matches this page, workspace_id: {workspace_id}")
131
+ except Exception as e:
132
+ logger.debug(f"Could not load local config: {e}")
133
+
134
+ return {
135
+ "workspace_id": workspace_id,
136
+ "workspace_name": workspace_name,
137
+ "initialized_at": initialized_at,
138
+ "database_ids": database_ids,
139
+ "detection_method": "database_scan",
140
+ "databases_count": matches
141
+ }
142
+ except Exception as e:
143
+ logger.debug(f"Could not scan for databases: {e}")
144
+
145
+ return None
146
+
147
+ @staticmethod
148
+ def save_workspace_config(
149
+ page_id: str,
150
+ workspace_id: str,
151
+ workspace_name: str,
152
+ database_ids: dict[str, str],
153
+ path: Optional[Path] = None
154
+ ) -> Path:
155
+ """Save workspace configuration to local file.
156
+
157
+ Args:
158
+ page_id: Parent page ID
159
+ workspace_id: Unique workspace ID
160
+ workspace_name: Workspace name
161
+ database_ids: Dict of database name to ID
162
+ path: Optional custom path for config file
163
+
164
+ Returns:
165
+ Path to saved config file
166
+ """
167
+ if path is None:
168
+ path = Path.home() / ".notion" / "workspace.json"
169
+
170
+ path.parent.mkdir(parents=True, exist_ok=True)
171
+
172
+ config = {
173
+ "workspace_id": workspace_id,
174
+ "workspace_name": workspace_name,
175
+ "parent_page": page_id,
176
+ "initialized_at": datetime.now(timezone.utc).isoformat(),
177
+ "version": "1.5.4", # Track version for migrations
178
+ "database_ids": database_ids
179
+ }
180
+
181
+ with open(path, "w", encoding="utf-8") as f:
182
+ json.dump(config, f, indent=2)
183
+
184
+ logger.info(f"Saved workspace config to {path}")
185
+ return path
@@ -6,12 +6,14 @@ required databases for the workflow management system.
6
6
 
7
7
  import json
8
8
  import logging
9
+ from datetime import datetime, timezone
9
10
  from pathlib import Path
10
11
  from typing import Any, Dict, Optional
11
12
 
12
13
  from better_notion._cli.config import Config
13
14
  from better_notion._sdk.client import NotionClient
14
15
  from better_notion._sdk.models.page import Page
16
+ from better_notion.utils.agents.metadata import WorkspaceMetadata
15
17
  from better_notion.utils.agents.schemas import (
16
18
  IncidentSchema,
17
19
  IdeaSchema,
@@ -48,11 +50,15 @@ class WorkspaceInitializer:
48
50
  """
49
51
  self._client = client
50
52
  self._database_ids: Dict[str, str] = {}
53
+ self._workspace_id: Optional[str] = None
54
+ self._parent_page_id: Optional[str] = None
55
+ self._workspace_name: Optional[str] = None
51
56
 
52
57
  async def initialize_workspace(
53
58
  self,
54
59
  parent_page_id: str,
55
60
  workspace_name: str = "Agents Workspace",
61
+ skip_detection: bool = False,
56
62
  ) -> Dict[str, str]:
57
63
  """Initialize a complete workspace with all databases.
58
64
 
@@ -62,15 +68,22 @@ class WorkspaceInitializer:
62
68
  Args:
63
69
  parent_page_id: ID of the parent page where databases will be created
64
70
  workspace_name: Name for the workspace (used for database titles)
71
+ skip_detection: If True, skip duplicate detection (for reset operation)
65
72
 
66
73
  Returns:
67
74
  Dict mapping database names to their IDs
68
75
 
69
76
  Raises:
70
77
  Exception: If database creation fails with detailed error message
78
+ Exception: If workspace already exists (and skip_detection is False)
71
79
  """
72
80
  logger.info(f"Initializing workspace '{workspace_name}' in page {parent_page_id}")
73
81
 
82
+ # Store workspace info
83
+ self._parent_page_id = parent_page_id
84
+ self._workspace_name = workspace_name
85
+ self._workspace_id = WorkspaceMetadata.generate_workspace_id()
86
+
74
87
  # Get parent page
75
88
  try:
76
89
  parent = await Page.get(parent_page_id, client=self._client)
@@ -80,6 +93,26 @@ class WorkspaceInitializer:
80
93
  logger.error(error_msg)
81
94
  raise Exception(error_msg) from e
82
95
 
96
+ # Check for existing workspace unless skipped
97
+ if not skip_detection:
98
+ existing = await WorkspaceMetadata.detect_workspace(parent, self._client)
99
+ if existing:
100
+ workspace_info = {
101
+ "workspace_id": existing.get("workspace_id", "unknown"),
102
+ "workspace_name": existing.get("workspace_name", workspace_name),
103
+ "initialized_at": existing.get("initialized_at", "unknown"),
104
+ "databases": existing.get("database_ids", {})
105
+ }
106
+ error_msg = (
107
+ f"Workspace already initialized in this page\n"
108
+ f"Workspace ID: {workspace_info['workspace_id']}\n"
109
+ f"Initialized: {workspace_info['initialized_at']}\n"
110
+ f"Databases: {len(workspace_info.get('databases', {}))} created\n"
111
+ f"Use --reset to reinitialize or --skip to keep existing"
112
+ )
113
+ logger.error(error_msg)
114
+ raise Exception(error_msg) from None
115
+
83
116
  # Create databases in order (independent first, then dependent)
84
117
  self._database_ids = {}
85
118
  databases_order = [
@@ -108,7 +141,11 @@ class WorkspaceInitializer:
108
141
  logger.error(error_msg)
109
142
  raise Exception(error_msg) from e
110
143
 
144
+ # Save workspace metadata
145
+ self.save_database_ids()
146
+
111
147
  logger.info(f"Workspace initialization complete. Created {len(self._database_ids)} databases")
148
+ logger.info(f"Workspace ID: {self._workspace_id}")
112
149
  return self._database_ids
113
150
 
114
151
  async def _create_organizations_db(self, parent: Page) -> None:
@@ -312,7 +349,7 @@ class WorkspaceInitializer:
312
349
  pass
313
350
 
314
351
  def save_database_ids(self, path: Optional[Path] = None) -> None:
315
- """Save database IDs to a config file.
352
+ """Save workspace metadata (database IDs and workspace info) to config file.
316
353
 
317
354
  Args:
318
355
  path: Path to save config file (default: ~/.notion/workspace.json)
@@ -322,8 +359,18 @@ class WorkspaceInitializer:
322
359
 
323
360
  path.parent.mkdir(parents=True, exist_ok=True)
324
361
 
362
+ # Save full workspace metadata
363
+ config = {
364
+ "workspace_id": self._workspace_id,
365
+ "workspace_name": self._workspace_name,
366
+ "parent_page": self._parent_page_id,
367
+ "initialized_at": datetime.now(timezone.utc).isoformat(),
368
+ "version": "1.5.4",
369
+ "database_ids": self._database_ids
370
+ }
371
+
325
372
  with open(path, "w", encoding="utf-8") as f:
326
- json.dump(self._database_ids, f, indent=2)
373
+ json.dump(config, f, indent=2)
327
374
 
328
375
  @classmethod
329
376
  def load_database_ids(cls, path: Optional[Path] = None) -> Dict[str, str]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: better-notion
3
- Version: 1.5.4
3
+ Version: 1.5.5
4
4
  Summary: A high-level Python SDK for the Notion API with developer experience in mind.
5
5
  Project-URL: Homepage, https://github.com/nesalia-inc/better-notion
6
6
  Project-URL: Documentation, https://github.com/nesalia-inc/better-notion#readme
@@ -107,7 +107,7 @@ better_notion/plugins/base.py,sha256=3h9jOZzS--UqmVW3RREtcQ2h1GTWWPUryTencsJKhTM
107
107
  better_notion/plugins/loader.py,sha256=zCWsMdJyvZs1IHFm0zjEiqm_l_5jB1Uw4x30Kq8rLS4,9527
108
108
  better_notion/plugins/state.py,sha256=jH_tZWvC35hqLO4qwl2Kwq9ziWVavwCEUcCqy3s5wMY,3780
109
109
  better_notion/plugins/official/__init__.py,sha256=rPg5vdk1cEANVstMPzxcWmImtsOpdSR40JSml7h1uUk,426
110
- better_notion/plugins/official/agents.py,sha256=HDCAQOq7za00_YZXLftU5cBaeK3VSgutFWkXwwvtRxs,26636
110
+ better_notion/plugins/official/agents.py,sha256=Sa_Yxsj9CG3gULVLGJOrpIcdX9Tp4afi3nsCNQ-dwCU,32023
111
111
  better_notion/plugins/official/agents_cli.py,sha256=8l6e1zJCAT4DdAO-QfdjK_vrrrik3pmrojwakE32ZNY,53048
112
112
  better_notion/plugins/official/productivity.py,sha256=_-whP4pYA4HufE1aUFbIdhrjU-O9njI7xUO_Id2M1J8,8726
113
113
  better_notion/plugins/official/agents_sdk/__init__.py,sha256=luQBzZLsJ7fC5U0jFu8dfzMviiXj2SBZXcTohM53wkQ,725
@@ -121,13 +121,14 @@ better_notion/utils/validators.py,sha256=RyAGcYJexKOq1YepF6insoBu4QEOVPoAMWqi8Mt
121
121
  better_notion/utils/agents/__init__.py,sha256=Zu32q0abbimrJY5dczjWNEastE-IrtGPQjpxD4JS4IU,1715
122
122
  better_notion/utils/agents/auth.py,sha256=_SBcqBjXmX8CJMCPpRWM-UuaDg7-OOtMWbhnYEiIBTs,6568
123
123
  better_notion/utils/agents/dependency_resolver.py,sha256=PfHHDIQztGih4LwylMb0_MyhDFbOYPjvUxcxY52mSEs,12033
124
+ better_notion/utils/agents/metadata.py,sha256=thQSXfYx9mWgmBud8HtlNTLr5SwH6E_O1AjSNSnMFoo,6614
124
125
  better_notion/utils/agents/project_context.py,sha256=aJlzy5H2rL4sAfW2jHL_3K2VkBJ4ihUhCRVolkpuO78,7477
125
126
  better_notion/utils/agents/rbac.py,sha256=8ZA8Y7wbOiVZDbpjpH7iC35SZrZ0jl4fcJ3xWCm3SsE,11820
126
127
  better_notion/utils/agents/schemas.py,sha256=eHfGhY90FAPXA3E8qE6gP75dgNzn-9z5Ju1FMwBKnQQ,22120
127
128
  better_notion/utils/agents/state_machine.py,sha256=xUBEeDTbU1Xq-rsRo2sbr6AUYpYrV9DTHOtZT2cWES8,6699
128
- better_notion/utils/agents/workspace.py,sha256=FYarHj8eD2OeUG0KMPelqpBavm4RnYBoW2PVuwYkKI4,13614
129
- better_notion-1.5.4.dist-info/METADATA,sha256=JvdY_DpZZRM30OoqUazmG4bx6X1djPdoaOd0Scn3kBI,11096
130
- better_notion-1.5.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
131
- better_notion-1.5.4.dist-info/entry_points.txt,sha256=D0bUcP7Z00Zyjxw7r2p29T95UrwioDO0aGDoHe9I6fo,55
132
- better_notion-1.5.4.dist-info/licenses/LICENSE,sha256=BAdN3JpgMY_y_fWqZSCFSvSbC2mTHP-BKDAzF5FXQAI,1069
133
- better_notion-1.5.4.dist-info/RECORD,,
129
+ better_notion/utils/agents/workspace.py,sha256=nS7BNa0-RFCxeAKa0inmBZcPfRKTMB11pi9OW4WzvQ4,15915
130
+ better_notion-1.5.5.dist-info/METADATA,sha256=NwV0BcDdFVCjqHh7JbBZJechiXP_0YubbMlQPk9nTqU,11096
131
+ better_notion-1.5.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
132
+ better_notion-1.5.5.dist-info/entry_points.txt,sha256=D0bUcP7Z00Zyjxw7r2p29T95UrwioDO0aGDoHe9I6fo,55
133
+ better_notion-1.5.5.dist-info/licenses/LICENSE,sha256=BAdN3JpgMY_y_fWqZSCFSvSbC2mTHP-BKDAzF5FXQAI,1069
134
+ better_notion-1.5.5.dist-info/RECORD,,