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.
Files changed (64) hide show
  1. {notionary-0.1.6 → notionary-0.1.7}/PKG-INFO +1 -1
  2. notionary-0.1.7/notionary/core/database/notion_database_manager.py +246 -0
  3. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/database/notion_database_manager_factory.py +9 -52
  4. notionary-0.1.7/notionary/core/database/notion_database_schema.py +104 -0
  5. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/notion_client.py +2 -10
  6. {notionary-0.1.6/notionary/core/page → notionary-0.1.7/notionary/core/page/content}/page_content_manager.py +0 -1
  7. notionary-0.1.7/notionary/core/page/metadata/metadata_editor.py +109 -0
  8. notionary-0.1.7/notionary/core/page/metadata/notion_icon_manager.py +46 -0
  9. 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
  10. notionary-0.1.7/notionary/core/page/notion_page_manager.py +310 -0
  11. notionary-0.1.7/notionary/core/page/properites/database_property_service.py +330 -0
  12. notionary-0.1.7/notionary/core/page/properites/page_property_manager.py +146 -0
  13. {notionary-0.1.6/notionary/core/page → notionary-0.1.7/notionary/core/page/properites}/property_formatter.py +19 -20
  14. notionary-0.1.7/notionary/core/page/properites/property_operation_result.py +103 -0
  15. notionary-0.1.7/notionary/core/page/properites/property_value_extractor.py +46 -0
  16. notionary-0.1.7/notionary/core/page/relations/notion_page_relation_manager.py +364 -0
  17. notionary-0.1.7/notionary/core/page/relations/notion_page_title_resolver.py +43 -0
  18. notionary-0.1.7/notionary/core/page/relations/page_database_relation.py +70 -0
  19. notionary-0.1.7/notionary/core/page/relations/relation_operation_result.py +135 -0
  20. notionary-0.1.6/notionary/util/uuid_utils.py → notionary-0.1.7/notionary/util/page_id_utils.py +15 -0
  21. {notionary-0.1.6 → notionary-0.1.7}/notionary.egg-info/PKG-INFO +1 -1
  22. {notionary-0.1.6 → notionary-0.1.7}/notionary.egg-info/SOURCES.txt +15 -9
  23. {notionary-0.1.6 → notionary-0.1.7}/setup.py +1 -1
  24. notionary-0.1.6/notionary/core/database/database_query_service.py +0 -73
  25. notionary-0.1.6/notionary/core/database/database_schema_service.py +0 -57
  26. notionary-0.1.6/notionary/core/database/notion_database_manager.py +0 -332
  27. notionary-0.1.6/notionary/core/database/notion_database_schema.py +0 -417
  28. notionary-0.1.6/notionary/core/database/notion_database_writer.py +0 -390
  29. notionary-0.1.6/notionary/core/database/page_service.py +0 -161
  30. notionary-0.1.6/notionary/core/page/notion_page_manager.py +0 -151
  31. {notionary-0.1.6 → notionary-0.1.7}/LICENSE +0 -0
  32. {notionary-0.1.6 → notionary-0.1.7}/README.md +0 -0
  33. {notionary-0.1.6 → notionary-0.1.7}/notionary/__init__.py +0 -0
  34. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/__init__.py +0 -0
  35. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/bookmark_element.py +0 -0
  36. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/callout_element.py +0 -0
  37. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/code_block_element.py +0 -0
  38. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/column_element.py +0 -0
  39. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/divider_element.py +0 -0
  40. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/heading_element.py +0 -0
  41. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/image_element.py +0 -0
  42. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/list_element.py +0 -0
  43. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/notion_block_element.py +0 -0
  44. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/paragraph_element.py +0 -0
  45. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/qoute_element.py +0 -0
  46. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/table_element.py +0 -0
  47. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/text_inline_formatter.py +0 -0
  48. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/todo_lists.py +0 -0
  49. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/toggle_element.py +0 -0
  50. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/elements/video_element.py +0 -0
  51. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/markdown_to_notion_converter.py +0 -0
  52. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/notion_to_markdown_converter.py +0 -0
  53. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/registry/block_element_registry.py +0 -0
  54. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/converters/registry/block_element_registry_builder.py +0 -0
  55. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/database/database_info_service.py +0 -0
  56. {notionary-0.1.6 → notionary-0.1.7}/notionary/core/database/models/page_result.py +0 -0
  57. {notionary-0.1.6 → notionary-0.1.7}/notionary/exceptions/database_exceptions.py +0 -0
  58. {notionary-0.1.6 → notionary-0.1.7}/notionary/exceptions/page_creation_exception.py +0 -0
  59. {notionary-0.1.6 → notionary-0.1.7}/notionary/util/logging_mixin.py +0 -0
  60. {notionary-0.1.6 → notionary-0.1.7}/notionary/util/singleton_decorator.py +0 -0
  61. {notionary-0.1.6 → notionary-0.1.7}/notionary.egg-info/dependency_links.txt +0 -0
  62. {notionary-0.1.6 → notionary-0.1.7}/notionary.egg-info/requires.txt +0 -0
  63. {notionary-0.1.6 → notionary-0.1.7}/notionary.egg-info/top_level.txt +0 -0
  64. {notionary-0.1.6 → notionary-0.1.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: notionary
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: A toolkit to convert between Markdown and Notion blocks
5
5
  Home-page: https://github.com/mathisarends/notionary
6
6
  Author: Mathis Arends
@@ -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()
@@ -12,7 +12,7 @@ from notionary.exceptions.database_exceptions import (
12
12
  NotionDatabaseException,
13
13
  )
14
14
  from notionary.util.logging_mixin import LoggingMixin
15
- from notionary.util.uuid_utils import format_uuid
15
+ from notionary.util.page_id_utils import format_uuid
16
16
 
17
17
 
18
18
  class NotionDatabaseFactory(LoggingMixin):
@@ -44,21 +44,11 @@ class NotionDatabaseFactory(LoggingMixin):
44
44
 
45
45
  try:
46
46
  formatted_id = format_uuid(database_id) or database_id
47
-
47
+
48
48
  manager = NotionDatabaseManager(formatted_id, token)
49
+
49
50
 
50
- success = await manager.initialize()
51
-
52
- if not success:
53
- error_msg = (
54
- f"Failed to initialize database manager for ID: {formatted_id}"
55
- )
56
- logger.error(error_msg)
57
- raise DatabaseInitializationError(formatted_id, error_msg)
58
-
59
- logger.info(
60
- lambda: f"Successfully created database manager for ID: {formatted_id}"
61
- )
51
+ logger.info("Successfully created database manager for ID: %s", formatted_id)
62
52
  return manager
63
53
 
64
54
  except DatabaseInitializationError:
@@ -88,7 +78,7 @@ class NotionDatabaseFactory(LoggingMixin):
88
78
  An initialized NotionDatabaseManager instance
89
79
  """
90
80
  logger = cls.class_logger()
91
- logger.debug(lambda: f"Searching for database with name: {database_name}")
81
+ logger.debug("Searching for database with name: %s", database_name)
92
82
 
93
83
  client = NotionClient(token=token)
94
84
 
@@ -116,11 +106,8 @@ class NotionDatabaseFactory(LoggingMixin):
116
106
  logger.warning(error_msg)
117
107
  raise DatabaseNotFoundException(database_name, error_msg)
118
108
 
119
- logger.debug(
120
- lambda: f"Found {len(databases)} databases, searching for best match"
121
- )
109
+ logger.debug("Found %d databases, searching for best match", len(databases))
122
110
 
123
- # Find best match using fuzzy matching
124
111
  best_match = None
125
112
  best_score = 0
126
113
 
@@ -135,7 +122,6 @@ class NotionDatabaseFactory(LoggingMixin):
135
122
  best_score = score
136
123
  best_match = db
137
124
 
138
- # Use a minimum threshold for match quality (0.6 = 60% similarity)
139
125
  if best_score < 0.6 or not best_match:
140
126
  error_msg = f"No good database name match found for '{database_name}'. Best match had score {best_score:.2f}"
141
127
  logger.warning(error_msg)
@@ -150,23 +136,11 @@ class NotionDatabaseFactory(LoggingMixin):
150
136
 
151
137
  matched_name = cls._extract_title_from_database(best_match)
152
138
 
153
- logger.info(
154
- lambda: f"Found matching database: '{matched_name}' (ID: {database_id}) with score: {best_score:.2f}"
155
- )
139
+ logger.info("Found matching database: '%s' (ID: %s) with score: %.2f", matched_name, database_id, best_score)
156
140
 
157
141
  manager = NotionDatabaseManager(database_id, token)
158
- success = await manager.initialize()
159
142
 
160
- if not success:
161
- error_msg = (
162
- f"Failed to initialize database manager for database {database_id}"
163
- )
164
- logger.error(error_msg)
165
- raise DatabaseInitializationError(database_id, error_msg)
166
-
167
- logger.info(
168
- lambda: f"Successfully created database manager for '{matched_name}'"
169
- )
143
+ logger.info(f"Successfully created database manager for '{matched_name}'")
170
144
  await client.close()
171
145
  return manager
172
146
 
@@ -183,22 +157,11 @@ class NotionDatabaseFactory(LoggingMixin):
183
157
  def _extract_title_from_database(cls, database: Dict[str, Any]) -> str:
184
158
  """
185
159
  Extract the title from a database object.
186
-
187
- Args:
188
- database: A database object from the Notion API
189
-
190
- Returns:
191
- The extracted title or "Untitled" if no title is found
192
-
193
- Raises:
194
- DatabaseParsingError: If there's an error parsing the database title
195
160
  """
196
161
  try:
197
- # Check for title in the root object
198
162
  if "title" in database:
199
163
  return cls._extract_text_from_rich_text(database["title"])
200
164
 
201
- # Check for title in properties
202
165
  if "properties" in database and "title" in database["properties"]:
203
166
  title_prop = database["properties"]["title"]
204
167
  if "title" in title_prop:
@@ -215,12 +178,6 @@ class NotionDatabaseFactory(LoggingMixin):
215
178
  def _extract_text_from_rich_text(cls, rich_text: List[Dict[str, Any]]) -> str:
216
179
  """
217
180
  Extract plain text from a rich text array.
218
-
219
- Args:
220
- rich_text: A list of rich text objects from Notion API
221
-
222
- Returns:
223
- The concatenated plain text content
224
181
  """
225
182
  if not rich_text:
226
183
  return ""
@@ -230,4 +187,4 @@ class NotionDatabaseFactory(LoggingMixin):
230
187
  if "plain_text" in text_obj:
231
188
  text_parts.append(text_obj["plain_text"])
232
189
 
233
- return "".join(text_parts)
190
+ return "".join(text_parts)
@@ -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
+