notionary 0.1.2__py3-none-any.whl → 0.1.3__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.
- notionary/core/__init__.py +0 -0
- notionary/core/converters/__init__.py +50 -0
- notionary/core/converters/elements/__init__.py +0 -0
- notionary/core/converters/elements/bookmark_element.py +224 -0
- notionary/core/converters/elements/callout_element.py +179 -0
- notionary/core/converters/elements/code_block_element.py +153 -0
- notionary/core/converters/elements/column_element.py +294 -0
- notionary/core/converters/elements/divider_element.py +73 -0
- notionary/core/converters/elements/heading_element.py +84 -0
- notionary/core/converters/elements/image_element.py +130 -0
- notionary/core/converters/elements/list_element.py +130 -0
- notionary/core/converters/elements/notion_block_element.py +51 -0
- notionary/core/converters/elements/paragraph_element.py +73 -0
- notionary/core/converters/elements/qoute_element.py +242 -0
- notionary/core/converters/elements/table_element.py +306 -0
- notionary/core/converters/elements/text_inline_formatter.py +294 -0
- notionary/core/converters/elements/todo_lists.py +114 -0
- notionary/core/converters/elements/toggle_element.py +205 -0
- notionary/core/converters/elements/video_element.py +159 -0
- notionary/core/converters/markdown_to_notion_converter.py +482 -0
- notionary/core/converters/notion_to_markdown_converter.py +45 -0
- notionary/core/converters/registry/__init__.py +0 -0
- notionary/core/converters/registry/block_element_registry.py +234 -0
- notionary/core/converters/registry/block_element_registry_builder.py +280 -0
- notionary/core/database/database_info_service.py +43 -0
- notionary/core/database/database_query_service.py +73 -0
- notionary/core/database/database_schema_service.py +57 -0
- notionary/core/database/models/page_result.py +10 -0
- notionary/core/database/notion_database_manager.py +332 -0
- notionary/core/database/notion_database_manager_factory.py +233 -0
- notionary/core/database/notion_database_schema.py +415 -0
- notionary/core/database/notion_database_writer.py +390 -0
- notionary/core/database/page_service.py +161 -0
- notionary/core/notion_client.py +134 -0
- notionary/core/page/meta_data/metadata_editor.py +37 -0
- notionary/core/page/notion_page_manager.py +110 -0
- notionary/core/page/page_content_manager.py +85 -0
- notionary/core/page/property_formatter.py +97 -0
- notionary/exceptions/database_exceptions.py +76 -0
- notionary/exceptions/page_creation_exception.py +9 -0
- notionary/util/logging_mixin.py +47 -0
- notionary/util/singleton_decorator.py +20 -0
- notionary/util/uuid_utils.py +24 -0
- {notionary-0.1.2.dist-info → notionary-0.1.3.dist-info}/METADATA +1 -1
- notionary-0.1.3.dist-info/RECORD +49 -0
- notionary-0.1.2.dist-info/RECORD +0 -6
- {notionary-0.1.2.dist-info → notionary-0.1.3.dist-info}/WHEEL +0 -0
- {notionary-0.1.2.dist-info → notionary-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.1.2.dist-info → notionary-0.1.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,332 @@
|
|
1
|
+
from typing import Any, AsyncGenerator, Dict, List, Optional, Union
|
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
|
+
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
|
+
from notionary.core.page.notion_page_manager import NotionPageManager
|
12
|
+
from notionary.exceptions.database_exceptions import (
|
13
|
+
DatabaseInitializationError,
|
14
|
+
PropertyError,
|
15
|
+
)
|
16
|
+
from notionary.util.logging_mixin import LoggingMixin
|
17
|
+
from notionary.util.uuid_utils import format_uuid
|
18
|
+
|
19
|
+
|
20
|
+
class NotionDatabaseManager(LoggingMixin):
|
21
|
+
"""
|
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.
|
28
|
+
"""
|
29
|
+
|
30
|
+
def __init__(self, database_id: str, token: Optional[str] = None):
|
31
|
+
"""
|
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.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
database_id: The ID of the Notion database
|
40
|
+
token: Optional Notion API token (uses environment variable if not provided)
|
41
|
+
"""
|
42
|
+
self.database_id = format_uuid(database_id) or database_id
|
43
|
+
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
|
+
|
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
|
+
|
97
|
+
async def get_database_name(self) -> Optional[str]:
|
98
|
+
"""
|
99
|
+
Get the name of the current database.
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
The database name or None if it couldn't be retrieved
|
103
|
+
"""
|
104
|
+
await self._ensure_initialized()
|
105
|
+
|
106
|
+
if self.title:
|
107
|
+
return self.title
|
108
|
+
|
109
|
+
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", "")
|
214
|
+
)
|
215
|
+
|
216
|
+
return result
|
217
|
+
|
218
|
+
async def delete_page(self, page_id: str) -> PageResult:
|
219
|
+
"""
|
220
|
+
Delete (archive) a page.
|
221
|
+
|
222
|
+
Args:
|
223
|
+
page_id: The ID of the page to delete
|
224
|
+
|
225
|
+
Returns:
|
226
|
+
Result object with success status and message
|
227
|
+
"""
|
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
|
242
|
+
|
243
|
+
async def get_pages(
|
244
|
+
self,
|
245
|
+
limit: int = 100,
|
246
|
+
filter_conditions: Optional[Dict[str, Any]] = None,
|
247
|
+
sorts: Optional[List[Dict[str, Any]]] = None,
|
248
|
+
) -> List[NotionPageManager]:
|
249
|
+
"""
|
250
|
+
Get all pages from the database.
|
251
|
+
|
252
|
+
Args:
|
253
|
+
limit: Maximum number of pages to retrieve
|
254
|
+
filter_conditions: Optional filter to apply to the database query
|
255
|
+
sorts: Optional sort instructions for the database query
|
256
|
+
|
257
|
+
Returns:
|
258
|
+
List of NotionPageManager instances for each page
|
259
|
+
"""
|
260
|
+
await self._ensure_initialized()
|
261
|
+
|
262
|
+
self.logger.debug(
|
263
|
+
"Getting up to %d pages with filter: %s, sorts: %s",
|
264
|
+
limit,
|
265
|
+
filter_conditions,
|
266
|
+
sorts,
|
267
|
+
)
|
268
|
+
|
269
|
+
pages = await self._query_service.get_pages(
|
270
|
+
self.database_id, limit, filter_conditions, sorts
|
271
|
+
)
|
272
|
+
|
273
|
+
self.logger.debug(
|
274
|
+
"Retrieved %d pages from database %s", len(pages), self.database_id
|
275
|
+
)
|
276
|
+
return pages
|
277
|
+
|
278
|
+
async def iter_pages(
|
279
|
+
self,
|
280
|
+
page_size: int = 100,
|
281
|
+
filter_conditions: Optional[Dict[str, Any]] = None,
|
282
|
+
sorts: Optional[List[Dict[str, Any]]] = None,
|
283
|
+
) -> AsyncGenerator[NotionPageManager, None]:
|
284
|
+
"""
|
285
|
+
Asynchronous generator that yields pages from the database.
|
286
|
+
|
287
|
+
Args:
|
288
|
+
page_size: Number of pages to fetch per request
|
289
|
+
filter_conditions: Optional filter to apply to the database query
|
290
|
+
sorts: Optional sort instructions for the database query
|
291
|
+
|
292
|
+
Yields:
|
293
|
+
NotionPageManager instances for each page
|
294
|
+
"""
|
295
|
+
await self._ensure_initialized()
|
296
|
+
|
297
|
+
self.logger.debug(
|
298
|
+
"Iterating pages with page_size: %d, filter: %s, sorts: %s",
|
299
|
+
page_size,
|
300
|
+
filter_conditions,
|
301
|
+
sorts,
|
302
|
+
)
|
303
|
+
|
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
|
308
|
+
|
309
|
+
async def get_page_manager(self, page_id: str) -> Optional[NotionPageManager]:
|
310
|
+
"""
|
311
|
+
Get a NotionPageManager for a specific page.
|
312
|
+
|
313
|
+
Args:
|
314
|
+
page_id: The ID of the page
|
315
|
+
|
316
|
+
Returns:
|
317
|
+
NotionPageManager instance or None if the page wasn't found
|
318
|
+
"""
|
319
|
+
await self._ensure_initialized()
|
320
|
+
|
321
|
+
self.logger.debug("Getting page manager for page %s", page_id)
|
322
|
+
|
323
|
+
page_manager = await self._page_service.get_page_manager(page_id)
|
324
|
+
|
325
|
+
if not page_manager:
|
326
|
+
self.logger.error("Page %s not found", page_id)
|
327
|
+
|
328
|
+
return page_manager
|
329
|
+
|
330
|
+
async def close(self) -> None:
|
331
|
+
"""Close the client connection."""
|
332
|
+
await self._client.close()
|
@@ -0,0 +1,233 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import List, Optional, Dict, Any
|
3
|
+
from difflib import SequenceMatcher
|
4
|
+
|
5
|
+
from notionary.core.notion_client import NotionClient
|
6
|
+
from notionary.core.database.notion_database_manager import NotionDatabaseManager
|
7
|
+
from notionary.exceptions.database_exceptions import (
|
8
|
+
DatabaseConnectionError,
|
9
|
+
DatabaseInitializationError,
|
10
|
+
DatabaseNotFoundException,
|
11
|
+
DatabaseParsingError,
|
12
|
+
NotionDatabaseException,
|
13
|
+
)
|
14
|
+
from notionary.util.logging_mixin import LoggingMixin
|
15
|
+
from notionary.util.uuid_utils import format_uuid
|
16
|
+
|
17
|
+
|
18
|
+
class NotionDatabaseFactory(LoggingMixin):
|
19
|
+
"""
|
20
|
+
Factory class for creating NotionDatabaseManager instances.
|
21
|
+
Provides methods for creating managers by database ID or name.
|
22
|
+
"""
|
23
|
+
|
24
|
+
@classmethod
|
25
|
+
def class_logger(cls):
|
26
|
+
"""Class logger - for class methods"""
|
27
|
+
return logging.getLogger(cls.__name__)
|
28
|
+
|
29
|
+
@classmethod
|
30
|
+
async def from_database_id(
|
31
|
+
cls, database_id: str, token: Optional[str] = None
|
32
|
+
) -> NotionDatabaseManager:
|
33
|
+
"""
|
34
|
+
Create a NotionDatabaseManager from a database ID.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
database_id: The ID of the Notion database
|
38
|
+
token: Optional Notion API token (uses environment variable if not provided)
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
An initialized NotionDatabaseManager instance
|
42
|
+
"""
|
43
|
+
logger = cls.class_logger()
|
44
|
+
|
45
|
+
try:
|
46
|
+
formatted_id = format_uuid(database_id) or database_id
|
47
|
+
|
48
|
+
manager = NotionDatabaseManager(formatted_id, token)
|
49
|
+
|
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
|
+
)
|
62
|
+
return manager
|
63
|
+
|
64
|
+
except DatabaseInitializationError:
|
65
|
+
# Re-raise the already typed exception
|
66
|
+
raise
|
67
|
+
except NotionDatabaseException:
|
68
|
+
# Re-raise other custom exceptions
|
69
|
+
raise
|
70
|
+
except Exception as e:
|
71
|
+
error_msg = f"Error connecting to database {database_id}: {str(e)}"
|
72
|
+
logger.error(error_msg)
|
73
|
+
raise DatabaseConnectionError(error_msg) from e
|
74
|
+
|
75
|
+
@classmethod
|
76
|
+
async def from_database_name(
|
77
|
+
cls, database_name: str, token: Optional[str] = None
|
78
|
+
) -> NotionDatabaseManager:
|
79
|
+
"""
|
80
|
+
Create a NotionDatabaseManager by finding a database with a matching name.
|
81
|
+
Uses fuzzy matching to find the closest match to the given name.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
database_name: The name of the Notion database to search for
|
85
|
+
token: Optional Notion API token (uses environment variable if not provided)
|
86
|
+
|
87
|
+
Returns:
|
88
|
+
An initialized NotionDatabaseManager instance
|
89
|
+
"""
|
90
|
+
logger = cls.class_logger()
|
91
|
+
logger.debug(lambda: f"Searching for database with name: {database_name}")
|
92
|
+
|
93
|
+
client = NotionClient(token=token)
|
94
|
+
|
95
|
+
try:
|
96
|
+
logger.debug("Using search endpoint to find databases")
|
97
|
+
|
98
|
+
# Create search query for databases
|
99
|
+
search_payload = {
|
100
|
+
"filter": {"property": "object", "value": "database"},
|
101
|
+
"page_size": 100,
|
102
|
+
}
|
103
|
+
|
104
|
+
# Perform search
|
105
|
+
response = await client.post("search", search_payload)
|
106
|
+
|
107
|
+
if not response or "results" not in response:
|
108
|
+
error_msg = "Failed to fetch databases using search endpoint"
|
109
|
+
logger.error(error_msg)
|
110
|
+
raise DatabaseConnectionError(error_msg)
|
111
|
+
|
112
|
+
databases = response.get("results", [])
|
113
|
+
|
114
|
+
if not databases:
|
115
|
+
error_msg = "No databases found"
|
116
|
+
logger.warning(error_msg)
|
117
|
+
raise DatabaseNotFoundException(database_name, error_msg)
|
118
|
+
|
119
|
+
logger.debug(
|
120
|
+
lambda: f"Found {len(databases)} databases, searching for best match"
|
121
|
+
)
|
122
|
+
|
123
|
+
# Find best match using fuzzy matching
|
124
|
+
best_match = None
|
125
|
+
best_score = 0
|
126
|
+
|
127
|
+
for db in databases:
|
128
|
+
title = cls._extract_title_from_database(db)
|
129
|
+
|
130
|
+
score = SequenceMatcher(
|
131
|
+
None, database_name.lower(), title.lower()
|
132
|
+
).ratio()
|
133
|
+
|
134
|
+
if score > best_score:
|
135
|
+
best_score = score
|
136
|
+
best_match = db
|
137
|
+
|
138
|
+
# Use a minimum threshold for match quality (0.6 = 60% similarity)
|
139
|
+
if best_score < 0.6 or not best_match:
|
140
|
+
error_msg = f"No good database name match found for '{database_name}'. Best match had score {best_score:.2f}"
|
141
|
+
logger.warning(error_msg)
|
142
|
+
raise DatabaseNotFoundException(database_name, error_msg)
|
143
|
+
|
144
|
+
database_id = best_match.get("id")
|
145
|
+
|
146
|
+
if not database_id:
|
147
|
+
error_msg = "Best match database has no ID"
|
148
|
+
logger.error(error_msg)
|
149
|
+
raise DatabaseParsingError(error_msg)
|
150
|
+
|
151
|
+
matched_name = cls._extract_title_from_database(best_match)
|
152
|
+
|
153
|
+
logger.info(
|
154
|
+
lambda: f"Found matching database: '{matched_name}' (ID: {database_id}) with score: {best_score:.2f}"
|
155
|
+
)
|
156
|
+
|
157
|
+
manager = NotionDatabaseManager(database_id, token)
|
158
|
+
success = await manager.initialize()
|
159
|
+
|
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
|
+
)
|
170
|
+
await client.close()
|
171
|
+
return manager
|
172
|
+
|
173
|
+
except NotionDatabaseException:
|
174
|
+
await client.close()
|
175
|
+
raise
|
176
|
+
except Exception as e:
|
177
|
+
error_msg = f"Error finding database by name: {str(e)}"
|
178
|
+
logger.error(error_msg)
|
179
|
+
await client.close()
|
180
|
+
raise DatabaseConnectionError(error_msg) from e
|
181
|
+
|
182
|
+
@classmethod
|
183
|
+
def _extract_title_from_database(cls, database: Dict[str, Any]) -> str:
|
184
|
+
"""
|
185
|
+
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
|
+
"""
|
196
|
+
try:
|
197
|
+
# Check for title in the root object
|
198
|
+
if "title" in database:
|
199
|
+
return cls._extract_text_from_rich_text(database["title"])
|
200
|
+
|
201
|
+
# Check for title in properties
|
202
|
+
if "properties" in database and "title" in database["properties"]:
|
203
|
+
title_prop = database["properties"]["title"]
|
204
|
+
if "title" in title_prop:
|
205
|
+
return cls._extract_text_from_rich_text(title_prop["title"])
|
206
|
+
|
207
|
+
return "Untitled"
|
208
|
+
|
209
|
+
except Exception as e:
|
210
|
+
error_msg = f"Error extracting database title: {str(e)}"
|
211
|
+
cls.class_logger().warning(error_msg)
|
212
|
+
raise DatabaseParsingError(error_msg) from e
|
213
|
+
|
214
|
+
@classmethod
|
215
|
+
def _extract_text_from_rich_text(cls, rich_text: List[Dict[str, Any]]) -> str:
|
216
|
+
"""
|
217
|
+
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
|
+
"""
|
225
|
+
if not rich_text:
|
226
|
+
return ""
|
227
|
+
|
228
|
+
text_parts = []
|
229
|
+
for text_obj in rich_text:
|
230
|
+
if "plain_text" in text_obj:
|
231
|
+
text_parts.append(text_obj["plain_text"])
|
232
|
+
|
233
|
+
return "".join(text_parts)
|