notionary 0.1.12__py3-none-any.whl → 0.1.13__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 (54) hide show
  1. notionary/__init__.py +14 -10
  2. notionary/{core/converters → converters}/elements/audio_element.py +1 -1
  3. notionary/{core/converters → converters}/elements/bookmark_element.py +1 -1
  4. notionary/{core/converters → converters}/elements/callout_element.py +2 -2
  5. notionary/{core/converters → converters}/elements/code_block_element.py +1 -1
  6. notionary/{core/converters → converters}/elements/column_element.py +1 -1
  7. notionary/{core/converters → converters}/elements/divider_element.py +1 -1
  8. notionary/{core/converters → converters}/elements/embed_element.py +1 -1
  9. notionary/{core/converters → converters}/elements/heading_element.py +2 -2
  10. notionary/{core/converters → converters}/elements/image_element.py +1 -1
  11. notionary/{core/converters → converters}/elements/list_element.py +2 -2
  12. notionary/{core/converters → converters}/elements/paragraph_element.py +2 -2
  13. notionary/{core/converters → converters}/elements/qoute_element.py +1 -1
  14. notionary/{core/converters → converters}/elements/table_element.py +2 -2
  15. notionary/{core/converters → converters}/elements/todo_lists.py +2 -2
  16. notionary/{core/converters → converters}/elements/toggle_element.py +1 -6
  17. notionary/{core/converters → converters}/elements/video_element.py +1 -1
  18. notionary/{core/converters → converters}/markdown_to_notion_converter.py +2 -2
  19. notionary/{core/converters → converters}/notion_to_markdown_converter.py +2 -2
  20. notionary/{core/converters → converters}/registry/block_element_registry.py +2 -2
  21. notionary/{core/converters → converters}/registry/block_element_registry_builder.py +18 -18
  22. notionary/{core/database → database}/database_discovery.py +19 -17
  23. notionary/{core/database → database}/database_info_service.py +1 -1
  24. notionary/{core/database/notion_database_manager.py → database/notion_database.py} +13 -14
  25. notionary/{core/database/notion_database_manager_factory.py → database/notion_database_factory.py} +8 -12
  26. notionary/{core/page → page}/content/page_content_manager.py +6 -8
  27. notionary/{core/page → page}/metadata/metadata_editor.py +2 -2
  28. notionary/{core/page → page}/metadata/notion_icon_manager.py +1 -1
  29. notionary/{core/page → page}/metadata/notion_page_cover_manager.py +1 -1
  30. notionary/page/notion_page.py +504 -0
  31. notionary/page/notion_page_factory.py +256 -0
  32. notionary/{core/page → page}/properites/database_property_service.py +1 -1
  33. notionary/{core/page → page}/properites/page_property_manager.py +7 -7
  34. notionary/{core/page → page}/relations/notion_page_relation_manager.py +3 -3
  35. notionary/{core/page → page}/relations/notion_page_title_resolver.py +1 -1
  36. notionary/{core/page → page}/relations/page_database_relation.py +1 -1
  37. notionary/util/page_id_utils.py +3 -1
  38. {notionary-0.1.12.dist-info → notionary-0.1.13.dist-info}/METADATA +1 -1
  39. notionary-0.1.13.dist-info/RECORD +56 -0
  40. notionary/core/page/notion_page_manager.py +0 -312
  41. notionary-0.1.12.dist-info/RECORD +0 -55
  42. /notionary/{core/converters → converters}/__init__.py +0 -0
  43. /notionary/{core/converters → converters}/elements/notion_block_element.py +0 -0
  44. /notionary/{core/converters → converters}/elements/text_inline_formatter.py +0 -0
  45. /notionary/{core/database → database}/models/page_result.py +0 -0
  46. /notionary/{core/notion_client.py → notion_client.py} +0 -0
  47. /notionary/{core/page → page}/content/notion_page_content_chunker.py +0 -0
  48. /notionary/{core/page → page}/properites/property_formatter.py +0 -0
  49. /notionary/{core/page → page}/properites/property_operation_result.py +0 -0
  50. /notionary/{core/page → page}/properites/property_value_extractor.py +0 -0
  51. /notionary/{core/page → page}/relations/relation_operation_result.py +0 -0
  52. {notionary-0.1.12.dist-info → notionary-0.1.13.dist-info}/WHEEL +0 -0
  53. {notionary-0.1.12.dist-info → notionary-0.1.13.dist-info}/licenses/LICENSE +0 -0
  54. {notionary-0.1.12.dist-info → notionary-0.1.13.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,504 @@
1
+ import re
2
+ from typing import Any, Dict, List, Optional, Union
3
+ from notionary.converters.registry.block_element_registry import (
4
+ BlockElementRegistry,
5
+ )
6
+ from notionary.converters.registry.block_element_registry_builder import (
7
+ BlockElementRegistryBuilder,
8
+ )
9
+ from notionary.notion_client import NotionClient
10
+ from notionary.page.metadata.metadata_editor import MetadataEditor
11
+ from notionary.page.metadata.notion_icon_manager import NotionPageIconManager
12
+ from notionary.page.metadata.notion_page_cover_manager import (
13
+ NotionPageCoverManager,
14
+ )
15
+ from notionary.page.properites.database_property_service import (
16
+ DatabasePropertyService,
17
+ )
18
+ from notionary.page.relations.notion_page_relation_manager import (
19
+ NotionRelationManager,
20
+ )
21
+ from notionary.page.content.page_content_manager import PageContentManager
22
+ from notionary.page.properites.page_property_manager import PagePropertyManager
23
+ from notionary.util.logging_mixin import LoggingMixin
24
+ from notionary.util.page_id_utils import extract_and_validate_page_id
25
+ from notionary.page.relations.page_database_relation import PageDatabaseRelation
26
+
27
+
28
+ class NotionPage(LoggingMixin):
29
+ """
30
+ High-Level Facade for managing content and metadata of a Notion page.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ page_id: Optional[str] = None,
36
+ title: Optional[str] = None,
37
+ url: Optional[str] = None,
38
+ token: Optional[str] = None,
39
+ ):
40
+ self._page_id = extract_and_validate_page_id(page_id=page_id, url=url)
41
+ self._url = url
42
+ self._title = title
43
+ self._client = NotionClient(token=token)
44
+ self._page_data = None
45
+ self._title_loaded = title is not None
46
+ self._url_loaded = url is not None
47
+
48
+ self._block_element_registry = (
49
+ BlockElementRegistryBuilder.create_standard_registry()
50
+ )
51
+
52
+ self._page_content_manager = PageContentManager(
53
+ page_id=self._page_id,
54
+ client=self._client,
55
+ block_registry=self._block_element_registry,
56
+ )
57
+ self._metadata = MetadataEditor(self._page_id, self._client)
58
+ self._page_cover_manager = NotionPageCoverManager(
59
+ page_id=self._page_id, client=self._client
60
+ )
61
+ self._page_icon_manager = NotionPageIconManager(
62
+ page_id=self._page_id, client=self._client
63
+ )
64
+
65
+ self._db_relation = PageDatabaseRelation(
66
+ page_id=self._page_id, client=self._client
67
+ )
68
+ self._db_property_service = None
69
+
70
+ self._relation_manager = NotionRelationManager(
71
+ page_id=self._page_id, client=self._client
72
+ )
73
+
74
+ self._property_manager = PagePropertyManager(
75
+ self._page_id, self._client, self._metadata, self._db_relation
76
+ )
77
+
78
+ @property
79
+ def id(self) -> str:
80
+ """
81
+ Get the ID of the page.
82
+
83
+ Returns:
84
+ str: The page ID.
85
+ """
86
+ return self._page_id
87
+
88
+ @property
89
+ def block_registry(self) -> BlockElementRegistry:
90
+ """
91
+ Get the block element registry associated with this page.
92
+
93
+ Returns:
94
+ BlockElementRegistry: The registry of block elements.
95
+ """
96
+ return self._block_element_registry
97
+
98
+ @block_registry.setter
99
+ def block_registry(self, block_registry: BlockElementRegistry) -> None:
100
+ """
101
+ Set the block element registry for the page content manager.
102
+
103
+ Args:
104
+ block_registry: The registry of block elements to use.
105
+ """
106
+ self._block_element_registry = block_registry
107
+ self._page_content_manager = PageContentManager(
108
+ page_id=self._page_id, client=self._client, block_registry=block_registry
109
+ )
110
+
111
+ async def get_title(self) -> str:
112
+ """
113
+ Get the title of the page, loading it if necessary.
114
+
115
+ Returns:
116
+ str: The page title.
117
+ """
118
+ if not self._title_loaded:
119
+ await self._load_page_title()
120
+ return self._title
121
+
122
+ async def get_url(self) -> str:
123
+ """
124
+ Get the URL of the page, constructing it if necessary.
125
+
126
+ Returns:
127
+ str: The page URL.
128
+ """
129
+ if not self._url_loaded:
130
+ self._url = await self._build_notion_url()
131
+ self._url_loaded = True
132
+ return self._url
133
+
134
+ async def append_markdown(self, markdown: str) -> str:
135
+ """
136
+ Append markdown content to the page.
137
+
138
+ Args:
139
+ markdown: The markdown content to append.
140
+
141
+ Returns:
142
+ str: Status or confirmation message.
143
+ """
144
+ return await self._page_content_manager.append_markdown(markdown)
145
+
146
+ async def clear(self) -> str:
147
+ """
148
+ Clear all content from the page.
149
+
150
+ Returns:
151
+ str: Status or confirmation message.
152
+ """
153
+ return await self._page_content_manager.clear()
154
+
155
+ async def replace_content(self, markdown: str) -> str:
156
+ """
157
+ Replace the entire page content with new markdown content.
158
+
159
+ Args:
160
+ markdown: The new markdown content.
161
+
162
+ Returns:
163
+ str: Status or confirmation message.
164
+ """
165
+ await self._page_content_manager.clear()
166
+ return await self._page_content_manager.append_markdown(markdown)
167
+
168
+ async def get_text(self) -> str:
169
+ """
170
+ Get the text content of the page.
171
+
172
+ Returns:
173
+ str: The text content of the page.
174
+ """
175
+ return await self._page_content_manager.get_text()
176
+
177
+ async def set_title(self, title: str) -> Optional[Dict[str, Any]]:
178
+ """
179
+ Set the title of the page.
180
+
181
+ Args:
182
+ title: The new title.
183
+
184
+ Returns:
185
+ Optional[Dict[str, Any]]: Response data from the API if successful, None otherwise.
186
+ """
187
+ result = await self._metadata.set_title(title)
188
+ if result:
189
+ self._title = title
190
+ self._title_loaded = True
191
+ return result
192
+
193
+ async def set_page_icon(
194
+ self, emoji: Optional[str] = None, external_url: Optional[str] = None
195
+ ) -> Optional[Dict[str, Any]]:
196
+ """
197
+ Set the icon for the page. Provide either emoji or external_url.
198
+
199
+ Args:
200
+ emoji: Optional emoji to use as icon.
201
+ external_url: Optional URL to an external image to use as icon.
202
+
203
+ Returns:
204
+ Optional[Dict[str, Any]]: Response data from the API if successful, None otherwise.
205
+ """
206
+ return await self._page_icon_manager.set_icon(emoji, external_url)
207
+
208
+ async def get_icon(self) -> Optional[str]:
209
+ """
210
+ Retrieve the page icon - either emoji or external URL.
211
+
212
+ Returns:
213
+ Optional[str]: The icon emoji or URL, or None if no icon is set.
214
+ """
215
+ return await self._page_icon_manager.get_icon()
216
+
217
+ async def get_cover_url(self) -> str:
218
+ """
219
+ Get the URL of the page cover image.
220
+
221
+ Returns:
222
+ str: The URL of the cover image or empty string if not available.
223
+ """
224
+ return await self._page_cover_manager.get_cover_url()
225
+
226
+ async def set_page_cover(self, external_url: str) -> Optional[Dict[str, Any]]:
227
+ """
228
+ Set the cover image for the page using an external URL.
229
+
230
+ Args:
231
+ external_url: URL to the external image.
232
+
233
+ Returns:
234
+ Optional[Dict[str, Any]]: Response data from the API if successful, None otherwise.
235
+ """
236
+ return await self._page_cover_manager.set_cover(external_url)
237
+
238
+ async def set_random_gradient_cover(self) -> Optional[Dict[str, Any]]:
239
+ """
240
+ Set a random gradient as the page cover.
241
+
242
+ Returns:
243
+ Optional[Dict[str, Any]]: Response data from the API if successful, None otherwise.
244
+ """
245
+ return await self._page_cover_manager.set_random_gradient_cover()
246
+
247
+ async def get_properties(self) -> Dict[str, Any]:
248
+ """
249
+ Retrieve all properties of the page.
250
+
251
+ Returns:
252
+ Dict[str, Any]: Dictionary of property names and their values.
253
+ """
254
+ return await self._property_manager.get_properties()
255
+
256
+ async def get_property_value(self, property_name: str) -> Any:
257
+ """
258
+ Get the value of a specific property.
259
+
260
+ Args:
261
+ property_name: The name of the property.
262
+
263
+ Returns:
264
+ Any: The value of the property.
265
+ """
266
+ return await self._property_manager.get_property_value(
267
+ property_name, self._relation_manager.get_relation_values
268
+ )
269
+
270
+ async def set_property_by_name(
271
+ self, property_name: str, value: Any
272
+ ) -> Optional[Dict[str, Any]]:
273
+ """
274
+ Set the value of a specific property by its name.
275
+
276
+ Args:
277
+ property_name: The name of the property.
278
+ value: The new value to set.
279
+
280
+ Returns:
281
+ Optional[Dict[str, Any]]: Response data from the API if successful, None otherwise.
282
+ """
283
+ return await self._property_manager.set_property_by_name(
284
+ property_name=property_name,
285
+ value=value,
286
+ )
287
+
288
+ async def is_database_page(self) -> bool:
289
+ """
290
+ Check if this page belongs to a database.
291
+
292
+ Returns:
293
+ bool: True if the page belongs to a database, False otherwise.
294
+ """
295
+ return await self._db_relation.is_database_page()
296
+
297
+ async def get_parent_database_id(self) -> Optional[str]:
298
+ """
299
+ Get the ID of the database this page belongs to, if any.
300
+
301
+ Returns:
302
+ Optional[str]: The database ID or None if the page doesn't belong to a database.
303
+ """
304
+ return await self._db_relation.get_parent_database_id()
305
+
306
+ async def get_available_options_for_property(self, property_name: str) -> List[str]:
307
+ """
308
+ Get the available option names for a property (select, multi_select, status).
309
+
310
+ Args:
311
+ property_name: The name of the property.
312
+
313
+ Returns:
314
+ List[str]: List of available option names.
315
+ """
316
+ db_service = await self._get_db_property_service()
317
+ if db_service:
318
+ return await db_service.get_option_names(property_name)
319
+ return []
320
+
321
+ async def get_property_type(self, property_name: str) -> Optional[str]:
322
+ """
323
+ Get the type of a specific property.
324
+
325
+ Args:
326
+ property_name: The name of the property.
327
+
328
+ Returns:
329
+ Optional[str]: The type of the property or None if not found.
330
+ """
331
+ db_service = await self._get_db_property_service()
332
+ if db_service:
333
+ return await db_service.get_property_type(property_name)
334
+ return None
335
+
336
+ async def get_database_metadata(
337
+ self, include_types: Optional[List[str]] = None
338
+ ) -> Dict[str, Any]:
339
+ """
340
+ Get complete metadata about the database this page belongs to.
341
+
342
+ Args:
343
+ include_types: Optional list of property types to include. If None, all properties are included.
344
+
345
+ Returns:
346
+ Dict[str, Any]: Database metadata or empty dict if not a database page.
347
+ """
348
+ db_service = await self._get_db_property_service()
349
+ if db_service:
350
+ return await db_service.get_database_metadata(include_types)
351
+ return {"properties": {}}
352
+
353
+ async def get_relation_options(
354
+ self, property_name: str, limit: int = 100
355
+ ) -> List[Dict[str, Any]]:
356
+ """
357
+ Return available options for a relation property.
358
+
359
+ Args:
360
+ property_name: The name of the relation property.
361
+ limit: Maximum number of options to return.
362
+
363
+ Returns:
364
+ List[Dict[str, Any]]: List of available relation options.
365
+ """
366
+ return await self._relation_manager.get_relation_options(property_name, limit)
367
+
368
+ async def add_relations_by_name(
369
+ self, relation_property_name: str, page_titles: Union[str, List[str]]
370
+ ) -> Optional[Dict[str, Any]]:
371
+ """
372
+ Add one or more relations to a relation property.
373
+
374
+ Args:
375
+ relation_property_name: The name of the relation property.
376
+ page_titles: One or more page titles to relate to.
377
+
378
+ Returns:
379
+ Optional[Dict[str, Any]]: Response data from the API if successful, None otherwise.
380
+ """
381
+ return await self._relation_manager.add_relation_by_name(
382
+ property_name=relation_property_name, page_titles=page_titles
383
+ )
384
+
385
+ async def get_relation_values(self, property_name: str) -> List[str]:
386
+ """
387
+ Return the current relation values for a property.
388
+
389
+ Args:
390
+ property_name: The name of the relation property.
391
+
392
+ Returns:
393
+ List[str]: List of relation values.
394
+ """
395
+ return await self._relation_manager.get_relation_values(property_name)
396
+
397
+ async def get_relation_property_ids(self) -> List[str]:
398
+ """
399
+ Return a list of all relation property names.
400
+
401
+ Returns:
402
+ List[str]: List of relation property names.
403
+ """
404
+ return await self._relation_manager.get_relation_property_ids()
405
+
406
+ async def get_all_relations(self) -> Dict[str, List[str]]:
407
+ """
408
+ Return all relation properties and their values.
409
+
410
+ Returns:
411
+ Dict[str, List[str]]: Dictionary mapping relation property names to their values.
412
+ """
413
+ return await self._relation_manager.get_all_relations()
414
+
415
+ async def _load_page_title(self) -> str:
416
+ """
417
+ Load the page title from Notion API if not already loaded.
418
+
419
+ Returns:
420
+ str: The page title.
421
+ """
422
+ if self._title is not None:
423
+ return self._title
424
+
425
+ self.logger.debug("Lazy loading page title for page: %s", self._page_id)
426
+ try:
427
+ # Retrieve page data
428
+ page_data = await self._client.get(f"pages/{self._page_id}")
429
+ self._title = self._extract_title_from_page_data(page_data)
430
+ except Exception as e:
431
+ self.logger.error("Error loading page title: %s", str(e))
432
+ self._title = "Untitled"
433
+
434
+ self._title_loaded = True
435
+ self.logger.debug("Loaded page title: %s", self._title)
436
+ return self._title
437
+
438
+ def _extract_title_from_page_data(self, page_data: Dict[str, Any]) -> str:
439
+ """
440
+ Extract title from page data.
441
+
442
+ Args:
443
+ page_data: The page data from Notion API
444
+
445
+ Returns:
446
+ str: The extracted title or "Untitled" if not found
447
+ """
448
+ if "properties" not in page_data:
449
+ return "Untitled"
450
+
451
+ for prop_value in page_data["properties"].values():
452
+ if prop_value.get("type") != "title":
453
+ continue
454
+
455
+ title_array = prop_value.get("title", [])
456
+ if not title_array:
457
+ continue
458
+
459
+ text_parts = []
460
+ for text_obj in title_array:
461
+ if "plain_text" in text_obj:
462
+ text_parts.append(text_obj["plain_text"])
463
+
464
+ return "".join(text_parts) or "Untitled"
465
+
466
+ return "Untitled"
467
+
468
+ async def _build_notion_url(self) -> str:
469
+ """
470
+ Build a Notion URL from the page ID, including the title if available.
471
+
472
+ Returns:
473
+ str: The Notion URL for the page.
474
+ """
475
+ title = await self._load_page_title()
476
+
477
+ url_title = ""
478
+ if title and title != "Untitled":
479
+ url_title = re.sub(r"[^\w\s-]", "", title)
480
+ url_title = re.sub(r"[\s]+", "-", url_title)
481
+ url_title = f"{url_title}-"
482
+
483
+ clean_id = self._page_id.replace("-", "")
484
+
485
+ return f"https://www.notion.so/{url_title}{clean_id}"
486
+
487
+ async def _get_db_property_service(self) -> Optional[DatabasePropertyService]:
488
+ """
489
+ Gets the database property service, initializing it if necessary.
490
+ This is a more intuitive way to work with the instance variable.
491
+
492
+ Returns:
493
+ Optional[DatabasePropertyService]: The database property service or None if not applicable
494
+ """
495
+ if self._db_property_service is not None:
496
+ return self._db_property_service
497
+
498
+ database_id = await self._db_relation.get_parent_database_id()
499
+ if not database_id:
500
+ return None
501
+
502
+ self._db_property_service = DatabasePropertyService(database_id, self._client)
503
+ await self._db_property_service.load_schema()
504
+ return self._db_property_service