notionary 0.2.12__py3-none-any.whl → 0.2.14__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 (86) hide show
  1. notionary/__init__.py +3 -20
  2. notionary/{notion_client.py → base_notion_client.py} +92 -98
  3. notionary/blocks/__init__.py +61 -0
  4. notionary/{elements → blocks}/audio_element.py +6 -4
  5. notionary/{elements → blocks}/bookmark_element.py +3 -6
  6. notionary/{elements → blocks}/bulleted_list_element.py +5 -7
  7. notionary/{elements → blocks}/callout_element.py +5 -8
  8. notionary/{elements → blocks}/code_block_element.py +4 -6
  9. notionary/{elements → blocks}/column_element.py +3 -6
  10. notionary/{elements → blocks}/divider_element.py +3 -6
  11. notionary/{elements → blocks}/embed_element.py +4 -6
  12. notionary/{elements → blocks}/heading_element.py +5 -9
  13. notionary/{elements → blocks}/image_element.py +4 -6
  14. notionary/{elements → blocks}/mention_element.py +3 -7
  15. notionary/blocks/notion_block_client.py +26 -0
  16. notionary/blocks/notion_block_element.py +34 -0
  17. notionary/{elements → blocks}/numbered_list_element.py +4 -7
  18. notionary/{elements → blocks}/paragraph_element.py +4 -7
  19. notionary/{prompting/element_prompt_content.py → blocks/prompts/element_prompt_builder.py} +1 -40
  20. notionary/blocks/prompts/element_prompt_content.py +41 -0
  21. notionary/{elements → blocks}/qoute_element.py +4 -6
  22. notionary/{elements → blocks}/registry/block_registry.py +4 -26
  23. notionary/{elements → blocks}/registry/block_registry_builder.py +26 -25
  24. notionary/{elements → blocks}/table_element.py +6 -8
  25. notionary/{elements → blocks}/text_inline_formatter.py +1 -4
  26. notionary/{elements → blocks}/todo_element.py +6 -8
  27. notionary/{elements → blocks}/toggle_element.py +3 -6
  28. notionary/{elements → blocks}/toggleable_heading_element.py +5 -8
  29. notionary/{elements → blocks}/video_element.py +4 -6
  30. notionary/cli/main.py +245 -53
  31. notionary/cli/onboarding.py +117 -0
  32. notionary/database/__init__.py +0 -0
  33. notionary/database/client.py +132 -0
  34. notionary/database/database_exceptions.py +13 -0
  35. notionary/database/factory.py +0 -0
  36. notionary/database/filter_builder.py +175 -0
  37. notionary/database/notion_database.py +339 -128
  38. notionary/database/notion_database_provider.py +230 -0
  39. notionary/elements/__init__.py +0 -0
  40. notionary/models/notion_database_response.py +294 -13
  41. notionary/models/notion_page_response.py +9 -31
  42. notionary/models/search_response.py +0 -0
  43. notionary/page/__init__.py +0 -0
  44. notionary/page/client.py +110 -0
  45. notionary/page/content/page_content_retriever.py +5 -20
  46. notionary/page/content/page_content_writer.py +3 -4
  47. notionary/page/formatting/markdown_to_notion_converter.py +1 -3
  48. notionary/{prompting → page}/markdown_syntax_prompt_generator.py +1 -2
  49. notionary/page/notion_page.py +354 -317
  50. notionary/page/notion_to_markdown_converter.py +1 -4
  51. notionary/page/properites/property_value_extractor.py +0 -64
  52. notionary/page/{properites/property_formatter.py → property_formatter.py} +7 -4
  53. notionary/page/search_filter_builder.py +131 -0
  54. notionary/page/utils.py +60 -0
  55. notionary/util/__init__.py +12 -3
  56. notionary/util/factory_decorator.py +33 -0
  57. notionary/util/fuzzy_matcher.py +82 -0
  58. notionary/util/page_id_utils.py +0 -21
  59. notionary/util/singleton_metaclass.py +22 -0
  60. notionary/workspace.py +69 -0
  61. {notionary-0.2.12.dist-info → notionary-0.2.14.dist-info}/METADATA +4 -1
  62. notionary-0.2.14.dist-info/RECORD +72 -0
  63. notionary/database/database_discovery.py +0 -142
  64. notionary/database/notion_database_factory.py +0 -193
  65. notionary/elements/notion_block_element.py +0 -70
  66. notionary/exceptions/database_exceptions.py +0 -76
  67. notionary/exceptions/page_creation_exception.py +0 -9
  68. notionary/page/metadata/metadata_editor.py +0 -150
  69. notionary/page/metadata/notion_icon_manager.py +0 -77
  70. notionary/page/metadata/notion_page_cover_manager.py +0 -56
  71. notionary/page/notion_page_factory.py +0 -332
  72. notionary/page/properites/database_property_service.py +0 -302
  73. notionary/page/properites/page_property_manager.py +0 -152
  74. notionary/page/relations/notion_page_relation_manager.py +0 -350
  75. notionary/page/relations/notion_page_title_resolver.py +0 -104
  76. notionary/page/relations/page_database_relation.py +0 -68
  77. notionary/telemetry/__init__.py +0 -7
  78. notionary/telemetry/telemetry.py +0 -226
  79. notionary/telemetry/track_usage_decorator.py +0 -76
  80. notionary/util/warn_direct_constructor_usage.py +0 -54
  81. notionary-0.2.12.dist-info/RECORD +0 -70
  82. /notionary/util/{singleton.py → singleton_decorator.py} +0 -0
  83. {notionary-0.2.12.dist-info → notionary-0.2.14.dist-info}/WHEEL +0 -0
  84. {notionary-0.2.12.dist-info → notionary-0.2.14.dist-info}/entry_points.txt +0 -0
  85. {notionary-0.2.12.dist-info → notionary-0.2.14.dist-info}/licenses/LICENSE +0 -0
  86. {notionary-0.2.12.dist-info → notionary-0.2.14.dist-info}/top_level.txt +0 -0
@@ -1,29 +1,24 @@
1
1
  from __future__ import annotations
2
- from typing import Any, Dict, List, Optional
3
- import re
4
-
5
- from notionary.elements.registry.block_registry import BlockRegistry
6
- from notionary.elements.registry.block_registry_builder import BlockRegistryBuilder
7
- from notionary.notion_client import NotionClient
2
+ import asyncio
3
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING
4
+ import random
5
+
6
+ from notionary.blocks import BlockRegistry, BlockRegistryBuilder
7
+ from notionary.models.notion_database_response import NotionPageResponse
8
+ from notionary.models.notion_page_response import DatabaseParent
9
+ from notionary.page.client import NotionPageClient
8
10
  from notionary.page.content.page_content_retriever import PageContentRetriever
9
- from notionary.page.metadata.metadata_editor import MetadataEditor
10
- from notionary.page.metadata.notion_icon_manager import NotionPageIconManager
11
- from notionary.page.metadata.notion_page_cover_manager import (
12
- NotionPageCoverManager,
13
- )
14
- from notionary.page.properites.database_property_service import (
15
- DatabasePropertyService,
16
- )
17
- from notionary.page.relations.notion_page_relation_manager import (
18
- NotionPageRelationManager,
19
- )
11
+
12
+
20
13
  from notionary.page.content.page_content_writer import PageContentWriter
21
- from notionary.page.properites.page_property_manager import PagePropertyManager
22
- from notionary.page.relations.notion_page_title_resolver import NotionPageTitleResolver
23
- from notionary.util.warn_direct_constructor_usage import warn_direct_constructor_usage
24
- from notionary.util import LoggingMixin
25
- from notionary.util.page_id_utils import extract_and_validate_page_id
26
- from notionary.page.relations.page_database_relation import PageDatabaseRelation
14
+ from notionary.page.property_formatter import NotionPropertyFormatter
15
+ from notionary.page.utils import extract_property_value
16
+
17
+ from notionary.util import LoggingMixin, format_uuid, FuzzyMatcher, factory_only
18
+
19
+
20
+ if TYPE_CHECKING:
21
+ from notionary import NotionDatabase
27
22
 
28
23
 
29
24
  class NotionPage(LoggingMixin):
@@ -31,21 +26,29 @@ class NotionPage(LoggingMixin):
31
26
  Managing content and metadata of a Notion page.
32
27
  """
33
28
 
34
- @warn_direct_constructor_usage
29
+ @factory_only("from_page_id", "from_page_name")
35
30
  def __init__(
36
31
  self,
37
- page_id: Optional[str] = None,
38
- title: Optional[str] = None,
39
- url: Optional[str] = None,
32
+ page_id: str,
33
+ title: str,
34
+ url: str,
35
+ emoji_icon: Optional[str] = None,
36
+ properties: Optional[Dict[str, Any]] = None,
37
+ parent_database: Optional[NotionDatabase] = None,
40
38
  token: Optional[str] = None,
41
39
  ):
42
- self._page_id = extract_and_validate_page_id(page_id=page_id, url=url)
43
- self._url = url
40
+ """
41
+ Initialize the page manager with all metadata.
42
+ """
43
+ self._page_id = page_id
44
44
  self._title = title
45
- self._client = NotionClient(token=token)
45
+ self._url = url
46
+ self._emoji_icon = emoji_icon
47
+ self._properties = properties
48
+ self._parent_database = parent_database
49
+
50
+ self._client = NotionPageClient(token=token)
46
51
  self._page_data = None
47
- self._title_loaded = title is not None
48
- self._url_loaded = url is not None
49
52
 
50
53
  self._block_element_registry = BlockRegistryBuilder.create_full_registry()
51
54
 
@@ -57,83 +60,76 @@ class NotionPage(LoggingMixin):
57
60
 
58
61
  self._page_content_retriever = PageContentRetriever(
59
62
  page_id=self._page_id,
60
- client=self._client,
61
63
  block_registry=self._block_element_registry,
62
64
  )
63
65
 
64
- self._metadata = MetadataEditor(self._page_id, self._client)
65
- self._page_cover_manager = NotionPageCoverManager(
66
- page_id=self._page_id, client=self._client
67
- )
68
- self._page_icon_manager = NotionPageIconManager(
69
- page_id=self._page_id, client=self._client
70
- )
71
-
72
- self._db_relation = PageDatabaseRelation(
73
- page_id=self._page_id, client=self._client
74
- )
75
- self._db_property_service = None
76
-
77
- self._relation_manager = NotionPageRelationManager(
78
- page_id=self._page_id, client=self._client
79
- )
80
-
81
- self._property_manager = PagePropertyManager(
82
- self._page_id, self._client, self._metadata, self._db_relation
83
- )
84
-
85
66
  @classmethod
86
- def from_page_id(cls, page_id: str, token: Optional[str] = None) -> NotionPage:
67
+ async def from_page_id(
68
+ cls, page_id: str, token: Optional[str] = None
69
+ ) -> NotionPage:
87
70
  """
88
71
  Create a NotionPage from a page ID.
89
72
 
90
73
  Args:
91
74
  page_id: The ID of the Notion page
92
75
  token: Optional Notion API token (uses environment variable if not provided)
93
-
94
- Returns:
95
- An initialized NotionPage instance
96
76
  """
97
- from notionary.page.notion_page_factory import NotionPageFactory
77
+ formatted_id = format_uuid(page_id) or page_id
98
78
 
99
- cls.logger.info("Creating page from ID: %s", page_id)
100
- return NotionPageFactory().from_page_id(page_id, token)
79
+ async with NotionPageClient(token=token) as client:
80
+ page_response = await client.get_page(formatted_id)
81
+ return await cls._create_from_response(page_response, token)
101
82
 
102
83
  @classmethod
103
- def from_url(cls, url: str, token: Optional[str] = None) -> NotionPage:
84
+ async def from_page_name(
85
+ cls, page_name: str, token: Optional[str] = None, min_similarity: float = 0.6
86
+ ) -> NotionPage:
104
87
  """
105
- Create a NotionPage from a Notion URL.
106
-
107
- Args:
108
- url: The URL of the Notion page
109
- token: Optional Notion API token (uses environment variable if not provided)
110
-
111
- Returns:
112
- An initialized NotionPage instance
88
+ Create a NotionPage by finding a page with fuzzy matching on the title.
89
+ Uses Notion's search API and fuzzy matching to find the best result.
113
90
  """
114
- from notionary.page.notion_page_factory import NotionPageFactory
91
+ from notionary.workspace import NotionWorkspace
115
92
 
116
- cls.logger.info("Creating page from URL: %s", url)
117
- return NotionPageFactory().from_url(url, token)
93
+ workspace = NotionWorkspace()
118
94
 
119
- @classmethod
120
- async def from_page_name(
121
- cls, page_name: str, token: Optional[str] = None
122
- ) -> NotionPage:
123
- """
124
- Create a NotionPage by finding a page with a matching name.
125
- Uses fuzzy matching to find the closest match to the given name.
95
+ try:
96
+ search_results: List[NotionPage] = await workspace.search_pages(
97
+ page_name, limit=10
98
+ )
126
99
 
127
- Args:
128
- page_name: The name of the Notion page to search for
129
- token: Optional Notion API token (uses environment variable if not provided)
100
+ if not search_results:
101
+ cls.logger.warning("No pages found for name: %s", page_name)
102
+ raise ValueError(f"No pages found for name: {page_name}")
130
103
 
131
- Returns:
132
- An initialized NotionPage instance
133
- """
134
- from notionary.page.notion_page_factory import NotionPageFactory
104
+ best_match = FuzzyMatcher.find_best_match(
105
+ query=page_name,
106
+ items=search_results,
107
+ text_extractor=lambda page: page.title,
108
+ min_similarity=min_similarity,
109
+ )
110
+
111
+ if not best_match:
112
+ available_titles = [result.title for result in search_results[:5]]
113
+ cls.logger.warning(
114
+ "No sufficiently similar page found for '%s' (min: %.3f). Available: %s",
115
+ page_name,
116
+ min_similarity,
117
+ available_titles,
118
+ )
119
+ raise ValueError(
120
+ f"No sufficiently similar page found for '{page_name}'"
121
+ )
122
+
123
+ async with NotionPageClient(token=token) as client:
124
+ page_response = await client.get_page(page_id=best_match.item.id)
125
+ instance = await cls._create_from_response(
126
+ page_response=page_response, token=token
127
+ )
128
+ return instance
135
129
 
136
- return await NotionPageFactory().from_page_name(page_name, token)
130
+ except Exception as e:
131
+ cls.logger.error("Error finding page by name: %s", str(e))
132
+ raise
137
133
 
138
134
  @property
139
135
  def id(self) -> str:
@@ -143,40 +139,43 @@ class NotionPage(LoggingMixin):
143
139
  return self._page_id
144
140
 
145
141
  @property
146
- def block_registry(self) -> BlockRegistry:
142
+ def title(self) -> str:
147
143
  """
148
- Get the block element registry associated with this page.
144
+ Get the title of the page.
145
+ """
146
+ return self._title
149
147
 
150
- Returns:
151
- BlockElementRegistry: The registry of block elements.
148
+ @property
149
+ def url(self) -> str:
152
150
  """
153
- return self._block_element_registry
151
+ Get the URL of the page.
152
+ If not set, generate it from the title and ID.
153
+ """
154
+ return self._url
154
155
 
155
156
  @property
156
- def block_registry_builder(self) -> BlockRegistryBuilder:
157
+ def emoji_icon(self) -> Optional[str]:
158
+ """
159
+ Get the emoji icon of the page.
157
160
  """
158
- Get the block element registry builder associated with this page.
161
+ return self._emoji_icon
159
162
 
160
- Returns:
161
- BlockElementRegistryBuilder: The builder for block elements.
163
+ @property
164
+ def properties(self) -> Optional[Dict[str, Any]]:
165
+ """
166
+ Get the properties of the page.
162
167
  """
163
- return self._block_element_registry.builder
168
+ return self._properties
164
169
 
165
- @block_registry.setter
166
- def block_registry(self, block_registry: BlockRegistry) -> None:
170
+ @property
171
+ def block_registry(self) -> BlockRegistry:
167
172
  """
168
- Set the block element registry for the page content manager.
173
+ Get the block element registry associated with this page.
169
174
 
170
- Args:
171
- block_registry: The registry of block elements to use.
175
+ Returns:
176
+ BlockElementRegistry: The registry of block elements.
172
177
  """
173
- self._block_element_registry = block_registry
174
- self._page_content_writer = PageContentWriter(
175
- page_id=self._page_id, client=self._client, block_registry=block_registry
176
- )
177
- self._page_content_retriever = PageContentRetriever(
178
- page_id=self._page_id, client=self._client, block_registry=block_registry
179
- )
178
+ return self._block_element_registry
180
179
 
181
180
  def get_notion_markdown_system_prompt(self) -> str:
182
181
  """
@@ -187,54 +186,29 @@ class NotionPage(LoggingMixin):
187
186
  """
188
187
  return self._block_element_registry.get_notion_markdown_syntax_prompt()
189
188
 
190
- async def get_title(self) -> str:
191
- """
192
- Get the title of the page, loading it if necessary.
193
-
194
- Returns:
195
- str: The page title.
196
- """
197
- if not self._title_loaded:
198
- self._title = await self._fetch_page_title()
199
- return self._title
200
-
201
- async def set_title(self, title: str) -> Optional[Dict[str, Any]]:
189
+ async def set_title(self, title: str) -> str:
202
190
  """
203
191
  Set the title of the page.
192
+ """
193
+ try:
194
+ data = {
195
+ "properties": {
196
+ "title": {"title": [{"type": "text", "text": {"content": title}}]}
197
+ }
198
+ }
204
199
 
205
- Args:
206
- title: The new title.
200
+ await self._client.patch_page(self._page_id, data)
207
201
 
208
- Returns:
209
- Optional[Dict[str, Any]]: Response data from the API if successful, None otherwise.
210
- """
211
- result = await self._metadata.set_title(title)
212
- if result:
213
202
  self._title = title
214
- self._title_loaded = True
215
- return result
203
+ return title
216
204
 
217
- async def get_url(self) -> str:
218
- """
219
- Get the URL of the page, constructing it if necessary.
220
-
221
- Returns:
222
- str: The page URL.
223
- """
224
- if not self._url_loaded:
225
- self._url = await self._generate_url_from_title()
226
- self._url_loaded = True
227
- return self._url
205
+ except Exception as e:
206
+ self.logger.error("Error setting page title: %s", str(e))
207
+ return None
228
208
 
229
209
  async def append_markdown(self, markdown: str, append_divider=False) -> bool:
230
210
  """
231
211
  Append markdown content to the page.
232
-
233
- Args:
234
- markdown: The markdown content to append.
235
-
236
- Returns:
237
- str: Status or confirmation message.
238
212
  """
239
213
  return await self._page_content_writer.append_markdown(
240
214
  markdown_text=markdown, append_divider=append_divider
@@ -243,9 +217,6 @@ class NotionPage(LoggingMixin):
243
217
  async def clear_page_content(self) -> bool:
244
218
  """
245
219
  Clear all content from the page.
246
-
247
- Returns:
248
- str: Status or confirmation message.
249
220
  """
250
221
  return await self._page_content_writer.clear_page_content()
251
222
 
@@ -277,251 +248,317 @@ class NotionPage(LoggingMixin):
277
248
  """
278
249
  return await self._page_content_retriever.get_page_content()
279
250
 
280
- async def get_icon(self) -> str:
281
- """
282
- Retrieve the page icon - either emoji or external URL.
283
-
284
- Returns:
285
- Optional[str]: The icon emoji or URL, or None if no icon is set.
286
- """
287
- return await self._page_icon_manager.get_icon()
288
-
289
251
  async def set_emoji_icon(self, emoji: str) -> Optional[str]:
290
252
  """
291
253
  Sets the page icon to an emoji.
254
+ """
255
+ try:
256
+ icon = {"type": "emoji", "emoji": emoji}
257
+ page_response = await self._client.patch_page(
258
+ page_id=self._page_id, data={"icon": icon}
259
+ )
292
260
 
293
- Args:
294
- emoji (str): The emoji character to set as the icon.
261
+ self._emoji = page_response.icon.emoji
262
+ return page_response.icon.emoji
263
+ except Exception as e:
295
264
 
296
- Returns:
297
- Optional[Dict[str, Any]]: Response data from the API if successful, None otherwise.
298
- """
299
- return await self._page_icon_manager.set_emoji_icon(emoji=emoji)
265
+ self.logger.error(f"Error updating page emoji: {str(e)}")
266
+ return None
300
267
 
301
268
  async def set_external_icon(self, url: str) -> Optional[str]:
302
269
  """
303
270
  Sets the page icon to an external image.
271
+ """
272
+ try:
273
+ icon = {"type": "external", "external": {"url": url}}
274
+ page_response = await self._client.patch_page(
275
+ page_id=self._page_id, data={"icon": icon}
276
+ )
304
277
 
305
- Args:
306
- url (str): The URL of the external image to set as the icon.
278
+ # For external icons, we clear the emoji since we now have external icon
279
+ self._emoji = None
280
+ self.logger.info(f"Successfully updated page external icon to: {url}")
281
+ return page_response.icon.external.url
307
282
 
308
- Returns:
309
- Optional[Dict[str, Any]]: Response data from the API if successful, None otherwise.
310
- """
311
- return await self._page_icon_manager.set_external_icon(external_icon_url=url)
283
+ except Exception as e:
284
+ self.logger.error(f"Error updating page external icon: {str(e)}")
285
+ return None
312
286
 
313
287
  async def get_cover_url(self) -> Optional[str]:
314
288
  """
315
289
  Get the URL of the page cover image.
316
-
317
- Returns:
318
- str: The URL of the cover image or empty string if not available.
319
290
  """
320
- return await self._page_cover_manager.get_cover_url()
291
+ try:
292
+ page_data = await self._client.get_page(self.id)
293
+ if not page_data or not page_data.cover:
294
+ return None
295
+ if page_data.cover.type == "external":
296
+ return page_data.cover.external.url
297
+ except Exception as e:
298
+ self.logger.error(f"Error fetching cover URL: {str(e)}")
299
+ return None
321
300
 
322
301
  async def set_cover(self, external_url: str) -> Optional[str]:
323
302
  """
324
303
  Set the cover image for the page using an external URL.
325
-
326
- Args:
327
- external_url: URL to the external image.
328
-
329
- Returns:
330
- Optional[Dict[str, Any]]: Response data from the API if successful, None otherwise.
331
304
  """
332
- return await self._page_cover_manager.set_cover(external_url)
305
+ data = {"cover": {"type": "external", "external": {"url": external_url}}}
306
+ try:
307
+ updated_page = await self._client.patch_page(self.id, data=data)
308
+ return updated_page.cover.external.url
309
+ except Exception as e:
310
+ self.logger.error("Failed to set cover image: %s", str(e))
311
+ return None
333
312
 
334
- async def set_random_gradient_cover(self) -> Optional[Dict[str, Any]]:
313
+ async def set_random_gradient_cover(self) -> Optional[str]:
335
314
  """
336
315
  Set a random gradient as the page cover.
337
-
338
- Returns:
339
- Optional[Dict[str, Any]]: Response data from the API if successful, None otherwise.
340
316
  """
341
- return await self._page_cover_manager.set_random_gradient_cover()
317
+ default_notion_covers = [
318
+ f"https://www.notion.so/images/page-cover/gradients_{i}.png"
319
+ for i in range(1, 10)
320
+ ]
321
+ random_cover_url = random.choice(default_notion_covers)
322
+ return await self.set_cover(random_cover_url)
342
323
 
343
324
  async def get_property_value_by_name(self, property_name: str) -> Any:
344
325
  """
345
326
  Get the value of a specific property.
346
-
347
- Args:
348
- property_name: The name of the property.
349
-
350
- Returns:
351
- Any: The value of the property.
352
327
  """
353
- properties = await self._property_manager._get_properties()
328
+ if not self._parent_database:
329
+ return None
330
+
331
+ database_property_schema = self._parent_database.properties.get(property_name)
354
332
 
355
- if property_name not in properties:
333
+ if not database_property_schema:
334
+ self.logger.warning(
335
+ "Property '%s' not found in database schema", property_name
336
+ )
356
337
  return None
357
338
 
358
- prop_data = properties[property_name]
359
- prop_type = prop_data.get("type")
339
+ property_type = database_property_schema.get("type")
360
340
 
361
- if prop_type == "relation":
362
- return await self._relation_manager.get_relation_values(property_name)
341
+ if property_type == "relation":
342
+ return await self._get_relation_property_values_by_name(property_name)
343
+
344
+ if property_name not in self._properties:
345
+ self.logger.warning(
346
+ "Property '%s' not found in page properties", property_name
347
+ )
348
+ return None
363
349
 
364
- return await self._property_manager.get_property_value(property_name)
350
+ property_data = self._properties.get(property_name)
351
+ return extract_property_value(property_data)
365
352
 
366
- async def get_options_for_property(
367
- self, property_name: str, limit: int = 100
353
+ async def _get_relation_property_values_by_name(
354
+ self, property_name: str
368
355
  ) -> List[str]:
369
356
  """
370
- Get the available options for a property (select, multi_select, status, relation).
371
-
372
- Args:
373
- property_name: The name of the property.
374
- limit: Maximum number of options to return (only affects relation properties).
375
-
376
- Returns:
377
- List[str]: List of available option names or page titles.
357
+ Retrieve the titles of all related pages for a relation property.
378
358
  """
379
- property_type = await self._get_property_type(property_name)
359
+ page_property_schema = self.properties.get(property_name)
360
+ relation_page_ids = [
361
+ rel.get("id") for rel in page_property_schema.get("relation", [])
362
+ ]
363
+ notion_pages = [
364
+ await NotionPage.from_page_id(page_id) for page_id in relation_page_ids
365
+ ]
366
+ return [page.title for page in notion_pages if page]
380
367
 
381
- if property_type is None:
368
+ async def get_options_for_property_by_name(self, property_name: str) -> List[str]:
369
+ """
370
+ Get the available options for a property (select, multi_select, status, relation).
371
+ """
372
+ if not self._parent_database:
373
+ self.logger.error(
374
+ "Parent database not set. Cannot get options for property: %s",
375
+ property_name,
376
+ )
382
377
  return []
383
378
 
384
- if property_type == "relation":
385
- return await self._relation_manager.get_relation_options(
386
- property_name, limit
379
+ try:
380
+ return await self._parent_database.get_options_by_property_name(
381
+ property_name=property_name
387
382
  )
383
+ except Exception as e:
384
+ self.logger.error(
385
+ "Error getting options for property '%s': %s", property_name, str(e)
386
+ )
387
+ return []
388
388
 
389
- db_service = await self._get_db_property_service()
390
- if db_service:
391
- return await db_service.get_option_names(property_name)
392
-
393
- return []
394
-
395
- async def set_property_value_by_name(
396
- self, property_name: str, value: Any
397
- ) -> Optional[Dict[str, Any]]:
389
+ # Diese Methode hier sollte auch für relation properties funktionieren aber gerne auch eine dedizierte hier
390
+ async def set_property_value_by_name(self, property_name: str, value: Any) -> Any:
398
391
  """
399
392
  Set the value of a specific property by its name.
393
+ """
394
+ if not self._parent_database:
395
+ return None
400
396
 
401
- Args:
402
- property_name: The name of the property.
403
- value: The new value to set.
397
+ property_type = self._parent_database.properties.get(property_name).get("type")
404
398
 
405
- Returns:
406
- Optional[Dict[str, Any]]: Response data from the API if successful, None otherwise.
407
- """
408
- return await self._property_manager.set_property_by_name(
409
- property_name=property_name,
410
- value=value,
399
+ if not property_type:
400
+ return None
401
+
402
+ if property_type == "relation":
403
+ return await self.set_relation_property_values_by_name(
404
+ property_name=property_name, page_titles=value
405
+ )
406
+
407
+ property_formatter = NotionPropertyFormatter()
408
+ update_data = property_formatter.format_value(
409
+ property_name=property_name, property_type=property_type, value=value
411
410
  )
412
411
 
412
+ try:
413
+ updated_page_response = await self._client.patch_page(
414
+ page_id=self._page_id, data=update_data
415
+ )
416
+ self._properties = updated_page_response.properties
417
+ return extract_property_value(self._properties.get(property_name))
418
+ except Exception as e:
419
+ self.logger.error(
420
+ "Error setting property '%s' to value '%s': %s",
421
+ property_name,
422
+ value,
423
+ str(e),
424
+ )
425
+ return None
426
+
413
427
  async def set_relation_property_values_by_name(
414
- self, relation_property_name: str, page_titles: List[str]
428
+ self, property_name: str, page_titles: List[str]
415
429
  ) -> List[str]:
416
430
  """
417
431
  Add one or more relations to a relation property.
432
+ """
433
+ if not self._parent_database:
434
+ return []
418
435
 
419
- Args:
420
- relation_property_name: The name of the relation property.
421
- page_titles: A list of page titles to relate to.
436
+ property_type = self._parent_database.properties.get(property_name).get("type")
422
437
 
423
- Returns:
424
- Optional[Dict[str, Any]]: Response data from the API if successful, None otherwise.
425
- """
426
- return await self._relation_manager.set_relation_values_by_page_titles(
427
- property_name=relation_property_name, page_titles=page_titles
438
+ # for direct calls
439
+ if property_type != "relation":
440
+ return []
441
+
442
+ relation_pages = await asyncio.gather(
443
+ *(
444
+ NotionPage.from_page_name(page_name=page_title)
445
+ for page_title in page_titles
446
+ )
428
447
  )
429
448
 
430
- async def get_relation_property_values_by_name(
431
- self, property_name: str
432
- ) -> List[str]:
433
- """
434
- Return the current relation values for a property.
449
+ relation_page_ids = [page.id for page in relation_pages]
435
450
 
436
- Args:
437
- property_name: The name of the relation property.
451
+ property_formatter = NotionPropertyFormatter()
438
452
 
439
- Returns:
440
- List[str]: List of relation values.
441
- """
442
- return await self._relation_manager.get_relation_values(property_name)
443
-
444
- async def get_last_edit_time(self) -> str:
445
- """
446
- Get the timestamp when the page was last edited.
453
+ update_data = property_formatter.format_value(
454
+ property_name=property_name,
455
+ property_type="relation",
456
+ value=relation_page_ids,
457
+ )
447
458
 
448
- Returns:
449
- str: ISO 8601 formatted timestamp string of when the page was last edited.
450
- """
451
459
  try:
452
- page_response = await self._client.get_page(self._page_id)
453
- return (
454
- page_response.last_edited_time if page_response.last_edited_by else ""
460
+ updated_page_response = await self._client.patch_page(
461
+ page_id=self._page_id, data=update_data
455
462
  )
456
-
463
+ self._properties = updated_page_response.properties
464
+ return page_titles
457
465
  except Exception as e:
458
- self.logger.error("Error retrieving last edited time: %s", str(e))
459
- return ""
466
+ self.logger.error(
467
+ "Error setting property '%s' to value '%s': %s",
468
+ property_name,
469
+ page_titles,
470
+ str(e),
471
+ )
472
+ return []
460
473
 
461
- async def _fetch_page_title(self) -> str:
474
+ async def archive(self) -> bool:
462
475
  """
463
- Load the page title from Notion API if not already loaded.
464
-
465
- Returns:
466
- str: The page title.
476
+ Archive the page by moving it to the trash.
467
477
  """
468
- notion_page_title_resolver = NotionPageTitleResolver(self._client)
469
- return await notion_page_title_resolver.get_title_by_page_id(
470
- page_id=self._page_id
471
- )
478
+ try:
479
+ result = await self._client.patch_page(
480
+ page_id=self._page_id, data={"archived": True}
481
+ )
482
+ return result is not None
483
+ except Exception as e:
484
+ self.logger.error("Error archiving page %s: %s", self._page_id, str(e))
485
+ return False
472
486
 
473
- async def _generate_url_from_title(self) -> str:
487
+ @classmethod
488
+ async def _create_from_response(
489
+ cls,
490
+ page_response: NotionPageResponse,
491
+ token: Optional[str],
492
+ ) -> NotionPage:
474
493
  """
475
- Build a Notion URL from the page ID, including the title if available.
476
-
477
- Returns:
478
- str: The Notion URL for the page.
494
+ Create NotionPage instance from API response.
479
495
  """
480
- title = await self._fetch_page_title()
496
+ from notionary.database.notion_database import NotionDatabase
481
497
 
482
- url_title = ""
483
- if title and title != "Untitled":
484
- url_title = re.sub(r"[^\w\s-]", "", title)
485
- url_title = re.sub(r"[\s]+", "-", url_title)
486
- url_title = f"{url_title}-"
498
+ title = cls._extract_title(page_response)
499
+ emoji = cls._extract_emoji(page_response)
500
+ parent_database_id = cls._extract_parent_database_id(page_response)
487
501
 
488
- clean_id = self._page_id.replace("-", "")
489
-
490
- return f"https://www.notion.so/{url_title}{clean_id}"
502
+ parent_database = (
503
+ await NotionDatabase.from_database_id(
504
+ database_id=parent_database_id, token=token
505
+ )
506
+ if parent_database_id
507
+ else None
508
+ )
491
509
 
492
- async def _get_db_property_service(self) -> Optional[DatabasePropertyService]:
493
- """
494
- Gets the database property service, initializing it if necessary.
495
- This is a more intuitive way to work with the instance variable.
510
+ instance = cls(
511
+ page_id=page_response.id,
512
+ title=title,
513
+ url=page_response.url,
514
+ emoji_icon=emoji,
515
+ properties=page_response.properties,
516
+ parent_database=parent_database,
517
+ token=token,
518
+ )
496
519
 
497
- Returns:
498
- Optional[DatabasePropertyService]: The database property service or None if not applicable
499
- """
500
- if self._db_property_service is not None:
501
- return self._db_property_service
520
+ cls.logger.info("Created page manager: '%s' (ID: %s)", title, page_response.id)
521
+ return instance
502
522
 
503
- database_id = await self._db_relation.get_parent_database_id()
504
- if not database_id:
505
- return None
523
+ @staticmethod
524
+ def _extract_title(page_response: NotionPageResponse) -> str:
525
+ """Extract title from page response. Returns empty string if not found."""
506
526
 
507
- self._db_property_service = DatabasePropertyService(database_id, self._client)
508
- await self._db_property_service.load_schema()
509
- return self._db_property_service
527
+ if not page_response.properties:
528
+ return ""
510
529
 
511
- async def _get_property_type(self, property_name: str) -> Optional[str]:
512
- """
513
- Get the type of a specific property.
530
+ title_property = next(
531
+ (
532
+ prop
533
+ for prop in page_response.properties.values()
534
+ if isinstance(prop, dict) and prop.get("type") == "title"
535
+ ),
536
+ None,
537
+ )
514
538
 
515
- Args:
516
- property_name: The name of the property.
539
+ if not title_property or "title" not in title_property:
540
+ return ""
517
541
 
518
- Returns:
519
- Optional[str]: The type of the property, or None if not found.
520
- """
521
- properties = await self._property_manager._get_properties()
542
+ try:
543
+ title_parts = title_property["title"]
544
+ return "".join(part.get("plain_text", "") for part in title_parts)
545
+ except (KeyError, TypeError, AttributeError):
546
+ return ""
522
547
 
523
- if property_name not in properties:
548
+ @staticmethod
549
+ def _extract_emoji(page_response: NotionPageResponse) -> Optional[str]:
550
+ """Extract emoji from database response."""
551
+ if not page_response.icon:
524
552
  return None
525
553
 
526
- prop_data = properties[property_name]
527
- return prop_data.get("type")
554
+ if page_response.icon.type == "emoji":
555
+ return page_response.icon.emoji
556
+
557
+ return None
558
+
559
+ @staticmethod
560
+ def _extract_parent_database_id(page_response: NotionPageResponse) -> Optional[str]:
561
+ """Extract parent database ID from page response."""
562
+ parent = page_response.parent
563
+ if isinstance(parent, DatabaseParent):
564
+ return parent.database_id