notionary 0.1.6__tar.gz → 0.1.7__tar.gz
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-0.1.6 → notionary-0.1.7}/PKG-INFO +1 -1
- notionary-0.1.7/notionary/core/database/notion_database_manager.py +246 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/database/notion_database_manager_factory.py +9 -52
- notionary-0.1.7/notionary/core/database/notion_database_schema.py +104 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/notion_client.py +2 -10
- {notionary-0.1.6/notionary/core/page → notionary-0.1.7/notionary/core/page/content}/page_content_manager.py +0 -1
- notionary-0.1.7/notionary/core/page/metadata/metadata_editor.py +109 -0
- notionary-0.1.7/notionary/core/page/metadata/notion_icon_manager.py +46 -0
- notionary-0.1.6/notionary/core/page/meta_data/metadata_editor.py → notionary-0.1.7/notionary/core/page/metadata/notion_page_cover_manager.py +20 -30
- notionary-0.1.7/notionary/core/page/notion_page_manager.py +310 -0
- notionary-0.1.7/notionary/core/page/properites/database_property_service.py +330 -0
- notionary-0.1.7/notionary/core/page/properites/page_property_manager.py +146 -0
- {notionary-0.1.6/notionary/core/page → notionary-0.1.7/notionary/core/page/properites}/property_formatter.py +19 -20
- notionary-0.1.7/notionary/core/page/properites/property_operation_result.py +103 -0
- notionary-0.1.7/notionary/core/page/properites/property_value_extractor.py +46 -0
- notionary-0.1.7/notionary/core/page/relations/notion_page_relation_manager.py +364 -0
- notionary-0.1.7/notionary/core/page/relations/notion_page_title_resolver.py +43 -0
- notionary-0.1.7/notionary/core/page/relations/page_database_relation.py +70 -0
- notionary-0.1.7/notionary/core/page/relations/relation_operation_result.py +135 -0
- notionary-0.1.6/notionary/util/uuid_utils.py → notionary-0.1.7/notionary/util/page_id_utils.py +15 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary.egg-info/PKG-INFO +1 -1
- {notionary-0.1.6 → notionary-0.1.7}/notionary.egg-info/SOURCES.txt +15 -9
- {notionary-0.1.6 → notionary-0.1.7}/setup.py +1 -1
- notionary-0.1.6/notionary/core/database/database_query_service.py +0 -73
- notionary-0.1.6/notionary/core/database/database_schema_service.py +0 -57
- notionary-0.1.6/notionary/core/database/notion_database_manager.py +0 -332
- notionary-0.1.6/notionary/core/database/notion_database_schema.py +0 -417
- notionary-0.1.6/notionary/core/database/notion_database_writer.py +0 -390
- notionary-0.1.6/notionary/core/database/page_service.py +0 -161
- notionary-0.1.6/notionary/core/page/notion_page_manager.py +0 -151
- {notionary-0.1.6 → notionary-0.1.7}/LICENSE +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/README.md +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/__init__.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/__init__.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/bookmark_element.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/callout_element.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/code_block_element.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/column_element.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/divider_element.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/heading_element.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/image_element.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/list_element.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/notion_block_element.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/paragraph_element.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/qoute_element.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/table_element.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/text_inline_formatter.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/todo_lists.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/toggle_element.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/video_element.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/markdown_to_notion_converter.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/notion_to_markdown_converter.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/registry/block_element_registry.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/registry/block_element_registry_builder.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/database/database_info_service.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/core/database/models/page_result.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/exceptions/database_exceptions.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/exceptions/page_creation_exception.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/util/logging_mixin.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary/util/singleton_decorator.py +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary.egg-info/dependency_links.txt +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary.egg-info/requires.txt +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/notionary.egg-info/top_level.txt +0 -0
- {notionary-0.1.6 → notionary-0.1.7}/setup.cfg +0 -0
@@ -0,0 +1,246 @@
|
|
1
|
+
from typing import Any, AsyncGenerator, Dict, List, Optional
|
2
|
+
|
3
|
+
from notionary.core.notion_client import NotionClient
|
4
|
+
from notionary.core.page.notion_page_manager import NotionPageManager
|
5
|
+
from notionary.util.logging_mixin import LoggingMixin
|
6
|
+
from notionary.util.page_id_utils import format_uuid
|
7
|
+
|
8
|
+
|
9
|
+
class NotionDatabaseManager(LoggingMixin):
|
10
|
+
"""
|
11
|
+
Minimal manager for Notion databases.
|
12
|
+
Focused exclusively on creating basic pages and retrieving page managers
|
13
|
+
for further page operations.
|
14
|
+
"""
|
15
|
+
|
16
|
+
def __init__(self, database_id: str, token: Optional[str] = None):
|
17
|
+
"""
|
18
|
+
Initialize the minimal database manager.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
database_id: ID of the Notion database
|
22
|
+
token: Optional Notion API token
|
23
|
+
"""
|
24
|
+
self.database_id = format_uuid(database_id) or database_id
|
25
|
+
self._client = NotionClient(token=token)
|
26
|
+
|
27
|
+
|
28
|
+
async def create_blank_page(self) -> Optional[str]:
|
29
|
+
"""
|
30
|
+
Create a new blank page in the database with minimal properties.
|
31
|
+
|
32
|
+
Returns:
|
33
|
+
Optional[str]: The ID of the created page, or None if creation failed
|
34
|
+
"""
|
35
|
+
try:
|
36
|
+
response = await self._client.post(
|
37
|
+
"pages",
|
38
|
+
{
|
39
|
+
"parent": {"database_id": self.database_id},
|
40
|
+
"properties": {}
|
41
|
+
}
|
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
|
55
|
+
|
56
|
+
async def get_page_manager(self, page_id: str) -> Optional[NotionPageManager]:
|
57
|
+
"""
|
58
|
+
Get a NotionPageManager for a specific page.
|
59
|
+
|
60
|
+
Args:
|
61
|
+
page_id: The ID of the page
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
NotionPageManager instance or None if the page wasn't found
|
65
|
+
"""
|
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
|
81
|
+
|
82
|
+
async def get_pages(
|
83
|
+
self,
|
84
|
+
limit: int = 100,
|
85
|
+
filter_conditions: Optional[Dict[str, Any]] = None,
|
86
|
+
sorts: Optional[List[Dict[str, Any]]] = None,
|
87
|
+
) -> List[NotionPageManager]:
|
88
|
+
"""
|
89
|
+
Get all pages from the database.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
limit: Maximum number of pages to retrieve
|
93
|
+
filter_conditions: Optional filter to apply to the database query
|
94
|
+
sorts: Optional sort instructions for the database query
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
List of NotionPageManager instances for each page
|
98
|
+
"""
|
99
|
+
self.logger.debug(
|
100
|
+
"Getting up to %d pages with filter: %s, sorts: %s",
|
101
|
+
limit,
|
102
|
+
filter_conditions,
|
103
|
+
sorts,
|
104
|
+
)
|
105
|
+
|
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
|
119
|
+
|
120
|
+
self.logger.debug(
|
121
|
+
"Retrieved %d pages from database %s", len(pages), self.database_id
|
122
|
+
)
|
123
|
+
return pages
|
124
|
+
|
125
|
+
async def iter_pages(
|
126
|
+
self,
|
127
|
+
page_size: int = 100,
|
128
|
+
filter_conditions: Optional[Dict[str, Any]] = None,
|
129
|
+
sorts: Optional[List[Dict[str, Any]]] = None,
|
130
|
+
) -> AsyncGenerator[NotionPageManager, None]:
|
131
|
+
"""
|
132
|
+
Asynchronous generator that yields pages from the database.
|
133
|
+
Directly queries the Notion API without using the schema.
|
134
|
+
|
135
|
+
Args:
|
136
|
+
page_size: Number of pages to fetch per request
|
137
|
+
filter_conditions: Optional filter to apply to the database query
|
138
|
+
sorts: Optional sort instructions for the database query
|
139
|
+
|
140
|
+
Yields:
|
141
|
+
NotionPageManager instances for each page
|
142
|
+
"""
|
143
|
+
self.logger.debug(
|
144
|
+
"Iterating pages with page_size: %d, filter: %s, sorts: %s",
|
145
|
+
page_size,
|
146
|
+
filter_conditions,
|
147
|
+
sorts,
|
148
|
+
)
|
149
|
+
|
150
|
+
start_cursor: Optional[str] = None
|
151
|
+
has_more = True
|
152
|
+
|
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:
|
188
|
+
"""
|
189
|
+
Extracts the title from a Notion page object.
|
190
|
+
|
191
|
+
Args:
|
192
|
+
page: The Notion page object
|
193
|
+
|
194
|
+
Returns:
|
195
|
+
The extracted title as a string, or an empty string if no title found
|
196
|
+
"""
|
197
|
+
properties = page.get("properties", {})
|
198
|
+
if not properties:
|
199
|
+
return ""
|
200
|
+
|
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
|
208
|
+
|
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.
|
216
|
+
|
217
|
+
Args:
|
218
|
+
page_id: The ID of the page to delete
|
219
|
+
|
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)}"}
|
243
|
+
|
244
|
+
async def close(self) -> None:
|
245
|
+
"""Close the client connection."""
|
246
|
+
await self._client.close()
|
{notionary-0.1.6 → notionary-0.1.7}/notionary/core/database/notion_database_manager_factory.py
RENAMED
@@ -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.
|
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
|
-
|
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(
|
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
|
-
|
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)
|
@@ -0,0 +1,104 @@
|
|
1
|
+
from typing import (
|
2
|
+
AsyncGenerator,
|
3
|
+
Dict,
|
4
|
+
Optional,
|
5
|
+
Any,
|
6
|
+
)
|
7
|
+
from notionary.core.notion_client import NotionClient
|
8
|
+
from notionary.util.logging_mixin import LoggingMixin
|
9
|
+
|
10
|
+
class NotionDatabaseAccessor(LoggingMixin):
|
11
|
+
"""
|
12
|
+
A utility class that provides methods to access Notion databases.
|
13
|
+
Focused on efficient, paginated access to databases without unnecessary complexity.
|
14
|
+
"""
|
15
|
+
|
16
|
+
def __init__(self, client: Optional[NotionClient] = None) -> None:
|
17
|
+
"""
|
18
|
+
Initialize the accessor with a NotionClient.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
client: NotionClient instance for API communication
|
22
|
+
"""
|
23
|
+
self._client = client if client else NotionClient()
|
24
|
+
self.logger.info("NotionDatabaseAccessor initialized")
|
25
|
+
|
26
|
+
async def iter_databases(
|
27
|
+
self, page_size: int = 100
|
28
|
+
) -> AsyncGenerator[Dict[str, Any], None]:
|
29
|
+
"""
|
30
|
+
Asynchronous generator that yields Notion databases one by one.
|
31
|
+
|
32
|
+
Uses the Notion API to provide paginated access to all databases
|
33
|
+
without loading all of them into memory at once.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
page_size: The number of databases to fetch per request
|
37
|
+
|
38
|
+
Yields:
|
39
|
+
Individual database objects from the Notion API
|
40
|
+
"""
|
41
|
+
start_cursor: Optional[str] = None
|
42
|
+
|
43
|
+
while True:
|
44
|
+
body: Dict[str, Any] = {
|
45
|
+
"filter": {"value": "database", "property": "object"},
|
46
|
+
"page_size": page_size,
|
47
|
+
}
|
48
|
+
|
49
|
+
if start_cursor:
|
50
|
+
body["start_cursor"] = start_cursor
|
51
|
+
|
52
|
+
result = await self._client.post("search", data=body)
|
53
|
+
|
54
|
+
if not result or "results" not in result:
|
55
|
+
self.logger.error("Error fetching databases")
|
56
|
+
break
|
57
|
+
|
58
|
+
for database in result["results"]:
|
59
|
+
yield database
|
60
|
+
|
61
|
+
if "has_more" in result and result["has_more"] and "next_cursor" in result:
|
62
|
+
start_cursor = result["next_cursor"]
|
63
|
+
else:
|
64
|
+
break
|
65
|
+
|
66
|
+
async def get_database(self, database_id: str) -> Optional[Dict[str, Any]]:
|
67
|
+
"""
|
68
|
+
Get the details for a specific database.
|
69
|
+
|
70
|
+
Args:
|
71
|
+
database_id: The ID of the database
|
72
|
+
|
73
|
+
Returns:
|
74
|
+
Database details or None if not found
|
75
|
+
"""
|
76
|
+
db_details = await self._client.get(f"databases/{database_id}")
|
77
|
+
if not db_details:
|
78
|
+
self.logger.error("Failed to retrieve database %s", database_id)
|
79
|
+
return None
|
80
|
+
|
81
|
+
return db_details
|
82
|
+
|
83
|
+
def extract_database_title(self, database: Dict[str, Any]) -> str:
|
84
|
+
"""
|
85
|
+
Extract the database title from a Notion API response.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
database: The database object from the Notion API
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
The extracted title or "Untitled" if no title is found
|
92
|
+
"""
|
93
|
+
title = "Untitled"
|
94
|
+
|
95
|
+
if "title" in database:
|
96
|
+
title_parts = []
|
97
|
+
for text_obj in database["title"]:
|
98
|
+
if "plain_text" in text_obj:
|
99
|
+
title_parts.append(text_obj["plain_text"])
|
100
|
+
|
101
|
+
if title_parts:
|
102
|
+
title = "".join(title_parts)
|
103
|
+
|
104
|
+
return title
|
@@ -1,12 +1,11 @@
|
|
1
1
|
import asyncio
|
2
2
|
import os
|
3
|
+
import weakref
|
3
4
|
from enum import Enum
|
4
5
|
from typing import Dict, Any, Optional, Union
|
5
6
|
import httpx
|
6
7
|
from dotenv import load_dotenv
|
7
8
|
from notionary.util.logging_mixin import LoggingMixin
|
8
|
-
import weakref
|
9
|
-
|
10
9
|
|
11
10
|
class HttpMethod(Enum):
|
12
11
|
"""Enum für HTTP-Methoden."""
|
@@ -16,7 +15,6 @@ class HttpMethod(Enum):
|
|
16
15
|
PATCH = "patch"
|
17
16
|
DELETE = "delete"
|
18
17
|
|
19
|
-
|
20
18
|
class NotionClient(LoggingMixin):
|
21
19
|
"""Verbesserter Notion-Client mit automatischer Ressourcenverwaltung."""
|
22
20
|
|
@@ -113,12 +111,6 @@ class NotionClient(LoggingMixin):
|
|
113
111
|
return None
|
114
112
|
|
115
113
|
def __del__(self):
|
116
|
-
"""
|
117
|
-
Destruktor, der beim Garbage Collecting aufgerufen wird.
|
118
|
-
|
119
|
-
Hinweis: Dies ist nur ein Fallback, da __del__ nicht garantiert für async Cleanup funktioniert.
|
120
|
-
Die bessere Praxis ist, close() explizit zu rufen, wenn möglich.
|
121
|
-
"""
|
122
114
|
if not hasattr(self, "client") or not self.client:
|
123
115
|
return
|
124
116
|
|
@@ -134,4 +126,4 @@ class NotionClient(LoggingMixin):
|
|
134
126
|
loop.create_task(self.close())
|
135
127
|
self.logger.debug("Created cleanup task for NotionClient")
|
136
128
|
except RuntimeError:
|
137
|
-
self.logger.warning("No event loop available for auto-closing NotionClient")
|
129
|
+
self.logger.warning("No event loop available for auto-closing NotionClient")
|
@@ -54,7 +54,6 @@ class PageContentManager(LoggingMixin):
|
|
54
54
|
|
55
55
|
return f"Deleted {deleted}/{len(results)} blocks."
|
56
56
|
|
57
|
-
# Methods from PageContentReader
|
58
57
|
async def get_blocks(self) -> List[Dict[str, Any]]:
|
59
58
|
result = await self._client.get(f"blocks/{self.page_id}/children")
|
60
59
|
if not result:
|
@@ -0,0 +1,109 @@
|
|
1
|
+
from typing import Any, Dict, Optional
|
2
|
+
from notionary.core.notion_client import NotionClient
|
3
|
+
from notionary.core.page.properites.property_formatter import NotionPropertyFormatter
|
4
|
+
from notionary.util.logging_mixin import LoggingMixin
|
5
|
+
|
6
|
+
|
7
|
+
class MetadataEditor(LoggingMixin):
|
8
|
+
def __init__(self, page_id: str, client: NotionClient):
|
9
|
+
self.page_id = page_id
|
10
|
+
self._client = client
|
11
|
+
self._property_formatter = NotionPropertyFormatter()
|
12
|
+
|
13
|
+
async def set_title(self, title: str) -> Optional[Dict[str, Any]]:
|
14
|
+
return await self._client.patch(
|
15
|
+
f"pages/{self.page_id}",
|
16
|
+
{
|
17
|
+
"properties": {
|
18
|
+
"title": {"title": [{"type": "text", "text": {"content": title}}]}
|
19
|
+
}
|
20
|
+
},
|
21
|
+
)
|
22
|
+
|
23
|
+
async def set_property(self, property_name: str, property_value: Any, property_type: str) -> Optional[Dict[str, Any]]:
|
24
|
+
"""
|
25
|
+
Generic method to set any property on a Notion page.
|
26
|
+
|
27
|
+
Args:
|
28
|
+
property_name: The name of the property in Notion
|
29
|
+
property_value: The value to set
|
30
|
+
property_type: The type of property ('select', 'multi_select', 'status', 'relation', etc.)
|
31
|
+
|
32
|
+
Returns:
|
33
|
+
Optional[Dict[str, Any]]: The API response or None if the operation fails
|
34
|
+
"""
|
35
|
+
property_payload = self._property_formatter.format_value(property_type, property_value)
|
36
|
+
|
37
|
+
if not property_payload:
|
38
|
+
self.logger.warning("Could not create payload for property type: %s", property_type)
|
39
|
+
return None
|
40
|
+
|
41
|
+
return await self._client.patch(
|
42
|
+
f"pages/{self.page_id}",
|
43
|
+
{
|
44
|
+
"properties": {
|
45
|
+
property_name: property_payload
|
46
|
+
}
|
47
|
+
},
|
48
|
+
)
|
49
|
+
|
50
|
+
|
51
|
+
async def get_property_schema(self) -> Dict[str, Dict[str, Any]]:
|
52
|
+
"""
|
53
|
+
Retrieves the schema for all properties of the page.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
Dict[str, Dict[str, Any]]: A dictionary mapping property names to their schema
|
57
|
+
"""
|
58
|
+
page_data = await self._client.get_page(self.page_id)
|
59
|
+
property_schema = {}
|
60
|
+
|
61
|
+
if not page_data or "properties" not in page_data:
|
62
|
+
return property_schema
|
63
|
+
|
64
|
+
for prop_name, prop_data in page_data["properties"].items():
|
65
|
+
prop_type = prop_data.get("type")
|
66
|
+
property_schema[prop_name] = {
|
67
|
+
"id": prop_data.get("id"),
|
68
|
+
"type": prop_type,
|
69
|
+
"name": prop_name
|
70
|
+
}
|
71
|
+
|
72
|
+
try:
|
73
|
+
if prop_type == "select" and "select" in prop_data:
|
74
|
+
# Make sure prop_data["select"] is a dictionary before calling .get()
|
75
|
+
if isinstance(prop_data["select"], dict):
|
76
|
+
property_schema[prop_name]["options"] = prop_data["select"].get("options", [])
|
77
|
+
elif prop_type == "multi_select" and "multi_select" in prop_data:
|
78
|
+
# Make sure prop_data["multi_select"] is a dictionary before calling .get()
|
79
|
+
if isinstance(prop_data["multi_select"], dict):
|
80
|
+
property_schema[prop_name]["options"] = prop_data["multi_select"].get("options", [])
|
81
|
+
elif prop_type == "status" and "status" in prop_data:
|
82
|
+
# Make sure prop_data["status"] is a dictionary before calling .get()
|
83
|
+
if isinstance(prop_data["status"], dict):
|
84
|
+
property_schema[prop_name]["options"] = prop_data["status"].get("options", [])
|
85
|
+
except Exception as e:
|
86
|
+
if hasattr(self, 'logger') and self.logger:
|
87
|
+
self.logger.warning("Error processing property schema for '%s': %s", prop_name, e)
|
88
|
+
|
89
|
+
return property_schema
|
90
|
+
|
91
|
+
async def set_property_by_name(self, property_name: str, value: Any) -> Optional[Dict[str, Any]]:
|
92
|
+
"""
|
93
|
+
Sets a property value based on the property name, automatically detecting the property type.
|
94
|
+
|
95
|
+
Args:
|
96
|
+
property_name: The name of the property in Notion
|
97
|
+
value: The value to set
|
98
|
+
|
99
|
+
Returns:
|
100
|
+
Optional[Dict[str, Any]]: The API response or None if the operation fails
|
101
|
+
"""
|
102
|
+
property_schema = await self.get_property_schema()
|
103
|
+
|
104
|
+
if property_name not in property_schema:
|
105
|
+
self.logger.warning("Property '%s' not found in database schema", property_name)
|
106
|
+
return None
|
107
|
+
|
108
|
+
property_type = property_schema[property_name]["type"]
|
109
|
+
return await self.set_property(property_name, value, property_type)
|
@@ -0,0 +1,46 @@
|
|
1
|
+
from typing import Any, Dict, Optional
|
2
|
+
|
3
|
+
from notionary.core.notion_client import NotionClient
|
4
|
+
from notionary.util.logging_mixin import LoggingMixin
|
5
|
+
|
6
|
+
class NotionPageIconManager(LoggingMixin):
|
7
|
+
def __init__(self, page_id: str, client: NotionClient):
|
8
|
+
self.page_id = page_id
|
9
|
+
self._client = client
|
10
|
+
|
11
|
+
async def set_icon(
|
12
|
+
self, emoji: Optional[str] = None, external_url: Optional[str] = None
|
13
|
+
) -> Optional[Dict[str, Any]]:
|
14
|
+
if emoji:
|
15
|
+
icon = {"type": "emoji", "emoji": emoji}
|
16
|
+
elif external_url:
|
17
|
+
icon = {"type": "external", "external": {"url": external_url}}
|
18
|
+
else:
|
19
|
+
return None
|
20
|
+
|
21
|
+
return await self._client.patch(f"pages/{self.page_id}", {"icon": icon})
|
22
|
+
|
23
|
+
|
24
|
+
async def get_icon(self) -> Optional[str]:
|
25
|
+
"""
|
26
|
+
Retrieves the page icon - either emoji or external URL.
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
str: Emoji character or URL if set, None if no icon
|
30
|
+
"""
|
31
|
+
page_data = await self._client.get_page(self.page_id)
|
32
|
+
|
33
|
+
if not page_data or "icon" not in page_data:
|
34
|
+
return None
|
35
|
+
|
36
|
+
icon_data = page_data.get("icon", {})
|
37
|
+
icon_type = icon_data.get("type")
|
38
|
+
|
39
|
+
if icon_type == "emoji":
|
40
|
+
return icon_data.get("emoji")
|
41
|
+
elif icon_type == "external":
|
42
|
+
return icon_data.get("external", {}).get("url")
|
43
|
+
|
44
|
+
return None
|
45
|
+
|
46
|
+
|