notionary 0.1.6__py3-none-any.whl → 0.1.7__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.
Files changed (28) hide show
  1. notionary/core/database/notion_database_manager.py +146 -232
  2. notionary/core/database/notion_database_manager_factory.py +9 -52
  3. notionary/core/database/notion_database_schema.py +1 -314
  4. notionary/core/notion_client.py +2 -10
  5. notionary/core/page/{page_content_manager.py → content/page_content_manager.py} +0 -1
  6. notionary/core/page/metadata/metadata_editor.py +109 -0
  7. notionary/core/page/metadata/notion_icon_manager.py +46 -0
  8. notionary/core/page/{meta_data/metadata_editor.py → metadata/notion_page_cover_manager.py} +20 -30
  9. notionary/core/page/notion_page_manager.py +218 -59
  10. notionary/core/page/properites/database_property_service.py +330 -0
  11. notionary/core/page/properites/page_property_manager.py +146 -0
  12. notionary/core/page/{property_formatter.py → properites/property_formatter.py} +19 -20
  13. notionary/core/page/properites/property_operation_result.py +103 -0
  14. notionary/core/page/properites/property_value_extractor.py +46 -0
  15. notionary/core/page/relations/notion_page_relation_manager.py +364 -0
  16. notionary/core/page/relations/notion_page_title_resolver.py +43 -0
  17. notionary/core/page/relations/page_database_relation.py +70 -0
  18. notionary/core/page/relations/relation_operation_result.py +135 -0
  19. notionary/util/{uuid_utils.py → page_id_utils.py} +15 -0
  20. {notionary-0.1.6.dist-info → notionary-0.1.7.dist-info}/METADATA +1 -1
  21. {notionary-0.1.6.dist-info → notionary-0.1.7.dist-info}/RECORD +24 -18
  22. notionary/core/database/database_query_service.py +0 -73
  23. notionary/core/database/database_schema_service.py +0 -57
  24. notionary/core/database/notion_database_writer.py +0 -390
  25. notionary/core/database/page_service.py +0 -161
  26. {notionary-0.1.6.dist-info → notionary-0.1.7.dist-info}/WHEEL +0 -0
  27. {notionary-0.1.6.dist-info → notionary-0.1.7.dist-info}/licenses/LICENSE +0 -0
  28. {notionary-0.1.6.dist-info → notionary-0.1.7.dist-info}/top_level.txt +0 -0
@@ -1,244 +1,83 @@
1
- from typing import Any, AsyncGenerator, Dict, List, Optional, Union
1
+ from typing import Any, AsyncGenerator, Dict, List, Optional
2
2
 
3
- from notionary.core.database.database_info_service import DatabaseInfoService
4
- from notionary.core.database.database_query_service import DatabaseQueryService
5
- from notionary.core.database.database_schema_service import DatabaseSchemaService
6
- from notionary.core.database.models.page_result import PageResult
7
- from notionary.core.database.page_service import DatabasePageService
8
3
  from notionary.core.notion_client import NotionClient
9
- from notionary.core.database.notion_database_schema import NotionDatabaseSchema
10
- from notionary.core.database.notion_database_writer import DatabaseWritter
11
4
  from notionary.core.page.notion_page_manager import NotionPageManager
12
- from notionary.exceptions.database_exceptions import (
13
- DatabaseInitializationError,
14
- PropertyError,
15
- )
16
5
  from notionary.util.logging_mixin import LoggingMixin
17
- from notionary.util.uuid_utils import format_uuid
6
+ from notionary.util.page_id_utils import format_uuid
18
7
 
19
8
 
20
9
  class NotionDatabaseManager(LoggingMixin):
21
10
  """
22
- High-level facade for working with Notion databases.
23
- Provides simplified operations for creating, reading, updating and deleting pages.
24
-
25
- Note:
26
- It is recommended to create instances of this class using the NotionDatabaseFactory
27
- instead of directly calling the constructor.
11
+ Minimal manager for Notion databases.
12
+ Focused exclusively on creating basic pages and retrieving page managers
13
+ for further page operations.
28
14
  """
29
15
 
30
16
  def __init__(self, database_id: str, token: Optional[str] = None):
31
17
  """
32
- Initialize the database facade with a database ID.
33
-
34
- Note:
35
- It's recommended to use NotionDatabaseFactory to create instances of this class
36
- rather than using this constructor directly.
18
+ Initialize the minimal database manager.
37
19
 
38
20
  Args:
39
- database_id: The ID of the Notion database
40
- token: Optional Notion API token (uses environment variable if not provided)
21
+ database_id: ID of the Notion database
22
+ token: Optional Notion API token
41
23
  """
42
24
  self.database_id = format_uuid(database_id) or database_id
43
25
  self._client = NotionClient(token=token)
44
- self._schema = NotionDatabaseSchema(self.database_id, self._client)
45
- self._writer = DatabaseWritter(self._client, self._schema)
46
- self._initialized = False
47
-
48
- self._info_service = DatabaseInfoService(self._client, self.database_id)
49
- self._page_service = DatabasePageService(
50
- self._client, self._schema, self._writer
51
- )
52
- self._query_service = DatabaseQueryService(self._schema)
53
- self._schema_service = DatabaseSchemaService(self._schema)
54
26
 
55
- @property
56
- def title(self) -> Optional[str]:
57
- """Get the database title."""
58
- return self._info_service.title
59
-
60
- async def initialize(self) -> bool:
61
- """
62
- Initialize the database facade by loading the schema.
63
-
64
- This method needs to be called after creating a new instance via the constructor.
65
- When using NotionDatabaseFactory, this is called automatically.
66
- """
67
- try:
68
- success = await self._schema.load()
69
- if not success:
70
- self.logger.error(
71
- "Failed to load schema for database %s", self.database_id
72
- )
73
- return False
74
-
75
- await self._info_service.load_title()
76
- self.logger.debug("Loaded database title: %s", self.title)
77
-
78
- self._initialized = True
79
- return True
80
- except Exception as e:
81
- self.logger.error("Error initializing database: %s", str(e))
82
- return False
83
-
84
- async def _ensure_initialized(self) -> None:
85
- """
86
- Ensure the database manager is initialized before use.
87
-
88
- Raises:
89
- DatabaseInitializationError: If the database isn't initialized
90
- """
91
- if not self._initialized:
92
- raise DatabaseInitializationError(
93
- self.database_id,
94
- "Database manager not initialized. Call initialize() first.",
95
- )
96
27
 
97
- async def get_database_name(self) -> Optional[str]:
28
+ async def create_blank_page(self) -> Optional[str]:
98
29
  """
99
- Get the name of the current database.
100
-
30
+ Create a new blank page in the database with minimal properties.
31
+
101
32
  Returns:
102
- The database name or None if it couldn't be retrieved
33
+ Optional[str]: The ID of the created page, or None if creation failed
103
34
  """
104
- await self._ensure_initialized()
105
-
106
- if self.title:
107
- return self.title
108
-
109
35
  try:
110
- return await self._info_service.load_title()
111
- except PropertyError as e:
112
- self.logger.error("Error getting database name: %s", str(e))
113
- return None
114
-
115
- async def get_property_types(self) -> Dict[str, str]:
116
- """
117
- Get all property types for the database.
118
-
119
- Returns:
120
- Dictionary mapping property names to their types
121
- """
122
- await self._ensure_initialized()
123
- return await self._schema_service.get_property_types()
124
-
125
- async def get_select_options(self, property_name: str) -> List[Dict[str, str]]:
126
- """
127
- Get options for a select, multi-select, or status property.
128
-
129
- Args:
130
- property_name: Name of the property
131
-
132
- Returns:
133
- List of select options with name, id, and color (if available)
134
- """
135
- await self._ensure_initialized()
136
- return await self._schema_service.get_select_options(property_name)
137
-
138
- async def get_relation_options(
139
- self, property_name: str, limit: int = 100
140
- ) -> List[Dict[str, str]]:
141
- """
142
- Get available options for a relation property.
143
-
144
- Args:
145
- property_name: Name of the relation property
146
- limit: Maximum number of options to retrieve
147
-
148
- Returns:
149
- List of relation options with id and title
150
- """
151
- await self._ensure_initialized()
152
- return await self._schema_service.get_relation_options(property_name, limit)
153
-
154
- async def create_page(
155
- self,
156
- properties: Dict[str, Any],
157
- relations: Optional[Dict[str, Union[str, List[str]]]] = None,
158
- ) -> PageResult:
159
- """
160
- Create a new page in the database.
161
-
162
- Args:
163
- properties: Dictionary of property names and values
164
- relations: Optional dictionary of relation property names and titles
165
-
166
- Returns:
167
- Result object with success status and page information
168
- """
169
- await self._ensure_initialized()
170
-
171
- result = await self._page_service.create_page(
172
- self.database_id, properties, relations
173
- )
174
-
175
- if result["success"]:
176
- self.logger.info(
177
- "Created page %s in database %s",
178
- result.get("page_id", ""),
179
- self.database_id,
180
- )
181
- else:
182
- self.logger.warning("Page creation failed: %s", result.get("message", ""))
183
-
184
- return result
185
-
186
- async def update_page(
187
- self,
188
- page_id: str,
189
- properties: Optional[Dict[str, Any]] = None,
190
- relations: Optional[Dict[str, Union[str, List[str]]]] = None,
191
- ) -> PageResult:
192
- """
193
- Update an existing page.
194
-
195
- Args:
196
- page_id: The ID of the page to update
197
- properties: Dictionary of property names and values to update
198
- relations: Optional dictionary of relation property names and titles
199
-
200
- Returns:
201
- Result object with success status and message
202
- """
203
- await self._ensure_initialized()
204
-
205
- self.logger.debug("Updating page %s", page_id)
206
-
207
- result = await self._page_service.update_page(page_id, properties, relations)
208
-
209
- if result["success"]:
210
- self.logger.info("Successfully updated page %s", result.get("page_id", ""))
211
- else:
212
- self.logger.error(
213
- "Error updating page %s: %s", page_id, result.get("message", "")
36
+ response = await self._client.post(
37
+ "pages",
38
+ {
39
+ "parent": {"database_id": self.database_id},
40
+ "properties": {}
41
+ }
214
42
  )
43
+
44
+ if response and "id" in response:
45
+ page_id = response["id"]
46
+ self.logger.info("Created blank page %s in database %s", page_id, self.database_id)
47
+ return page_id
48
+
49
+ self.logger.warning("Page creation failed: invalid response")
50
+ return None
51
+
52
+ except Exception as e:
53
+ self.logger.error("Error creating blank page: %s", str(e))
54
+ return None
215
55
 
216
- return result
217
-
218
- async def delete_page(self, page_id: str) -> PageResult:
56
+ async def get_page_manager(self, page_id: str) -> Optional[NotionPageManager]:
219
57
  """
220
- Delete (archive) a page.
58
+ Get a NotionPageManager for a specific page.
221
59
 
222
60
  Args:
223
- page_id: The ID of the page to delete
61
+ page_id: The ID of the page
224
62
 
225
63
  Returns:
226
- Result object with success status and message
64
+ NotionPageManager instance or None if the page wasn't found
227
65
  """
228
- await self._ensure_initialized()
229
-
230
- self.logger.debug("Deleting page %s", page_id)
231
-
232
- result = await self._page_service.delete_page(page_id)
233
-
234
- if result["success"]:
235
- self.logger.info("Successfully deleted page %s", result.get("page_id", ""))
236
- else:
237
- self.logger.error(
238
- "Error deleting page %s: %s", page_id, result.get("message", "")
239
- )
240
-
241
- return result
66
+ self.logger.debug("Getting page manager for page %s", page_id)
67
+
68
+ try:
69
+ # Check if the page exists
70
+ page_data = await self._client.get_page(page_id)
71
+
72
+ if not page_data:
73
+ self.logger.error("Page %s not found", page_id)
74
+ return None
75
+
76
+ return NotionPageManager(page_id=page_id)
77
+
78
+ except Exception as e:
79
+ self.logger.error("Error getting page manager: %s", str(e))
80
+ return None
242
81
 
243
82
  async def get_pages(
244
83
  self,
@@ -257,8 +96,6 @@ class NotionDatabaseManager(LoggingMixin):
257
96
  Returns:
258
97
  List of NotionPageManager instances for each page
259
98
  """
260
- await self._ensure_initialized()
261
-
262
99
  self.logger.debug(
263
100
  "Getting up to %d pages with filter: %s, sorts: %s",
264
101
  limit,
@@ -266,9 +103,19 @@ class NotionDatabaseManager(LoggingMixin):
266
103
  sorts,
267
104
  )
268
105
 
269
- pages = await self._query_service.get_pages(
270
- self.database_id, limit, filter_conditions, sorts
271
- )
106
+ pages: List[NotionPageManager] = []
107
+ count = 0
108
+
109
+ async for page in self.iter_pages(
110
+ page_size=min(limit, 100),
111
+ filter_conditions=filter_conditions,
112
+ sorts=sorts,
113
+ ):
114
+ pages.append(page)
115
+ count += 1
116
+
117
+ if count >= limit:
118
+ break
272
119
 
273
120
  self.logger.debug(
274
121
  "Retrieved %d pages from database %s", len(pages), self.database_id
@@ -283,6 +130,7 @@ class NotionDatabaseManager(LoggingMixin):
283
130
  ) -> AsyncGenerator[NotionPageManager, None]:
284
131
  """
285
132
  Asynchronous generator that yields pages from the database.
133
+ Directly queries the Notion API without using the schema.
286
134
 
287
135
  Args:
288
136
  page_size: Number of pages to fetch per request
@@ -292,8 +140,6 @@ class NotionDatabaseManager(LoggingMixin):
292
140
  Yields:
293
141
  NotionPageManager instances for each page
294
142
  """
295
- await self._ensure_initialized()
296
-
297
143
  self.logger.debug(
298
144
  "Iterating pages with page_size: %d, filter: %s, sorts: %s",
299
145
  page_size,
@@ -301,32 +147,100 @@ class NotionDatabaseManager(LoggingMixin):
301
147
  sorts,
302
148
  )
303
149
 
304
- async for page_manager in self._query_service.iter_pages(
305
- self.database_id, page_size, filter_conditions, sorts
306
- ):
307
- yield page_manager
150
+ start_cursor: Optional[str] = None
151
+ has_more = True
308
152
 
309
- async def get_page_manager(self, page_id: str) -> Optional[NotionPageManager]:
153
+ # Prepare the query body
154
+ body: Dict[str, Any] = {"page_size": page_size}
155
+
156
+ if filter_conditions:
157
+ body["filter"] = filter_conditions
158
+
159
+ if sorts:
160
+ body["sorts"] = sorts
161
+
162
+ while has_more:
163
+ current_body = body.copy()
164
+ if start_cursor:
165
+ current_body["start_cursor"] = start_cursor
166
+
167
+ result = await self._client.post(
168
+ f"databases/{self.database_id}/query", data=current_body
169
+ )
170
+
171
+ if not result or "results" not in result:
172
+ return
173
+
174
+ for page in result["results"]:
175
+ page_id: str = page.get("id", "")
176
+ title = self._extract_page_title(page)
177
+
178
+ page_url = f"https://notion.so/{page_id.replace('-', '')}"
179
+
180
+ notion_page_manager = NotionPageManager(page_id=page_id, title=title, url=page_url)
181
+ yield notion_page_manager
182
+
183
+ # Update pagination parameters
184
+ has_more = result.get("has_more", False)
185
+ start_cursor = result.get("next_cursor") if has_more else None
186
+
187
+ def _extract_page_title(self, page: Dict[str, Any]) -> str:
310
188
  """
311
- Get a NotionPageManager for a specific page.
189
+ Extracts the title from a Notion page object.
312
190
 
313
191
  Args:
314
- page_id: The ID of the page
192
+ page: The Notion page object
315
193
 
316
194
  Returns:
317
- NotionPageManager instance or None if the page wasn't found
195
+ The extracted title as a string, or an empty string if no title found
318
196
  """
319
- await self._ensure_initialized()
197
+ properties = page.get("properties", {})
198
+ if not properties:
199
+ return ""
320
200
 
321
- self.logger.debug("Getting page manager for page %s", page_id)
201
+ for prop_value in properties.values():
202
+ if prop_value.get("type") != "title":
203
+ continue
204
+
205
+ title_array = prop_value.get("title", [])
206
+ if not title_array:
207
+ continue
322
208
 
323
- page_manager = await self._page_service.get_page_manager(page_id)
209
+ return title_array[0].get("plain_text", "")
210
+
211
+ return ""
212
+
213
+ async def delete_page(self, page_id: str) -> Dict[str, Any]:
214
+ """
215
+ Delete (archive) a page.
324
216
 
325
- if not page_manager:
326
- self.logger.error("Page %s not found", page_id)
217
+ Args:
218
+ page_id: The ID of the page to delete
327
219
 
328
- return page_manager
220
+ Returns:
221
+ Dict with success status, message, and page_id when successful
222
+ """
223
+ try:
224
+ formatted_page_id = format_uuid(page_id) or page_id
225
+
226
+ # Archive the page (Notion's way of deleting)
227
+ data = {"archived": True}
228
+
229
+ result = await self._client.patch(f"pages/{formatted_page_id}", data)
230
+ if not result:
231
+ self.logger.error("Error deleting page %s", formatted_page_id)
232
+ return {
233
+ "success": False,
234
+ "message": f"Failed to delete page {formatted_page_id}",
235
+ }
236
+
237
+ self.logger.info("Page %s successfully deleted (archived)", formatted_page_id)
238
+ return {"success": True, "page_id": formatted_page_id}
239
+
240
+ except Exception as e:
241
+ self.logger.error("Error in delete_page: %s", str(e))
242
+ return {"success": False, "message": f"Error: {str(e)}"}
329
243
 
330
244
  async def close(self) -> None:
331
245
  """Close the client connection."""
332
- await self._client.close()
246
+ await self._client.close()
@@ -12,7 +12,7 @@ from notionary.exceptions.database_exceptions import (
12
12
  NotionDatabaseException,
13
13
  )
14
14
  from notionary.util.logging_mixin import LoggingMixin
15
- from notionary.util.uuid_utils import format_uuid
15
+ from notionary.util.page_id_utils import format_uuid
16
16
 
17
17
 
18
18
  class NotionDatabaseFactory(LoggingMixin):
@@ -44,21 +44,11 @@ class NotionDatabaseFactory(LoggingMixin):
44
44
 
45
45
  try:
46
46
  formatted_id = format_uuid(database_id) or database_id
47
-
47
+
48
48
  manager = NotionDatabaseManager(formatted_id, token)
49
+
49
50
 
50
- success = await manager.initialize()
51
-
52
- if not success:
53
- error_msg = (
54
- f"Failed to initialize database manager for ID: {formatted_id}"
55
- )
56
- logger.error(error_msg)
57
- raise DatabaseInitializationError(formatted_id, error_msg)
58
-
59
- logger.info(
60
- lambda: f"Successfully created database manager for ID: {formatted_id}"
61
- )
51
+ logger.info("Successfully created database manager for ID: %s", formatted_id)
62
52
  return manager
63
53
 
64
54
  except DatabaseInitializationError:
@@ -88,7 +78,7 @@ class NotionDatabaseFactory(LoggingMixin):
88
78
  An initialized NotionDatabaseManager instance
89
79
  """
90
80
  logger = cls.class_logger()
91
- logger.debug(lambda: f"Searching for database with name: {database_name}")
81
+ logger.debug("Searching for database with name: %s", database_name)
92
82
 
93
83
  client = NotionClient(token=token)
94
84
 
@@ -116,11 +106,8 @@ class NotionDatabaseFactory(LoggingMixin):
116
106
  logger.warning(error_msg)
117
107
  raise DatabaseNotFoundException(database_name, error_msg)
118
108
 
119
- logger.debug(
120
- lambda: f"Found {len(databases)} databases, searching for best match"
121
- )
109
+ logger.debug("Found %d databases, searching for best match", len(databases))
122
110
 
123
- # Find best match using fuzzy matching
124
111
  best_match = None
125
112
  best_score = 0
126
113
 
@@ -135,7 +122,6 @@ class NotionDatabaseFactory(LoggingMixin):
135
122
  best_score = score
136
123
  best_match = db
137
124
 
138
- # Use a minimum threshold for match quality (0.6 = 60% similarity)
139
125
  if best_score < 0.6 or not best_match:
140
126
  error_msg = f"No good database name match found for '{database_name}'. Best match had score {best_score:.2f}"
141
127
  logger.warning(error_msg)
@@ -150,23 +136,11 @@ class NotionDatabaseFactory(LoggingMixin):
150
136
 
151
137
  matched_name = cls._extract_title_from_database(best_match)
152
138
 
153
- logger.info(
154
- lambda: f"Found matching database: '{matched_name}' (ID: {database_id}) with score: {best_score:.2f}"
155
- )
139
+ logger.info("Found matching database: '%s' (ID: %s) with score: %.2f", matched_name, database_id, best_score)
156
140
 
157
141
  manager = NotionDatabaseManager(database_id, token)
158
- success = await manager.initialize()
159
142
 
160
- if not success:
161
- error_msg = (
162
- f"Failed to initialize database manager for database {database_id}"
163
- )
164
- logger.error(error_msg)
165
- raise DatabaseInitializationError(database_id, error_msg)
166
-
167
- logger.info(
168
- lambda: f"Successfully created database manager for '{matched_name}'"
169
- )
143
+ logger.info(f"Successfully created database manager for '{matched_name}'")
170
144
  await client.close()
171
145
  return manager
172
146
 
@@ -183,22 +157,11 @@ class NotionDatabaseFactory(LoggingMixin):
183
157
  def _extract_title_from_database(cls, database: Dict[str, Any]) -> str:
184
158
  """
185
159
  Extract the title from a database object.
186
-
187
- Args:
188
- database: A database object from the Notion API
189
-
190
- Returns:
191
- The extracted title or "Untitled" if no title is found
192
-
193
- Raises:
194
- DatabaseParsingError: If there's an error parsing the database title
195
160
  """
196
161
  try:
197
- # Check for title in the root object
198
162
  if "title" in database:
199
163
  return cls._extract_text_from_rich_text(database["title"])
200
164
 
201
- # Check for title in properties
202
165
  if "properties" in database and "title" in database["properties"]:
203
166
  title_prop = database["properties"]["title"]
204
167
  if "title" in title_prop:
@@ -215,12 +178,6 @@ class NotionDatabaseFactory(LoggingMixin):
215
178
  def _extract_text_from_rich_text(cls, rich_text: List[Dict[str, Any]]) -> str:
216
179
  """
217
180
  Extract plain text from a rich text array.
218
-
219
- Args:
220
- rich_text: A list of rich text objects from Notion API
221
-
222
- Returns:
223
- The concatenated plain text content
224
181
  """
225
182
  if not rich_text:
226
183
  return ""
@@ -230,4 +187,4 @@ class NotionDatabaseFactory(LoggingMixin):
230
187
  if "plain_text" in text_obj:
231
188
  text_parts.append(text_obj["plain_text"])
232
189
 
233
- return "".join(text_parts)
190
+ return "".join(text_parts)