notionary 0.1.11__py3-none-any.whl → 0.1.12__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 (32) hide show
  1. notionary/__init__.py +13 -2
  2. notionary/core/converters/elements/audio_element.py +6 -4
  3. notionary/core/converters/elements/embed_element.py +2 -4
  4. notionary/core/converters/elements/toggle_element.py +28 -20
  5. notionary/core/converters/markdown_to_notion_converter.py +70 -109
  6. notionary/core/converters/registry/block_element_registry.py +3 -3
  7. notionary/core/database/database_discovery.py +140 -0
  8. notionary/core/database/notion_database_manager.py +26 -49
  9. notionary/core/database/notion_database_manager_factory.py +10 -4
  10. notionary/core/notion_client.py +4 -2
  11. notionary/core/page/content/notion_page_content_chunker.py +84 -0
  12. notionary/core/page/content/page_content_manager.py +26 -8
  13. notionary/core/page/metadata/metadata_editor.py +57 -44
  14. notionary/core/page/metadata/notion_icon_manager.py +9 -11
  15. notionary/core/page/metadata/notion_page_cover_manager.py +15 -20
  16. notionary/core/page/notion_page_manager.py +139 -149
  17. notionary/core/page/properites/database_property_service.py +114 -98
  18. notionary/core/page/properites/page_property_manager.py +78 -49
  19. notionary/core/page/properites/property_formatter.py +1 -1
  20. notionary/core/page/properites/property_operation_result.py +43 -30
  21. notionary/core/page/properites/property_value_extractor.py +26 -8
  22. notionary/core/page/relations/notion_page_relation_manager.py +71 -52
  23. notionary/core/page/relations/notion_page_title_resolver.py +11 -11
  24. notionary/core/page/relations/page_database_relation.py +14 -14
  25. notionary/core/page/relations/relation_operation_result.py +50 -41
  26. notionary/util/page_id_utils.py +11 -7
  27. {notionary-0.1.11.dist-info → notionary-0.1.12.dist-info}/METADATA +1 -1
  28. {notionary-0.1.11.dist-info → notionary-0.1.12.dist-info}/RECORD +31 -30
  29. notionary/core/database/notion_database_schema.py +0 -104
  30. {notionary-0.1.11.dist-info → notionary-0.1.12.dist-info}/WHEEL +0 -0
  31. {notionary-0.1.11.dist-info → notionary-0.1.12.dist-info}/licenses/LICENSE +0 -0
  32. {notionary-0.1.11.dist-info → notionary-0.1.12.dist-info}/top_level.txt +0 -0
@@ -12,7 +12,7 @@ class DatabasePropertyService(LoggingMixin):
12
12
  def __init__(self, database_id: str, client: NotionClient):
13
13
  """
14
14
  Initialize the database property service.
15
-
15
+
16
16
  Args:
17
17
  database_id: ID of the Notion database
18
18
  client: Instance of NotionClient
@@ -20,20 +20,20 @@ class DatabasePropertyService(LoggingMixin):
20
20
  self._database_id = database_id
21
21
  self._client = client
22
22
  self._schema = None
23
-
23
+
24
24
  async def load_schema(self, force_refresh=False) -> bool:
25
25
  """
26
26
  Loads the database schema.
27
-
27
+
28
28
  Args:
29
29
  force_refresh: Whether to force a refresh of the schema
30
-
30
+
31
31
  Returns:
32
32
  bool: True if schema loaded successfully, False otherwise
33
33
  """
34
34
  if self._schema is not None and not force_refresh:
35
35
  return True
36
-
36
+
37
37
  try:
38
38
  database = await self._client.get(f"databases/{self._database_id}")
39
39
  if database and "properties" in database:
@@ -41,290 +41,306 @@ class DatabasePropertyService(LoggingMixin):
41
41
  self.logger.debug("Loaded schema for database %s", self._database_id)
42
42
  return True
43
43
  else:
44
- self.logger.error("Failed to load schema: missing 'properties' in response")
44
+ self.logger.error(
45
+ "Failed to load schema: missing 'properties' in response"
46
+ )
45
47
  return False
46
48
  except Exception as e:
47
49
  self.logger.error("Error loading database schema: %s", str(e))
48
50
  return False
49
-
51
+
50
52
  async def _ensure_schema_loaded(self) -> None:
51
53
  """
52
54
  Ensures the schema is loaded before accessing it.
53
55
  """
54
56
  if self._schema is None:
55
57
  await self.load_schema()
56
-
58
+
57
59
  async def get_schema(self) -> Dict[str, Any]:
58
60
  """
59
61
  Gets the database schema.
60
-
62
+
61
63
  Returns:
62
64
  Dict[str, Any]: The database schema
63
65
  """
64
66
  await self._ensure_schema_loaded()
65
67
  return self._schema or {}
66
-
68
+
67
69
  async def get_property_types(self) -> Dict[str, str]:
68
70
  """
69
71
  Gets all property types for the database.
70
-
72
+
71
73
  Returns:
72
74
  Dict[str, str]: Dictionary mapping property names to their types
73
75
  """
74
76
  await self._ensure_schema_loaded()
75
-
77
+
76
78
  if not self._schema:
77
79
  return {}
78
-
80
+
79
81
  return {
80
82
  prop_name: prop_data.get("type", "unknown")
81
83
  for prop_name, prop_data in self._schema.items()
82
84
  }
83
-
85
+
84
86
  async def get_property_schema(self, property_name: str) -> Optional[Dict[str, Any]]:
85
87
  """
86
88
  Gets the schema for a specific property.
87
-
89
+
88
90
  Args:
89
91
  property_name: The name of the property
90
-
92
+
91
93
  Returns:
92
94
  Optional[Dict[str, Any]]: The property schema or None if not found
93
95
  """
94
96
  await self._ensure_schema_loaded()
95
-
97
+
96
98
  if not self._schema or property_name not in self._schema:
97
99
  return None
98
-
100
+
99
101
  return self._schema[property_name]
100
-
102
+
101
103
  async def get_property_type(self, property_name: str) -> Optional[str]:
102
104
  """
103
105
  Gets the type of a specific property.
104
-
106
+
105
107
  Args:
106
108
  property_name: The name of the property
107
-
109
+
108
110
  Returns:
109
111
  Optional[str]: The property type or None if not found
110
112
  """
111
113
  property_schema = await self.get_property_schema(property_name)
112
-
114
+
113
115
  if not property_schema:
114
116
  return None
115
-
117
+
116
118
  return property_schema.get("type")
117
-
119
+
118
120
  async def property_exists(self, property_name: str) -> bool:
119
121
  """
120
122
  Checks if a property exists in the database.
121
-
123
+
122
124
  Args:
123
125
  property_name: The name of the property
124
-
126
+
125
127
  Returns:
126
128
  bool: True if the property exists, False otherwise
127
129
  """
128
130
  property_schema = await self.get_property_schema(property_name)
129
131
  return property_schema is not None
130
-
132
+
131
133
  async def get_property_options(self, property_name: str) -> List[Dict[str, Any]]:
132
134
  """
133
135
  Gets the available options for a property (select, multi_select, status).
134
-
136
+
135
137
  Args:
136
138
  property_name: The name of the property
137
-
139
+
138
140
  Returns:
139
141
  List[Dict[str, Any]]: List of available options with their metadata
140
142
  """
141
143
  property_schema = await self.get_property_schema(property_name)
142
-
144
+
143
145
  if not property_schema:
144
146
  return []
145
-
147
+
146
148
  property_type = property_schema.get("type")
147
-
149
+
148
150
  if property_type in ["select", "multi_select", "status"]:
149
151
  return property_schema.get(property_type, {}).get("options", [])
150
-
152
+
151
153
  return []
152
-
154
+
153
155
  async def get_option_names(self, property_name: str) -> List[str]:
154
156
  """
155
157
  Gets the available option names for a property (select, multi_select, status).
156
-
158
+
157
159
  Args:
158
160
  property_name: The name of the property
159
-
161
+
160
162
  Returns:
161
163
  List[str]: List of available option names
162
164
  """
163
165
  options = await self.get_property_options(property_name)
164
166
  return [option.get("name", "") for option in options]
165
-
166
- async def get_relation_details(self, property_name: str) -> Optional[Dict[str, Any]]:
167
+
168
+ async def get_relation_details(
169
+ self, property_name: str
170
+ ) -> Optional[Dict[str, Any]]:
167
171
  """
168
172
  Gets details about a relation property, including the related database.
169
-
173
+
170
174
  Args:
171
175
  property_name: The name of the property
172
-
176
+
173
177
  Returns:
174
178
  Optional[Dict[str, Any]]: The relation details or None if not a relation
175
179
  """
176
180
  property_schema = await self.get_property_schema(property_name)
177
-
181
+
178
182
  if not property_schema or property_schema.get("type") != "relation":
179
183
  return None
180
-
184
+
181
185
  return property_schema.get("relation", {})
182
-
183
- async def get_relation_options(self, property_name: str, limit: int = 100) -> List[Dict[str, Any]]:
186
+
187
+ async def get_relation_options(
188
+ self, property_name: str, limit: int = 100
189
+ ) -> List[Dict[str, Any]]:
184
190
  """
185
191
  Gets available options for a relation property by querying the related database.
186
-
192
+
187
193
  Args:
188
194
  property_name: The name of the relation property
189
195
  limit: Maximum number of options to retrieve
190
-
196
+
191
197
  Returns:
192
198
  List[Dict[str, Any]]: List of pages from the related database
193
199
  """
194
200
  relation_details = await self.get_relation_details(property_name)
195
-
201
+
196
202
  if not relation_details or "database_id" not in relation_details:
197
203
  return []
198
-
204
+
199
205
  related_db_id = relation_details["database_id"]
200
-
206
+
201
207
  try:
202
208
  # Query the related database to get options
203
209
  query_result = await self._client.post(
204
210
  f"databases/{related_db_id}/query",
205
211
  {
206
212
  "page_size": limit,
207
- }
213
+ },
208
214
  )
209
-
215
+
210
216
  if not query_result or "results" not in query_result:
211
217
  return []
212
-
218
+
213
219
  # Extract relevant information from each page
214
220
  options = []
215
221
  for page in query_result["results"]:
216
222
  page_id = page.get("id")
217
223
  title = self._extract_title_from_page(page)
218
-
224
+
219
225
  if page_id and title:
220
- options.append({
221
- "id": page_id,
222
- "name": title
223
- })
224
-
226
+ options.append({"id": page_id, "name": title})
227
+
225
228
  return options
226
229
  except Exception as e:
227
230
  self.logger.error(f"Error getting relation options: {str(e)}")
228
231
  return []
229
-
232
+
230
233
  def _extract_title_from_page(self, page: Dict[str, Any]) -> Optional[str]:
231
234
  """
232
235
  Extracts the title from a page object.
233
-
236
+
234
237
  Args:
235
238
  page: The page object from Notion API
236
-
239
+
237
240
  Returns:
238
241
  Optional[str]: The page title or None if not found
239
242
  """
240
243
  if "properties" not in page:
241
244
  return None
242
-
245
+
243
246
  properties = page["properties"]
244
-
247
+
245
248
  # Look for a title property
246
249
  for prop_data in properties.values():
247
250
  if prop_data.get("type") == "title" and "title" in prop_data:
248
251
  title_parts = prop_data["title"]
249
- return "".join([text_obj.get("plain_text", "") for text_obj in title_parts])
250
-
252
+ return "".join(
253
+ [text_obj.get("plain_text", "") for text_obj in title_parts]
254
+ )
255
+
251
256
  return None
252
-
253
- async def validate_property_value(self, property_name: str, value: Any) -> Tuple[bool, Optional[str], Optional[List[str]]]:
257
+
258
+ async def validate_property_value(
259
+ self, property_name: str, value: Any
260
+ ) -> Tuple[bool, Optional[str], Optional[List[str]]]:
254
261
  """
255
262
  Validates a value for a property.
256
-
263
+
257
264
  Args:
258
265
  property_name: The name of the property
259
266
  value: The value to validate
260
-
267
+
261
268
  Returns:
262
- Tuple[bool, Optional[str], Optional[List[str]]]:
269
+ Tuple[bool, Optional[str], Optional[List[str]]]:
263
270
  - Boolean indicating if valid
264
271
  - Error message if invalid
265
272
  - Available options if applicable
266
273
  """
267
274
  property_schema = await self.get_property_schema(property_name)
268
-
275
+
269
276
  if not property_schema:
270
277
  return False, f"Property '{property_name}' does not exist", None
271
-
278
+
272
279
  property_type = property_schema.get("type")
273
-
280
+
274
281
  # Validate select, multi_select, status properties
275
282
  if property_type in ["select", "status"]:
276
283
  options = await self.get_option_names(property_name)
277
-
284
+
278
285
  if isinstance(value, str) and value not in options:
279
- return False, f"Invalid {property_type} option. Value '{value}' is not in the available options.", options
280
-
286
+ return (
287
+ False,
288
+ f"Invalid {property_type} option. Value '{value}' is not in the available options.",
289
+ options,
290
+ )
291
+
281
292
  elif property_type == "multi_select":
282
293
  options = await self.get_option_names(property_name)
283
-
294
+
284
295
  if isinstance(value, list):
285
296
  invalid_values = [val for val in value if val not in options]
286
297
  if invalid_values:
287
- return False, f"Invalid multi_select options: {', '.join(invalid_values)}", options
288
-
298
+ return (
299
+ False,
300
+ f"Invalid multi_select options: {', '.join(invalid_values)}",
301
+ options,
302
+ )
303
+
289
304
  return True, None, None
290
-
291
- async def get_database_metadata(self, include_types: Optional[List[str]] = None) -> Dict[str, Any]:
305
+
306
+ async def get_database_metadata(
307
+ self, include_types: Optional[List[str]] = None
308
+ ) -> Dict[str, Any]:
292
309
  """
293
310
  Gets the complete metadata of the database, including property options.
294
-
311
+
295
312
  Args:
296
313
  include_types: List of property types to include (if None, include all)
297
-
314
+
298
315
  Returns:
299
316
  Dict[str, Any]: The database metadata
300
317
  """
301
318
  await self._ensure_schema_loaded()
302
-
319
+
303
320
  if not self._schema:
304
321
  return {"properties": {}}
305
-
322
+
306
323
  metadata = {"properties": {}}
307
-
324
+
308
325
  for prop_name, prop_data in self._schema.items():
309
326
  prop_type = prop_data.get("type")
310
-
327
+
311
328
  # Skip if we're filtering and this type isn't included
312
329
  if include_types and prop_type not in include_types:
313
330
  continue
314
-
315
- prop_metadata = {
316
- "type": prop_type,
317
- "options": []
318
- }
319
-
331
+
332
+ prop_metadata = {"type": prop_type, "options": []}
333
+
320
334
  # Include options for select, multi_select, status
321
335
  if prop_type in ["select", "multi_select", "status"]:
322
- prop_metadata["options"] = prop_data.get(prop_type, {}).get("options", [])
323
-
336
+ prop_metadata["options"] = prop_data.get(prop_type, {}).get(
337
+ "options", []
338
+ )
339
+
324
340
  # For relation properties, we might want to include related database info
325
341
  elif prop_type == "relation":
326
342
  prop_metadata["relation_details"] = prop_data.get("relation", {})
327
-
343
+
328
344
  metadata["properties"][prop_name] = prop_metadata
329
-
330
- return metadata
345
+
346
+ return metadata
@@ -1,40 +1,53 @@
1
1
  from typing import Dict, Any, List, Optional
2
2
  from notionary.core.notion_client import NotionClient
3
3
  from notionary.core.page.metadata.metadata_editor import MetadataEditor
4
- from notionary.core.page.properites.property_operation_result import PropertyOperationResult
5
- from notionary.core.page.relations.notion_page_title_resolver import NotionPageTitleResolver
6
- from notionary.core.page.properites.database_property_service import DatabasePropertyService
4
+ from notionary.core.page.properites.property_operation_result import (
5
+ PropertyOperationResult,
6
+ )
7
+ from notionary.core.page.relations.notion_page_title_resolver import (
8
+ NotionPageTitleResolver,
9
+ )
10
+ from notionary.core.page.properites.database_property_service import (
11
+ DatabasePropertyService,
12
+ )
7
13
  from notionary.core.page.relations.page_database_relation import PageDatabaseRelation
8
- from notionary.core.page.properites.property_value_extractor import PropertyValueExtractor
14
+ from notionary.core.page.properites.property_value_extractor import (
15
+ PropertyValueExtractor,
16
+ )
9
17
  from notionary.util.logging_mixin import LoggingMixin
10
18
 
19
+
11
20
  class PagePropertyManager(LoggingMixin):
12
21
  """Verwaltet den Zugriff auf und die Änderung von Seiteneigenschaften."""
13
-
14
- def __init__(self, page_id: str, client: NotionClient,
15
- metadata_editor: MetadataEditor,
16
- db_relation: PageDatabaseRelation):
22
+
23
+ def __init__(
24
+ self,
25
+ page_id: str,
26
+ client: NotionClient,
27
+ metadata_editor: MetadataEditor,
28
+ db_relation: PageDatabaseRelation,
29
+ ):
17
30
  self._page_id = page_id
18
31
  self._client = client
19
32
  self._page_data = None
20
33
  self._metadata_editor = metadata_editor
21
34
  self._db_relation = db_relation
22
35
  self._db_property_service = None
23
-
36
+
24
37
  self._extractor = PropertyValueExtractor(self.logger)
25
38
  self._title_resolver = NotionPageTitleResolver(client)
26
-
39
+
27
40
  async def get_properties(self) -> Dict[str, Any]:
28
41
  """Retrieves all properties of the page."""
29
42
  page_data = await self._get_page_data()
30
43
  if page_data and "properties" in page_data:
31
44
  return page_data["properties"]
32
45
  return {}
33
-
46
+
34
47
  async def get_property_value(self, property_name: str, relation_getter=None) -> Any:
35
48
  """
36
49
  Get the value of a specific property.
37
-
50
+
38
51
  Args:
39
52
  property_name: Name of the property to get
40
53
  relation_getter: Optional callback function to get relation values
@@ -42,105 +55,121 @@ class PagePropertyManager(LoggingMixin):
42
55
  properties = await self.get_properties()
43
56
  if property_name not in properties:
44
57
  return None
45
-
58
+
46
59
  prop_data = properties[property_name]
47
60
  return await self._extractor.extract(property_name, prop_data, relation_getter)
48
-
49
61
 
50
- async def set_property_by_name(self, property_name: str, value: Any) -> PropertyOperationResult:
62
+ async def set_property_by_name(
63
+ self, property_name: str, value: Any
64
+ ) -> PropertyOperationResult:
51
65
  """
52
66
  Set a property value by name, automatically detecting the property type.
53
-
67
+
54
68
  Args:
55
69
  property_name: Name of the property
56
70
  value: Value to set
57
-
71
+
58
72
  Returns:
59
- PropertyOperationResult: Result of the operation with status, error messages,
73
+ PropertyOperationResult: Result of the operation with status, error messages,
60
74
  and available options if applicable
61
75
  """
62
76
  property_type = await self.get_property_type(property_name)
63
-
77
+
64
78
  if property_type == "relation":
65
- result = PropertyOperationResult.from_relation_type_error(property_name, value)
79
+ result = PropertyOperationResult.from_relation_type_error(
80
+ property_name, value
81
+ )
66
82
  self.logger.warning(result.error)
67
83
  return result
68
-
84
+
69
85
  if not await self._db_relation.is_database_page():
70
- api_response = await self._metadata_editor.set_property_by_name(property_name, value)
86
+ api_response = await self._metadata_editor.set_property_by_name(
87
+ property_name, value
88
+ )
71
89
  if api_response:
72
90
  await self.invalidate_cache()
73
- return PropertyOperationResult.from_success(property_name, value, api_response)
91
+ return PropertyOperationResult.from_success(
92
+ property_name, value, api_response
93
+ )
74
94
  return PropertyOperationResult.from_no_api_response(property_name, value)
75
-
95
+
76
96
  db_service = await self._init_db_property_service()
77
-
97
+
78
98
  if not db_service:
79
- api_response = await self._metadata_editor.set_property_by_name(property_name, value)
99
+ api_response = await self._metadata_editor.set_property_by_name(
100
+ property_name, value
101
+ )
80
102
  if api_response:
81
103
  await self.invalidate_cache()
82
- return PropertyOperationResult.from_success(property_name, value, api_response)
104
+ return PropertyOperationResult.from_success(
105
+ property_name, value, api_response
106
+ )
83
107
  return PropertyOperationResult.from_no_api_response(property_name, value)
84
-
85
- is_valid, error_message, available_options = await db_service.validate_property_value(
86
- property_name, value
108
+
109
+ is_valid, error_message, available_options = (
110
+ await db_service.validate_property_value(property_name, value)
87
111
  )
88
-
112
+
89
113
  if not is_valid:
90
114
  if available_options:
91
115
  options_str = "', '".join(available_options)
92
116
  detailed_error = f"{error_message}\nAvailable options for '{property_name}': '{options_str}'"
93
117
  self.logger.warning(detailed_error)
94
118
  else:
95
- self.logger.warning("%s\nNo valid options available for '%s'", error_message, property_name)
96
-
119
+ self.logger.warning(
120
+ "%s\nNo valid options available for '%s'",
121
+ error_message,
122
+ property_name,
123
+ )
124
+
97
125
  return PropertyOperationResult.from_error(
98
- property_name,
99
- error_message,
100
- value,
101
- available_options
126
+ property_name, error_message, value, available_options
102
127
  )
103
-
104
- api_response = await self._metadata_editor.set_property_by_name(property_name, value)
128
+
129
+ api_response = await self._metadata_editor.set_property_by_name(
130
+ property_name, value
131
+ )
105
132
  if api_response:
106
133
  await self.invalidate_cache()
107
- return PropertyOperationResult.from_success(property_name, value, api_response)
108
-
134
+ return PropertyOperationResult.from_success(
135
+ property_name, value, api_response
136
+ )
137
+
109
138
  return PropertyOperationResult.from_no_api_response(property_name, value)
110
-
139
+
111
140
  async def get_property_type(self, property_name: str) -> Optional[str]:
112
141
  """Gets the type of a specific property."""
113
142
  db_service = await self._init_db_property_service()
114
143
  if db_service:
115
144
  return await db_service.get_property_type(property_name)
116
145
  return None
117
-
146
+
118
147
  async def get_available_options_for_property(self, property_name: str) -> List[str]:
119
148
  """Gets the available option names for a property."""
120
149
  db_service = await self._init_db_property_service()
121
150
  if db_service:
122
151
  return await db_service.get_option_names(property_name)
123
152
  return []
124
-
153
+
125
154
  async def _get_page_data(self, force_refresh=False) -> Dict[str, Any]:
126
155
  """Gets the page data and caches it for future use."""
127
156
  if self._page_data is None or force_refresh:
128
157
  self._page_data = await self._client.get_page(self._page_id)
129
158
  return self._page_data
130
-
159
+
131
160
  async def invalidate_cache(self) -> None:
132
161
  """Forces a refresh of the cached page data on next access."""
133
162
  self._page_data = None
134
-
163
+
135
164
  async def _init_db_property_service(self) -> Optional[DatabasePropertyService]:
136
165
  """Lazily initializes the database property service if needed."""
137
166
  if self._db_property_service is not None:
138
167
  return self._db_property_service
139
-
168
+
140
169
  database_id = await self._db_relation.get_parent_database_id()
141
170
  if not database_id:
142
171
  return None
143
-
172
+
144
173
  self._db_property_service = DatabasePropertyService(database_id, self._client)
145
174
  await self._db_property_service.load_schema()
146
- return self._db_property_service
175
+ return self._db_property_service
@@ -93,4 +93,4 @@ class NotionPropertyFormatter(LoggingMixin):
93
93
  self.logger.warning("Unknown property type: %s", property_type)
94
94
  return None
95
95
 
96
- return formatter(value)
96
+ return formatter(value)