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,390 @@
|
|
1
|
+
from typing import Any, Dict, List, Optional, Union, TypedDict, cast
|
2
|
+
from notionary.core.database.notion_database_schema import NotionDatabaseSchema
|
3
|
+
from notionary.core.notion_client import NotionClient
|
4
|
+
from notionary.core.page.property_formatter import NotionPropertyFormatter
|
5
|
+
from notionary.util.logging_mixin import LoggingMixin
|
6
|
+
|
7
|
+
|
8
|
+
class NotionRelationItem(TypedDict):
|
9
|
+
id: str
|
10
|
+
|
11
|
+
|
12
|
+
class NotionRelationProperty(TypedDict):
|
13
|
+
relation: List[NotionRelationItem]
|
14
|
+
|
15
|
+
|
16
|
+
class NotionFormattedValue(TypedDict, total=False):
|
17
|
+
title: List[Dict[str, Any]]
|
18
|
+
rich_text: List[Dict[str, Any]]
|
19
|
+
select: Dict[str, str]
|
20
|
+
multi_select: List[Dict[str, str]]
|
21
|
+
relation: List[NotionRelationItem]
|
22
|
+
number: Union[int, float]
|
23
|
+
date: Dict[str, str]
|
24
|
+
checkbox: bool
|
25
|
+
url: str
|
26
|
+
email: str
|
27
|
+
phone_number: str
|
28
|
+
|
29
|
+
|
30
|
+
class PageCreationResponse(TypedDict):
|
31
|
+
id: str
|
32
|
+
parent: Dict[str, str]
|
33
|
+
properties: Dict[str, Any]
|
34
|
+
|
35
|
+
|
36
|
+
class NotionRelationHandler(LoggingMixin):
|
37
|
+
"""
|
38
|
+
Handler for managing relations in Notion databases.
|
39
|
+
Provides a unified interface for working with relations.
|
40
|
+
"""
|
41
|
+
|
42
|
+
def __init__(self, client: NotionClient, db_schema: NotionDatabaseSchema) -> None:
|
43
|
+
self._client = client
|
44
|
+
self._db_schema = db_schema
|
45
|
+
self._formatter = NotionPropertyFormatter()
|
46
|
+
|
47
|
+
async def find_relation_by_title(
|
48
|
+
self, database_id: str, relation_prop_name: str, title: str
|
49
|
+
) -> Optional[str]:
|
50
|
+
"""
|
51
|
+
Finds a relation ID based on the title of the entry in the target database.
|
52
|
+
"""
|
53
|
+
target_db_id = await self._db_schema.get_relation_database_id(
|
54
|
+
relation_prop_name
|
55
|
+
)
|
56
|
+
if not target_db_id:
|
57
|
+
self.logger.error(
|
58
|
+
"No target database found for relation '%s' in database %s",
|
59
|
+
relation_prop_name,
|
60
|
+
database_id,
|
61
|
+
)
|
62
|
+
return None
|
63
|
+
|
64
|
+
options = await self._db_schema.get_relation_options(relation_prop_name)
|
65
|
+
|
66
|
+
for option in options:
|
67
|
+
if option["title"].lower() == title.lower():
|
68
|
+
self.logger.debug("Relation entry '%s' found: %s", title, option["id"])
|
69
|
+
return option["id"]
|
70
|
+
|
71
|
+
self.logger.warning("Relation entry '%s' not found", title)
|
72
|
+
return None
|
73
|
+
|
74
|
+
async def _get_title_properties(
|
75
|
+
self, database_id: str, title: str
|
76
|
+
) -> Optional[Dict[str, NotionFormattedValue]]:
|
77
|
+
"""
|
78
|
+
Determines the title property for a database and formats the value.
|
79
|
+
"""
|
80
|
+
if not await self._db_schema.load():
|
81
|
+
self.logger.error("Could not load database schema for %s", database_id)
|
82
|
+
return None
|
83
|
+
|
84
|
+
property_types = await self._db_schema.get_property_types()
|
85
|
+
|
86
|
+
title_prop_name: Optional[str] = None
|
87
|
+
for name, prop_type in property_types.items():
|
88
|
+
if prop_type == "title":
|
89
|
+
title_prop_name = name
|
90
|
+
break
|
91
|
+
|
92
|
+
if not title_prop_name:
|
93
|
+
self.logger.error("No title property found in database %s", database_id)
|
94
|
+
return None
|
95
|
+
|
96
|
+
formatted_title = self._formatter.format_value("title", title)
|
97
|
+
if not formatted_title:
|
98
|
+
self.logger.error("Could not format title '%s'", title)
|
99
|
+
return None
|
100
|
+
|
101
|
+
return {title_prop_name: cast(NotionFormattedValue, formatted_title)}
|
102
|
+
|
103
|
+
|
104
|
+
class DatabaseWritter(LoggingMixin):
|
105
|
+
"""
|
106
|
+
Enhanced class for creating and updating pages in Notion databases.
|
107
|
+
Supports both simple properties and relations.
|
108
|
+
"""
|
109
|
+
|
110
|
+
def __init__(
|
111
|
+
self, client: NotionClient, db_schema: Optional[NotionDatabaseSchema] = None
|
112
|
+
) -> None:
|
113
|
+
"""
|
114
|
+
Initialize with a NotionClient and optionally a NotionDatabaseSchema.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
client: The Notion API client
|
118
|
+
db_schema: Optional database schema instance
|
119
|
+
"""
|
120
|
+
self._client = client
|
121
|
+
self._formatter = NotionPropertyFormatter()
|
122
|
+
|
123
|
+
self._active_schema: Optional[NotionDatabaseSchema] = db_schema
|
124
|
+
self._relation_handler: Optional[NotionRelationHandler] = None
|
125
|
+
|
126
|
+
if db_schema:
|
127
|
+
self._relation_handler = NotionRelationHandler(client, db_schema)
|
128
|
+
|
129
|
+
async def _ensure_schema_for_database(self, database_id: str) -> bool:
|
130
|
+
"""
|
131
|
+
Stellt sicher, dass ein Schema für die angegebene Datenbank geladen ist.
|
132
|
+
|
133
|
+
Args:
|
134
|
+
database_id: ID der Datenbank
|
135
|
+
|
136
|
+
Returns:
|
137
|
+
True, wenn das Schema erfolgreich geladen wurde
|
138
|
+
"""
|
139
|
+
if self._active_schema and self._active_schema.database_id == database_id:
|
140
|
+
return True
|
141
|
+
|
142
|
+
self._active_schema = NotionDatabaseSchema(database_id, self._client)
|
143
|
+
self._relation_handler = NotionRelationHandler(
|
144
|
+
self._client, self._active_schema
|
145
|
+
)
|
146
|
+
|
147
|
+
return await self._active_schema.load()
|
148
|
+
|
149
|
+
async def create_page(
|
150
|
+
self,
|
151
|
+
database_id: str,
|
152
|
+
properties: Dict[str, Any],
|
153
|
+
relations: Optional[Dict[str, Union[str, List[str]]]] = None,
|
154
|
+
) -> Optional[PageCreationResponse]:
|
155
|
+
"""
|
156
|
+
Creates a new page in a database with support for relations.
|
157
|
+
"""
|
158
|
+
# Stelle sicher, dass wir ein Schema für diese Datenbank haben
|
159
|
+
if not await self._ensure_schema_for_database(database_id):
|
160
|
+
self.logger.error("Could not load schema for database %s", database_id)
|
161
|
+
return None
|
162
|
+
|
163
|
+
formatted_props = await self._format_properties(database_id, properties)
|
164
|
+
if not formatted_props:
|
165
|
+
return None
|
166
|
+
|
167
|
+
if relations:
|
168
|
+
relation_props = await self._process_relations(database_id, relations)
|
169
|
+
if relation_props:
|
170
|
+
formatted_props.update(relation_props)
|
171
|
+
|
172
|
+
data: Dict[str, Any] = {
|
173
|
+
"parent": {"database_id": database_id},
|
174
|
+
"properties": formatted_props,
|
175
|
+
}
|
176
|
+
|
177
|
+
result = await self._client.post("pages", data)
|
178
|
+
if not result:
|
179
|
+
self.logger.error("Error creating page in database %s", database_id)
|
180
|
+
return None
|
181
|
+
|
182
|
+
self.logger.info("Page successfully created in database %s", database_id)
|
183
|
+
return cast(PageCreationResponse, result)
|
184
|
+
|
185
|
+
async def update_page(
|
186
|
+
self,
|
187
|
+
page_id: str,
|
188
|
+
properties: Optional[Dict[str, Any]] = None,
|
189
|
+
relations: Optional[Dict[str, Union[str, List[str]]]] = None,
|
190
|
+
) -> Optional[Dict[str, Any]]:
|
191
|
+
"""
|
192
|
+
Updates a page with support for relations.
|
193
|
+
"""
|
194
|
+
page_data = await self._client.get(f"pages/{page_id}")
|
195
|
+
if (
|
196
|
+
not page_data
|
197
|
+
or "parent" not in page_data
|
198
|
+
or "database_id" not in page_data["parent"]
|
199
|
+
):
|
200
|
+
self.logger.error("Could not determine database ID for page %s", page_id)
|
201
|
+
return None
|
202
|
+
|
203
|
+
database_id = page_data["parent"]["database_id"]
|
204
|
+
|
205
|
+
# Stelle sicher, dass wir ein Schema für diese Datenbank haben
|
206
|
+
if not await self._ensure_schema_for_database(database_id):
|
207
|
+
self.logger.error("Could not load schema for database %s", database_id)
|
208
|
+
return None
|
209
|
+
|
210
|
+
if not properties and not relations:
|
211
|
+
self.logger.warning("No properties or relations specified for update")
|
212
|
+
return page_data
|
213
|
+
|
214
|
+
update_props: Dict[str, NotionFormattedValue] = {}
|
215
|
+
|
216
|
+
if properties:
|
217
|
+
formatted_props = await self._format_properties(database_id, properties)
|
218
|
+
if formatted_props:
|
219
|
+
update_props.update(formatted_props)
|
220
|
+
|
221
|
+
if relations:
|
222
|
+
relation_props = await self._process_relations(database_id, relations)
|
223
|
+
if relation_props:
|
224
|
+
update_props.update(relation_props)
|
225
|
+
|
226
|
+
if not update_props:
|
227
|
+
self.logger.warning("No valid properties to update for page %s", page_id)
|
228
|
+
return None
|
229
|
+
|
230
|
+
data = {"properties": update_props}
|
231
|
+
|
232
|
+
result = await self._client.patch(f"pages/{page_id}", data)
|
233
|
+
if not result:
|
234
|
+
self.logger.error("Error updating page %s", page_id)
|
235
|
+
return None
|
236
|
+
|
237
|
+
self.logger.info("Page %s successfully updated", page_id)
|
238
|
+
return result
|
239
|
+
|
240
|
+
async def delete_page(self, page_id: str) -> bool:
|
241
|
+
"""
|
242
|
+
Deletes a page (archives it in Notion).
|
243
|
+
"""
|
244
|
+
data = {"archived": True}
|
245
|
+
|
246
|
+
result = await self._client.patch(f"pages/{page_id}", data)
|
247
|
+
if not result:
|
248
|
+
self.logger.error("Error deleting page %s", page_id)
|
249
|
+
return False
|
250
|
+
|
251
|
+
self.logger.info("Page %s successfully deleted (archived)", page_id)
|
252
|
+
return True
|
253
|
+
|
254
|
+
async def _format_properties(
|
255
|
+
self, database_id: str, properties: Dict[str, Any]
|
256
|
+
) -> Optional[Dict[str, NotionFormattedValue]]:
|
257
|
+
"""
|
258
|
+
Formats properties according to their types in the database.
|
259
|
+
"""
|
260
|
+
if not self._active_schema:
|
261
|
+
self.logger.error("No active schema available for database %s", database_id)
|
262
|
+
return None
|
263
|
+
|
264
|
+
property_types = await self._active_schema.get_property_types()
|
265
|
+
if not property_types:
|
266
|
+
self.logger.error(
|
267
|
+
"Could not get property types for database %s", database_id
|
268
|
+
)
|
269
|
+
return None
|
270
|
+
|
271
|
+
formatted_props: Dict[str, NotionFormattedValue] = {}
|
272
|
+
|
273
|
+
for prop_name, value in properties.items():
|
274
|
+
if prop_name not in property_types:
|
275
|
+
self.logger.warning(
|
276
|
+
"Property '%s' does not exist in database %s",
|
277
|
+
prop_name,
|
278
|
+
database_id,
|
279
|
+
)
|
280
|
+
continue
|
281
|
+
|
282
|
+
prop_type = property_types[prop_name]
|
283
|
+
|
284
|
+
formatted_value = self._formatter.format_value(prop_type, value)
|
285
|
+
if formatted_value:
|
286
|
+
formatted_props[prop_name] = cast(NotionFormattedValue, formatted_value)
|
287
|
+
else:
|
288
|
+
self.logger.warning(
|
289
|
+
"Could not format value for property '%s' of type '%s'",
|
290
|
+
prop_name,
|
291
|
+
prop_type,
|
292
|
+
)
|
293
|
+
|
294
|
+
return formatted_props
|
295
|
+
|
296
|
+
async def _process_relations(
|
297
|
+
self, database_id: str, relations: Dict[str, Union[str, List[str]]]
|
298
|
+
) -> Dict[str, NotionRelationProperty]:
|
299
|
+
"""
|
300
|
+
Processes relation properties and converts titles to IDs.
|
301
|
+
"""
|
302
|
+
if not self._relation_handler:
|
303
|
+
self.logger.error("No relation handler available")
|
304
|
+
return {}
|
305
|
+
|
306
|
+
formatted_relations: Dict[str, NotionRelationProperty] = {}
|
307
|
+
property_types = (
|
308
|
+
await self._active_schema.get_property_types()
|
309
|
+
if self._active_schema
|
310
|
+
else {}
|
311
|
+
)
|
312
|
+
|
313
|
+
for prop_name, titles in relations.items():
|
314
|
+
relation_property = await self._process_single_relation(
|
315
|
+
database_id, prop_name, titles, property_types
|
316
|
+
)
|
317
|
+
if relation_property:
|
318
|
+
formatted_relations[prop_name] = relation_property
|
319
|
+
|
320
|
+
return formatted_relations
|
321
|
+
|
322
|
+
async def _process_single_relation(
|
323
|
+
self,
|
324
|
+
database_id: str,
|
325
|
+
prop_name: str,
|
326
|
+
titles: Union[str, List[str]],
|
327
|
+
property_types: Dict[str, str],
|
328
|
+
) -> Optional[NotionRelationProperty]:
|
329
|
+
"""
|
330
|
+
Process a single relation property and convert titles to IDs.
|
331
|
+
|
332
|
+
Args:
|
333
|
+
database_id: The database ID
|
334
|
+
prop_name: The property name
|
335
|
+
titles: The title or list of titles to convert
|
336
|
+
property_types: Dictionary of property types
|
337
|
+
|
338
|
+
Returns:
|
339
|
+
A formatted relation property or None if invalid
|
340
|
+
"""
|
341
|
+
if prop_name not in property_types:
|
342
|
+
self.logger.warning(
|
343
|
+
"Property '%s' does not exist in database %s", prop_name, database_id
|
344
|
+
)
|
345
|
+
return None
|
346
|
+
|
347
|
+
prop_type = property_types[prop_name]
|
348
|
+
if prop_type != "relation":
|
349
|
+
self.logger.warning(
|
350
|
+
"Property '%s' is not a relation (type: %s)", prop_name, prop_type
|
351
|
+
)
|
352
|
+
return None
|
353
|
+
|
354
|
+
title_list: List[str] = [titles] if isinstance(titles, str) else titles
|
355
|
+
relation_ids = await self._get_relation_ids(database_id, prop_name, title_list)
|
356
|
+
|
357
|
+
if not relation_ids:
|
358
|
+
return None
|
359
|
+
|
360
|
+
return {"relation": [{"id": rel_id} for rel_id in relation_ids]}
|
361
|
+
|
362
|
+
async def _get_relation_ids(
|
363
|
+
self, database_id: str, prop_name: str, titles: List[str]
|
364
|
+
) -> List[str]:
|
365
|
+
"""
|
366
|
+
Get relation IDs for a list of titles.
|
367
|
+
|
368
|
+
Args:
|
369
|
+
database_id: The database ID
|
370
|
+
prop_name: The property name
|
371
|
+
titles: List of titles to convert
|
372
|
+
|
373
|
+
Returns:
|
374
|
+
List of relation IDs
|
375
|
+
"""
|
376
|
+
relation_ids: List[str] = []
|
377
|
+
|
378
|
+
for title in titles:
|
379
|
+
relation_id = await self._relation_handler.find_relation_by_title(
|
380
|
+
database_id, prop_name, title
|
381
|
+
)
|
382
|
+
|
383
|
+
if relation_id:
|
384
|
+
relation_ids.append(relation_id)
|
385
|
+
else:
|
386
|
+
self.logger.warning(
|
387
|
+
"Could not find relation ID for '%s' in '%s'", title, prop_name
|
388
|
+
)
|
389
|
+
|
390
|
+
return relation_ids
|
@@ -0,0 +1,161 @@
|
|
1
|
+
from typing import Dict, Optional, Any, List, Union, TYPE_CHECKING
|
2
|
+
from notionary.core.database.models.page_result import PageResult
|
3
|
+
from notionary.core.database.notion_database_schema import NotionDatabaseSchema
|
4
|
+
from notionary.core.database.notion_database_writer import DatabaseWritter
|
5
|
+
from notionary.core.notion_client import NotionClient
|
6
|
+
|
7
|
+
from notionary.core.page.notion_page_manager import NotionPageManager
|
8
|
+
from notionary.exceptions.database_exceptions import (
|
9
|
+
PageNotFoundException,
|
10
|
+
PageOperationError,
|
11
|
+
)
|
12
|
+
from notionary.exceptions.page_creation_exception import PageCreationException
|
13
|
+
from notionary.util.uuid_utils import format_uuid
|
14
|
+
|
15
|
+
|
16
|
+
class DatabasePageService:
|
17
|
+
"""Service für den Umgang mit Datenbankseiten"""
|
18
|
+
|
19
|
+
def __init__(
|
20
|
+
self,
|
21
|
+
client: NotionClient,
|
22
|
+
schema: NotionDatabaseSchema,
|
23
|
+
writer: DatabaseWritter,
|
24
|
+
):
|
25
|
+
self._client = client
|
26
|
+
self._schema = schema
|
27
|
+
self._writer = writer
|
28
|
+
|
29
|
+
def _format_page_id(self, page_id: str) -> str:
|
30
|
+
"""
|
31
|
+
Format a page ID to ensure it's in the correct format.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
page_id: The page ID to format
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
The formatted page ID
|
38
|
+
"""
|
39
|
+
return format_uuid(page_id) or page_id
|
40
|
+
|
41
|
+
async def create_page(
|
42
|
+
self,
|
43
|
+
database_id: str,
|
44
|
+
properties: Dict[str, Any],
|
45
|
+
relations: Optional[Dict[str, Union[str, List[str]]]] = None,
|
46
|
+
) -> PageResult:
|
47
|
+
"""
|
48
|
+
Create a new page in the database.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
database_id: The database ID to create the page in
|
52
|
+
properties: Dictionary of property names and values
|
53
|
+
relations: Optional dictionary of relation property names and titles
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
Result object with success status and page information
|
57
|
+
"""
|
58
|
+
try:
|
59
|
+
response = await self._writer.create_page(
|
60
|
+
database_id, properties, relations
|
61
|
+
)
|
62
|
+
|
63
|
+
if not response:
|
64
|
+
return {
|
65
|
+
"success": False,
|
66
|
+
"message": f"Failed to create page in database {database_id}",
|
67
|
+
}
|
68
|
+
|
69
|
+
page_id = response.get("id", "")
|
70
|
+
page_url = response.get("url", None)
|
71
|
+
|
72
|
+
return {"success": True, "page_id": page_id, "url": page_url}
|
73
|
+
|
74
|
+
except PageCreationException as e:
|
75
|
+
return {"success": False, "message": str(e)}
|
76
|
+
|
77
|
+
async def update_page(
|
78
|
+
self,
|
79
|
+
page_id: str,
|
80
|
+
properties: Optional[Dict[str, Any]] = None,
|
81
|
+
relations: Optional[Dict[str, Union[str, List[str]]]] = None,
|
82
|
+
) -> PageResult:
|
83
|
+
"""
|
84
|
+
Update an existing page.
|
85
|
+
|
86
|
+
Args:
|
87
|
+
page_id: The ID of the page to update
|
88
|
+
properties: Dictionary of property names and values to update
|
89
|
+
relations: Optional dictionary of relation property names and titles
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
Result object with success status and message
|
93
|
+
"""
|
94
|
+
try:
|
95
|
+
formatted_page_id = self._format_page_id(page_id)
|
96
|
+
|
97
|
+
response = await self._writer.update_page(
|
98
|
+
formatted_page_id, properties, relations
|
99
|
+
)
|
100
|
+
|
101
|
+
if not response:
|
102
|
+
return {
|
103
|
+
"success": False,
|
104
|
+
"message": f"Failed to update page {formatted_page_id}",
|
105
|
+
}
|
106
|
+
|
107
|
+
return {"success": True, "page_id": formatted_page_id}
|
108
|
+
|
109
|
+
except PageOperationError as e:
|
110
|
+
return {"success": False, "message": f"Error: {str(e)}"}
|
111
|
+
|
112
|
+
async def delete_page(self, page_id: str) -> PageResult:
|
113
|
+
"""
|
114
|
+
Delete (archive) a page.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
page_id: The ID of the page to delete
|
118
|
+
|
119
|
+
Returns:
|
120
|
+
Result object with success status and message
|
121
|
+
"""
|
122
|
+
try:
|
123
|
+
formatted_page_id = self._format_page_id(page_id)
|
124
|
+
|
125
|
+
success = await self._writer.delete_page(formatted_page_id)
|
126
|
+
|
127
|
+
if not success:
|
128
|
+
return {
|
129
|
+
"success": False,
|
130
|
+
"message": f"Failed to delete page {formatted_page_id}",
|
131
|
+
}
|
132
|
+
|
133
|
+
return {"success": True, "page_id": formatted_page_id}
|
134
|
+
|
135
|
+
except PageOperationError as e:
|
136
|
+
return {"success": False, "message": f"Error: {str(e)}"}
|
137
|
+
|
138
|
+
async def get_page_manager(self, page_id: str) -> Optional[NotionPageManager]:
|
139
|
+
"""
|
140
|
+
Get a NotionPageManager for a specific page.
|
141
|
+
|
142
|
+
Args:
|
143
|
+
page_id: The ID of the page
|
144
|
+
|
145
|
+
Returns:
|
146
|
+
NotionPageManager instance or None if the page wasn't found
|
147
|
+
"""
|
148
|
+
formatted_page_id = self._format_page_id(page_id)
|
149
|
+
|
150
|
+
try:
|
151
|
+
page_data = await self._client.get(f"pages/{formatted_page_id}")
|
152
|
+
|
153
|
+
if not page_data:
|
154
|
+
return None
|
155
|
+
|
156
|
+
return NotionPageManager(
|
157
|
+
page_id=formatted_page_id, url=page_data.get("url")
|
158
|
+
)
|
159
|
+
|
160
|
+
except PageNotFoundException:
|
161
|
+
return None
|
@@ -0,0 +1,134 @@
|
|
1
|
+
import asyncio
|
2
|
+
import os
|
3
|
+
from enum import Enum
|
4
|
+
from typing import Dict, Any, Optional, Union
|
5
|
+
import httpx
|
6
|
+
from dotenv import load_dotenv
|
7
|
+
from notionary.util.logging_mixin import LoggingMixin
|
8
|
+
import weakref
|
9
|
+
|
10
|
+
|
11
|
+
class HttpMethod(Enum):
|
12
|
+
"""Enum für HTTP-Methoden."""
|
13
|
+
|
14
|
+
GET = "get"
|
15
|
+
POST = "post"
|
16
|
+
PATCH = "patch"
|
17
|
+
DELETE = "delete"
|
18
|
+
|
19
|
+
|
20
|
+
class NotionClient(LoggingMixin):
|
21
|
+
"""Verbesserter Notion-Client mit automatischer Ressourcenverwaltung."""
|
22
|
+
|
23
|
+
BASE_URL = "https://api.notion.com/v1"
|
24
|
+
NOTION_VERSION = "2022-06-28"
|
25
|
+
_instances = weakref.WeakSet()
|
26
|
+
|
27
|
+
def __init__(self, token: Optional[str] = None, timeout: int = 30):
|
28
|
+
load_dotenv()
|
29
|
+
self.token = token or os.getenv("NOTION_SECRET", "")
|
30
|
+
if not self.token:
|
31
|
+
raise ValueError("Notion API token is required")
|
32
|
+
|
33
|
+
self.headers = {
|
34
|
+
"Authorization": f"Bearer {self.token}",
|
35
|
+
"Content-Type": "application/json",
|
36
|
+
"Notion-Version": self.NOTION_VERSION,
|
37
|
+
}
|
38
|
+
|
39
|
+
self.client = httpx.AsyncClient(headers=self.headers, timeout=timeout)
|
40
|
+
|
41
|
+
self._instances.add(self)
|
42
|
+
|
43
|
+
@classmethod
|
44
|
+
async def close_all(cls):
|
45
|
+
for instance in list(cls._instances):
|
46
|
+
await instance.close()
|
47
|
+
|
48
|
+
async def close(self):
|
49
|
+
if hasattr(self, "client") and self.client:
|
50
|
+
await self.client.aclose()
|
51
|
+
self.client = None
|
52
|
+
|
53
|
+
async def get(self, endpoint: str) -> Optional[Dict[str, Any]]:
|
54
|
+
return await self._make_request(HttpMethod.GET, endpoint)
|
55
|
+
|
56
|
+
async def post(
|
57
|
+
self, endpoint: str, data: Optional[Dict[str, Any]] = None
|
58
|
+
) -> Optional[Dict[str, Any]]:
|
59
|
+
return await self._make_request(HttpMethod.POST, endpoint, data)
|
60
|
+
|
61
|
+
async def patch(
|
62
|
+
self, endpoint: str, data: Optional[Dict[str, Any]] = None
|
63
|
+
) -> Optional[Dict[str, Any]]:
|
64
|
+
return await self._make_request(HttpMethod.PATCH, endpoint, data)
|
65
|
+
|
66
|
+
async def delete(self, endpoint: str) -> bool:
|
67
|
+
result = await self._make_request(HttpMethod.DELETE, endpoint)
|
68
|
+
return result is not None
|
69
|
+
|
70
|
+
async def _make_request(
|
71
|
+
self,
|
72
|
+
method: Union[HttpMethod, str],
|
73
|
+
endpoint: str,
|
74
|
+
data: Optional[Dict[str, Any]] = None,
|
75
|
+
) -> Optional[Dict[str, Any]]:
|
76
|
+
"""
|
77
|
+
Führt eine HTTP-Anfrage aus und gibt direkt die Daten zurück oder None bei Fehler.
|
78
|
+
"""
|
79
|
+
url = f"{self.BASE_URL}/{endpoint.lstrip('/')}"
|
80
|
+
method_str = (
|
81
|
+
method.value if isinstance(method, HttpMethod) else str(method).lower()
|
82
|
+
)
|
83
|
+
|
84
|
+
try:
|
85
|
+
self.logger.debug("Sending %s request to %s", method_str.upper(), url)
|
86
|
+
|
87
|
+
if (
|
88
|
+
method_str in [HttpMethod.POST.value, HttpMethod.PATCH.value]
|
89
|
+
and data is not None
|
90
|
+
):
|
91
|
+
response = await getattr(self.client, method_str)(url, json=data)
|
92
|
+
else:
|
93
|
+
response = await getattr(self.client, method_str)(url)
|
94
|
+
|
95
|
+
response.raise_for_status()
|
96
|
+
result_data = response.json()
|
97
|
+
self.logger.debug("Request successful: %s", url)
|
98
|
+
return result_data
|
99
|
+
|
100
|
+
except httpx.HTTPStatusError as e:
|
101
|
+
error_msg = (
|
102
|
+
f"HTTP status error: {e.response.status_code} - {e.response.text}"
|
103
|
+
)
|
104
|
+
self.logger.error("Request failed (%s): %s", url, error_msg)
|
105
|
+
return None
|
106
|
+
|
107
|
+
except httpx.RequestError as e:
|
108
|
+
error_msg = f"Request error: {str(e)}"
|
109
|
+
self.logger.error("Request error (%s): %s", url, error_msg)
|
110
|
+
return None
|
111
|
+
|
112
|
+
def __del__(self):
|
113
|
+
"""
|
114
|
+
Destruktor, der beim Garbage Collecting aufgerufen wird.
|
115
|
+
|
116
|
+
Hinweis: Dies ist nur ein Fallback, da __del__ nicht garantiert für async Cleanup funktioniert.
|
117
|
+
Die bessere Praxis ist, close() explizit zu rufen, wenn möglich.
|
118
|
+
"""
|
119
|
+
if not hasattr(self, "client") or not self.client:
|
120
|
+
return
|
121
|
+
|
122
|
+
try:
|
123
|
+
loop = asyncio.get_event_loop()
|
124
|
+
if not loop.is_running():
|
125
|
+
self.logger.warning(
|
126
|
+
"Event loop not running, could not auto-close NotionClient"
|
127
|
+
)
|
128
|
+
return
|
129
|
+
|
130
|
+
# Versuche, Cleanup Task zu erstellen
|
131
|
+
loop.create_task(self.close())
|
132
|
+
self.logger.debug("Created cleanup task for NotionClient")
|
133
|
+
except RuntimeError:
|
134
|
+
self.logger.warning("No event loop available for auto-closing NotionClient")
|