notionary 0.1.1__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.
Files changed (51) hide show
  1. notionary/__init__.py +9 -0
  2. notionary/core/__init__.py +0 -0
  3. notionary/core/converters/__init__.py +50 -0
  4. notionary/core/converters/elements/__init__.py +0 -0
  5. notionary/core/converters/elements/bookmark_element.py +224 -0
  6. notionary/core/converters/elements/callout_element.py +179 -0
  7. notionary/core/converters/elements/code_block_element.py +153 -0
  8. notionary/core/converters/elements/column_element.py +294 -0
  9. notionary/core/converters/elements/divider_element.py +73 -0
  10. notionary/core/converters/elements/heading_element.py +84 -0
  11. notionary/core/converters/elements/image_element.py +130 -0
  12. notionary/core/converters/elements/list_element.py +130 -0
  13. notionary/core/converters/elements/notion_block_element.py +51 -0
  14. notionary/core/converters/elements/paragraph_element.py +73 -0
  15. notionary/core/converters/elements/qoute_element.py +242 -0
  16. notionary/core/converters/elements/table_element.py +306 -0
  17. notionary/core/converters/elements/text_inline_formatter.py +294 -0
  18. notionary/core/converters/elements/todo_lists.py +114 -0
  19. notionary/core/converters/elements/toggle_element.py +205 -0
  20. notionary/core/converters/elements/video_element.py +159 -0
  21. notionary/core/converters/markdown_to_notion_converter.py +482 -0
  22. notionary/core/converters/notion_to_markdown_converter.py +45 -0
  23. notionary/core/converters/registry/__init__.py +0 -0
  24. notionary/core/converters/registry/block_element_registry.py +234 -0
  25. notionary/core/converters/registry/block_element_registry_builder.py +280 -0
  26. notionary/core/database/database_info_service.py +43 -0
  27. notionary/core/database/database_query_service.py +73 -0
  28. notionary/core/database/database_schema_service.py +57 -0
  29. notionary/core/database/models/page_result.py +10 -0
  30. notionary/core/database/notion_database_manager.py +332 -0
  31. notionary/core/database/notion_database_manager_factory.py +233 -0
  32. notionary/core/database/notion_database_schema.py +415 -0
  33. notionary/core/database/notion_database_writer.py +390 -0
  34. notionary/core/database/page_service.py +161 -0
  35. notionary/core/notion_client.py +134 -0
  36. notionary/core/page/meta_data/metadata_editor.py +37 -0
  37. notionary/core/page/notion_page_manager.py +110 -0
  38. notionary/core/page/page_content_manager.py +85 -0
  39. notionary/core/page/property_formatter.py +97 -0
  40. notionary/exceptions/database_exceptions.py +76 -0
  41. notionary/exceptions/page_creation_exception.py +9 -0
  42. notionary/util/logging_mixin.py +47 -0
  43. notionary/util/singleton_decorator.py +20 -0
  44. notionary/util/uuid_utils.py +24 -0
  45. {notionary-0.1.1.dist-info → notionary-0.1.3.dist-info}/METADATA +1 -1
  46. notionary-0.1.3.dist-info/RECORD +49 -0
  47. notionary-0.1.3.dist-info/top_level.txt +1 -0
  48. notionary-0.1.1.dist-info/RECORD +0 -5
  49. notionary-0.1.1.dist-info/top_level.txt +0 -1
  50. {notionary-0.1.1.dist-info → notionary-0.1.3.dist-info}/WHEEL +0 -0
  51. {notionary-0.1.1.dist-info → notionary-0.1.3.dist-info}/licenses/LICENSE +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")