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
@@ -1,11 +1,12 @@
1
1
  from typing import Any, Dict, List, Optional
2
2
  from dataclasses import dataclass
3
3
 
4
+
4
5
  @dataclass
5
6
  class PropertyOperationResult:
6
7
  """
7
8
  Result of a property operation in Notion.
8
-
9
+
9
10
  Attributes:
10
11
  success: Whether the operation was successful
11
12
  property_name: Name of the affected property
@@ -14,90 +15,102 @@ class PropertyOperationResult:
14
15
  available_options: Available options for select-like properties
15
16
  api_response: The original API response
16
17
  """
18
+
17
19
  success: bool
18
20
  property_name: str
19
21
  value: Optional[Any] = None
20
22
  error: Optional[str] = None
21
23
  available_options: Optional[List[str]] = None
22
24
  api_response: Optional[Dict[str, Any]] = None
23
-
25
+
24
26
  # Common error messages
25
27
  NO_API_RESPONSE = "Failed to set property (no API response)"
26
28
  RELATION_TYPE_ERROR = "Property '{}' is of type 'relation'. Relations must be set using the RelationManager."
27
-
29
+
28
30
  @classmethod
29
- def from_success(cls, property_name: str, value: Any, api_response: Dict[str, Any]) -> "PropertyOperationResult":
31
+ def from_success(
32
+ cls, property_name: str, value: Any, api_response: Dict[str, Any]
33
+ ) -> "PropertyOperationResult":
30
34
  """Creates a success result."""
31
35
  return cls(
32
- success=True,
33
- property_name=property_name,
34
- value=value,
35
- api_response=api_response
36
+ success=True,
37
+ property_name=property_name,
38
+ value=value,
39
+ api_response=api_response,
36
40
  )
37
-
41
+
38
42
  @classmethod
39
- def from_error(cls, property_name: str,
40
- error: str,
41
- value: Optional[Any] = None,
42
- available_options: Optional[List[str]] = None) -> "PropertyOperationResult":
43
+ def from_error(
44
+ cls,
45
+ property_name: str,
46
+ error: str,
47
+ value: Optional[Any] = None,
48
+ available_options: Optional[List[str]] = None,
49
+ ) -> "PropertyOperationResult":
43
50
  """Creates an error result."""
44
51
  return cls(
45
52
  success=False,
46
53
  property_name=property_name,
47
54
  value=value,
48
55
  error=error,
49
- available_options=available_options or []
56
+ available_options=available_options or [],
50
57
  )
51
-
58
+
52
59
  @classmethod
53
- def from_api_error(cls, property_name: str, api_response: Dict[str, Any]) -> "PropertyOperationResult":
60
+ def from_api_error(
61
+ cls, property_name: str, api_response: Dict[str, Any]
62
+ ) -> "PropertyOperationResult":
54
63
  """Creates a result from an API error response."""
55
64
  return cls(
56
65
  success=False,
57
66
  property_name=property_name,
58
67
  error=api_response.get("message", "Unknown API error"),
59
- api_response=api_response
68
+ api_response=api_response,
60
69
  )
61
-
70
+
62
71
  @classmethod
63
- def from_no_api_response(cls, property_name: str, value: Optional[Any] = None) -> "PropertyOperationResult":
72
+ def from_no_api_response(
73
+ cls, property_name: str, value: Optional[Any] = None
74
+ ) -> "PropertyOperationResult":
64
75
  """Creates a standardized result for missing API responses."""
65
76
  return cls.from_error(property_name, cls.NO_API_RESPONSE, value)
66
77
 
67
78
  @classmethod
68
- def from_relation_type_error(cls, property_name: str, value: Optional[Any] = None) -> "PropertyOperationResult":
79
+ def from_relation_type_error(
80
+ cls, property_name: str, value: Optional[Any] = None
81
+ ) -> "PropertyOperationResult":
69
82
  """Creates a standardized error result for relation type properties."""
70
83
  error_msg = cls.RELATION_TYPE_ERROR.format(property_name)
71
84
  return cls.from_error(property_name, error_msg, value)
72
-
85
+
73
86
  def to_dict(self) -> Dict[str, Any]:
74
87
  """Converts the result to a dictionary."""
75
88
  result = {
76
89
  "success": self.success,
77
90
  "property": self.property_name,
78
91
  }
79
-
92
+
80
93
  if self.value is not None:
81
94
  result["value"] = self.value
82
-
95
+
83
96
  if not self.success:
84
97
  result["error"] = self.error
85
-
98
+
86
99
  if self.available_options:
87
100
  result["available_options"] = self.available_options
88
-
101
+
89
102
  if self.api_response:
90
103
  result["api_response"] = self.api_response
91
-
104
+
92
105
  return result
93
-
106
+
94
107
  def __str__(self) -> str:
95
108
  """String representation of the result."""
96
109
  if self.success:
97
110
  return f"Success: Property '{self.property_name}' set to '{self.value}'"
98
-
111
+
99
112
  if self.available_options:
100
113
  options = "', '".join(self.available_options)
101
114
  return f"Error: {self.error}\nAvailable options for '{self.property_name}': '{options}'"
102
-
103
- return f"Error: {self.error}"
115
+
116
+ return f"Error: {self.error}"
@@ -10,19 +10,33 @@ class PropertyValueExtractor:
10
10
  self,
11
11
  property_name: str,
12
12
  prop_data: dict,
13
- relation_resolver: Callable[[str], Awaitable[Any]]
13
+ relation_resolver: Callable[[str], Awaitable[Any]],
14
14
  ) -> Any:
15
15
  prop_type = prop_data.get("type")
16
16
  if not prop_type:
17
17
  return None
18
18
 
19
19
  handlers: dict[str, Callable[[], Awaitable[Any] | Any]] = {
20
- "title": lambda: "".join(t.get("plain_text", "") for t in prop_data.get("title", [])),
21
- "rich_text": lambda: "".join(t.get("plain_text", "") for t in prop_data.get("rich_text", [])),
20
+ "title": lambda: "".join(
21
+ t.get("plain_text", "") for t in prop_data.get("title", [])
22
+ ),
23
+ "rich_text": lambda: "".join(
24
+ t.get("plain_text", "") for t in prop_data.get("rich_text", [])
25
+ ),
22
26
  "number": lambda: prop_data.get("number"),
23
- "select": lambda: prop_data.get("select", {}).get("name") if prop_data.get("select") else None,
24
- "multi_select": lambda: [o.get("name") for o in prop_data.get("multi_select", [])],
25
- "status": lambda: prop_data.get("status", {}).get("name") if prop_data.get("status") else None,
27
+ "select": lambda: (
28
+ prop_data.get("select", {}).get("name")
29
+ if prop_data.get("select")
30
+ else None
31
+ ),
32
+ "multi_select": lambda: [
33
+ o.get("name") for o in prop_data.get("multi_select", [])
34
+ ],
35
+ "status": lambda: (
36
+ prop_data.get("status", {}).get("name")
37
+ if prop_data.get("status")
38
+ else None
39
+ ),
26
40
  "date": lambda: prop_data.get("date"),
27
41
  "checkbox": lambda: prop_data.get("checkbox"),
28
42
  "url": lambda: prop_data.get("url"),
@@ -31,9 +45,13 @@ class PropertyValueExtractor:
31
45
  "relation": lambda: relation_resolver(property_name),
32
46
  "people": lambda: [p.get("id") for p in prop_data.get("people", [])],
33
47
  "files": lambda: [
34
- f.get("external", {}).get("url") if f.get("type") == "external" else f.get("name")
48
+ (
49
+ f.get("external", {}).get("url")
50
+ if f.get("type") == "external"
51
+ else f.get("name")
52
+ )
35
53
  for f in prop_data.get("files", [])
36
- ]
54
+ ],
37
55
  }
38
56
 
39
57
  handler = handlers.get(prop_type)
@@ -1,17 +1,24 @@
1
1
  from typing import Any, Dict, List, Optional
2
2
  from notionary.core.notion_client import NotionClient
3
- from notionary.core.page.relations.notion_page_title_resolver import NotionPageTitleResolver
4
- from notionary.core.page.relations.relation_operation_result import RelationOperationResult
3
+ from notionary.core.page.relations.notion_page_title_resolver import (
4
+ NotionPageTitleResolver,
5
+ )
6
+ from notionary.core.page.relations.relation_operation_result import (
7
+ RelationOperationResult,
8
+ )
5
9
  from notionary.util.logging_mixin import LoggingMixin
6
10
  from notionary.util.page_id_utils import is_valid_uuid
7
11
 
12
+
8
13
  class NotionRelationManager(LoggingMixin):
9
14
  """
10
15
  Manager for relation properties of a Notion page.
11
16
  Manages links between pages and loads available relation options.
12
17
  """
13
18
 
14
- def __init__(self, page_id: str, client: NotionClient, database_id: Optional[str] = None):
19
+ def __init__(
20
+ self, page_id: str, client: NotionClient, database_id: Optional[str] = None
21
+ ):
15
22
  """
16
23
  Initializes the relation manager.
17
24
 
@@ -75,7 +82,8 @@ class NotionRelationManager(LoggingMixin):
75
82
  properties = await self._get_page_properties()
76
83
 
77
84
  return [
78
- prop_name for prop_name, prop_data in properties.items()
85
+ prop_name
86
+ for prop_name, prop_data in properties.items()
79
87
  if prop_data.get("type") == "relation"
80
88
  ]
81
89
 
@@ -101,7 +109,9 @@ class NotionRelationManager(LoggingMixin):
101
109
 
102
110
  return [rel.get("id") for rel in prop_data["relation"]]
103
111
 
104
- async def get_relation_details(self, property_name: str) -> Optional[Dict[str, Any]]:
112
+ async def get_relation_details(
113
+ self, property_name: str
114
+ ) -> Optional[Dict[str, Any]]:
105
115
  """
106
116
  Returns details about the relation property, including the linked database.
107
117
 
@@ -153,7 +163,9 @@ class NotionRelationManager(LoggingMixin):
153
163
 
154
164
  return relation_details.get("database_id")
155
165
 
156
- async def get_relation_options(self, property_name: str, limit: int = 100) -> List[Dict[str, Any]]:
166
+ async def get_relation_options(
167
+ self, property_name: str, limit: int = 100
168
+ ) -> List[Dict[str, Any]]:
157
169
  """
158
170
  Returns available options for a relation property.
159
171
 
@@ -174,7 +186,7 @@ class NotionRelationManager(LoggingMixin):
174
186
  f"databases/{related_db_id}/query",
175
187
  {
176
188
  "page_size": limit,
177
- }
189
+ },
178
190
  )
179
191
 
180
192
  if not query_result or "results" not in query_result:
@@ -186,10 +198,7 @@ class NotionRelationManager(LoggingMixin):
186
198
  title = self._extract_title_from_page(page)
187
199
 
188
200
  if page_id and title:
189
- options.append({
190
- "id": page_id,
191
- "name": title
192
- })
201
+ options.append({"id": page_id, "name": title})
193
202
 
194
203
  return options
195
204
  except Exception as e:
@@ -214,11 +223,15 @@ class NotionRelationManager(LoggingMixin):
214
223
  for prop_data in properties.values():
215
224
  if prop_data.get("type") == "title" and "title" in prop_data:
216
225
  title_parts = prop_data["title"]
217
- return "".join([text_obj.get("plain_text", "") for text_obj in title_parts])
226
+ return "".join(
227
+ [text_obj.get("plain_text", "") for text_obj in title_parts]
228
+ )
218
229
 
219
230
  return None
220
231
 
221
- async def add_relation(self, property_name: str, page_ids: List[str]) -> Optional[Dict[str, Any]]:
232
+ async def add_relation(
233
+ self, property_name: str, page_ids: List[str]
234
+ ) -> Optional[Dict[str, Any]]:
222
235
  """
223
236
  Adds one or more relations.
224
237
 
@@ -233,18 +246,12 @@ class NotionRelationManager(LoggingMixin):
233
246
 
234
247
  all_relations = list(set(existing_relations + page_ids))
235
248
 
236
- relation_payload = {
237
- "relation": [{"id": page_id} for page_id in all_relations]
238
- }
249
+ relation_payload = {"relation": [{"id": page_id} for page_id in all_relations]}
239
250
 
240
251
  try:
241
252
  result = await self._client.patch(
242
253
  f"pages/{self._page_id}",
243
- {
244
- "properties": {
245
- property_name: relation_payload
246
- }
247
- },
254
+ {"properties": {property_name: relation_payload}},
248
255
  )
249
256
 
250
257
  self._page_properties = None
@@ -254,7 +261,9 @@ class NotionRelationManager(LoggingMixin):
254
261
  self.logger.error("Error adding relation: %s", str(e))
255
262
  return None
256
263
 
257
- async def add_relation_by_name(self, property_name: str, page_titles: List[str]) -> RelationOperationResult:
264
+ async def add_relation_by_name(
265
+ self, property_name: str, page_titles: List[str]
266
+ ) -> RelationOperationResult:
258
267
  """
259
268
  Adds one or more relations based on page titles.
260
269
 
@@ -268,9 +277,13 @@ class NotionRelationManager(LoggingMixin):
268
277
  found_pages = []
269
278
  not_found_pages = []
270
279
  page_ids = []
271
-
272
- self.logger.info("Attempting to add %d relation(s) to property '%s'", len(page_titles), property_name)
273
-
280
+
281
+ self.logger.info(
282
+ "Attempting to add %d relation(s) to property '%s'",
283
+ len(page_titles),
284
+ property_name,
285
+ )
286
+
274
287
  for page in page_titles:
275
288
  if is_valid_uuid(page):
276
289
  page_ids.append(page)
@@ -287,38 +300,51 @@ class NotionRelationManager(LoggingMixin):
287
300
  self.logger.warning("No page found with title '%s'", page)
288
301
 
289
302
  if not page_ids:
290
- self.logger.warning("No valid page IDs found for any of the titles, no changes applied")
291
- return RelationOperationResult.from_no_pages_found(property_name, not_found_pages)
303
+ self.logger.warning(
304
+ "No valid page IDs found for any of the titles, no changes applied"
305
+ )
306
+ return RelationOperationResult.from_no_pages_found(
307
+ property_name, not_found_pages
308
+ )
292
309
 
293
310
  api_response = await self.add_relation(property_name, page_ids)
294
-
311
+
295
312
  if api_response:
296
313
  result = RelationOperationResult.from_success(
297
314
  property_name=property_name,
298
315
  found_pages=found_pages,
299
316
  not_found_pages=not_found_pages,
300
317
  page_ids_added=page_ids,
301
- api_response=api_response
318
+ api_response=api_response,
302
319
  )
303
-
320
+
304
321
  if not_found_pages:
305
322
  not_found_str = "', '".join(not_found_pages)
306
- self.logger.info("Added %d relation(s) to '%s', but couldn't find pages: '%s'",
307
- len(page_ids), property_name, not_found_str)
323
+ self.logger.info(
324
+ "Added %d relation(s) to '%s', but couldn't find pages: '%s'",
325
+ len(page_ids),
326
+ property_name,
327
+ not_found_str,
328
+ )
308
329
  else:
309
- self.logger.info("Successfully added all %d relation(s) to '%s'",
310
- len(page_ids), property_name)
311
-
330
+ self.logger.info(
331
+ "Successfully added all %d relation(s) to '%s'",
332
+ len(page_ids),
333
+ property_name,
334
+ )
335
+
312
336
  return result
313
-
337
+
314
338
  self.logger.error("Failed to add relations to '%s' (API error)", property_name)
315
339
  return RelationOperationResult.from_no_api_response(
316
340
  property_name=property_name,
317
341
  found_pages=found_pages,
318
- page_ids_added=page_ids
342
+ page_ids_added=page_ids,
319
343
  )
320
344
 
321
- async def set_relations(self, property_name: str, page_ids: List[str]) -> Optional[Dict[str, Any]]:
345
+ async def set_relations(
346
+ self, property_name: str, page_ids: List[str]
347
+ ) -> Optional[Dict[str, Any]]:
322
348
  """
323
349
  Sets the relations to the specified IDs (replaces existing ones).
324
350
 
@@ -329,18 +355,12 @@ class NotionRelationManager(LoggingMixin):
329
355
  Returns:
330
356
  Optional[Dict[str, Any]]: API response or None on error
331
357
  """
332
- relation_payload = {
333
- "relation": [{"id": page_id} for page_id in page_ids]
334
- }
358
+ relation_payload = {"relation": [{"id": page_id} for page_id in page_ids]}
335
359
 
336
360
  try:
337
361
  result = await self._client.patch(
338
362
  f"pages/{self._page_id}",
339
- {
340
- "properties": {
341
- property_name: relation_payload
342
- }
343
- },
363
+ {"properties": {property_name: relation_payload}},
344
364
  )
345
365
 
346
366
  self._page_properties = None
@@ -349,16 +369,15 @@ class NotionRelationManager(LoggingMixin):
349
369
  except Exception as e:
350
370
  self.logger.error("Error setting relations: %s", str(e))
351
371
  return None
352
-
372
+
353
373
  async def get_all_relations(self) -> Dict[str, List[str]]:
354
- """ Returns all relation properties and their values.
355
- """
374
+ """Returns all relation properties and their values."""
356
375
  relation_properties = await self.get_relation_property_ids()
357
376
  if not relation_properties:
358
377
  return {}
359
-
378
+
360
379
  result = {}
361
380
  for prop_name in relation_properties:
362
381
  result[prop_name] = await self.get_relation_values(prop_name)
363
-
364
- return result
382
+
383
+ return result
@@ -6,7 +6,7 @@ from notionary.util.logging_mixin import LoggingMixin
6
6
  class NotionPageTitleResolver(LoggingMixin):
7
7
  def __init__(self, client: NotionClient):
8
8
  self._client = client
9
-
9
+
10
10
  async def get_page_id_by_title(self, title: str) -> Optional[str]:
11
11
  """
12
12
  Searches for a Notion page by its title and returns the corresponding page ID if found.
@@ -14,13 +14,7 @@ class NotionPageTitleResolver(LoggingMixin):
14
14
  try:
15
15
  search_results = await self._client.post(
16
16
  "search",
17
- {
18
- "query": title,
19
- "filter": {
20
- "value": "page",
21
- "property": "object"
22
- }
23
- }
17
+ {"query": title, "filter": {"value": "page", "property": "object"}},
24
18
  )
25
19
 
26
20
  for result in search_results.get("results", []):
@@ -30,14 +24,20 @@ class NotionPageTitleResolver(LoggingMixin):
30
24
  if prop_value.get("type") == "title":
31
25
  title_texts = prop_value.get("title", [])
32
26
 
33
- page_title = " ".join([t.get("plain_text", "") for t in title_texts])
27
+ page_title = " ".join(
28
+ [t.get("plain_text", "") for t in title_texts]
29
+ )
34
30
 
35
31
  if page_title == title or title in page_title:
36
- self.logger.debug("Found page: '%s' with ID: %s", page_title, result.get("id"))
32
+ self.logger.debug(
33
+ "Found page: '%s' with ID: %s",
34
+ page_title,
35
+ result.get("id"),
36
+ )
37
37
  return result.get("id")
38
38
 
39
39
  self.logger.debug("No page found with title '%s'", title)
40
40
  return None
41
41
  except Exception as e:
42
42
  self.logger.error("Error while searching for page '%s': %s", title, e)
43
- return None
43
+ return None
@@ -8,11 +8,11 @@ class PageDatabaseRelation(LoggingMixin):
8
8
  Manages the relationship between a Notion page and its parent database.
9
9
  Provides methods to access database schema and property options.
10
10
  """
11
-
11
+
12
12
  def __init__(self, page_id: str, client: NotionClient):
13
13
  """
14
14
  Initialize the page-database relationship handler.
15
-
15
+
16
16
  Args:
17
17
  page_id: ID of the Notion page
18
18
  client: Instance of NotionClient
@@ -22,49 +22,49 @@ class PageDatabaseRelation(LoggingMixin):
22
22
  self._parent_database_id = None
23
23
  self._database_schema = None
24
24
  self._page_data = None
25
-
25
+
26
26
  async def _get_page_data(self, force_refresh=False) -> Dict[str, Any]:
27
27
  """
28
28
  Gets the page data and caches it for future use.
29
-
29
+
30
30
  Args:
31
31
  force_refresh: Whether to force a refresh of the page data
32
-
32
+
33
33
  Returns:
34
34
  Dict[str, Any]: The page data
35
35
  """
36
36
  if self._page_data is None or force_refresh:
37
37
  self._page_data = await self._client.get_page(self._page_id)
38
38
  return self._page_data
39
-
39
+
40
40
  async def get_parent_database_id(self) -> Optional[str]:
41
41
  """
42
42
  Gets the ID of the database this page belongs to, if any.
43
-
43
+
44
44
  Returns:
45
45
  Optional[str]: The database ID or None if the page doesn't belong to a database
46
46
  """
47
47
  if self._parent_database_id is not None:
48
48
  return self._parent_database_id
49
-
49
+
50
50
  page_data = await self._get_page_data()
51
-
51
+
52
52
  if not page_data or "parent" not in page_data:
53
53
  return None
54
-
54
+
55
55
  parent = page_data.get("parent", {})
56
56
  if parent.get("type") == "database_id":
57
57
  self._parent_database_id = parent.get("database_id")
58
58
  return self._parent_database_id
59
-
59
+
60
60
  return None
61
-
61
+
62
62
  async def is_database_page(self) -> bool:
63
63
  """
64
64
  Checks if this page belongs to a database.
65
-
65
+
66
66
  Returns:
67
67
  bool: True if the page belongs to a database, False otherwise
68
68
  """
69
69
  database_id = await self.get_parent_database_id()
70
- return database_id is not None
70
+ return database_id is not None