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.
- notionary/__init__.py +3 -20
- notionary/{notion_client.py → base_notion_client.py} +92 -98
- notionary/blocks/__init__.py +61 -0
- notionary/{elements → blocks}/audio_element.py +6 -4
- notionary/{elements → blocks}/bookmark_element.py +3 -6
- notionary/{elements → blocks}/bulleted_list_element.py +5 -7
- notionary/{elements → blocks}/callout_element.py +5 -8
- notionary/{elements → blocks}/code_block_element.py +4 -6
- notionary/{elements → blocks}/column_element.py +3 -6
- notionary/{elements → blocks}/divider_element.py +3 -6
- notionary/{elements → blocks}/embed_element.py +4 -6
- notionary/{elements → blocks}/heading_element.py +5 -9
- notionary/{elements → blocks}/image_element.py +4 -6
- notionary/{elements → blocks}/mention_element.py +3 -7
- notionary/blocks/notion_block_client.py +26 -0
- notionary/blocks/notion_block_element.py +34 -0
- notionary/{elements → blocks}/numbered_list_element.py +4 -7
- notionary/{elements → blocks}/paragraph_element.py +4 -7
- notionary/{prompting/element_prompt_content.py → blocks/prompts/element_prompt_builder.py} +1 -40
- notionary/blocks/prompts/element_prompt_content.py +41 -0
- notionary/{elements → blocks}/qoute_element.py +4 -6
- notionary/{elements → blocks}/registry/block_registry.py +4 -26
- notionary/{elements → blocks}/registry/block_registry_builder.py +26 -25
- notionary/{elements → blocks}/table_element.py +6 -8
- notionary/{elements → blocks}/text_inline_formatter.py +1 -4
- notionary/{elements → blocks}/todo_element.py +6 -8
- notionary/{elements → blocks}/toggle_element.py +3 -6
- notionary/{elements → blocks}/toggleable_heading_element.py +5 -8
- notionary/{elements → blocks}/video_element.py +4 -6
- notionary/cli/main.py +245 -53
- notionary/cli/onboarding.py +117 -0
- notionary/database/__init__.py +0 -0
- notionary/database/client.py +132 -0
- notionary/database/database_exceptions.py +13 -0
- notionary/database/factory.py +0 -0
- notionary/database/filter_builder.py +175 -0
- notionary/database/notion_database.py +339 -128
- notionary/database/notion_database_provider.py +230 -0
- notionary/elements/__init__.py +0 -0
- notionary/models/notion_database_response.py +294 -13
- notionary/models/notion_page_response.py +9 -31
- notionary/models/search_response.py +0 -0
- notionary/page/__init__.py +0 -0
- notionary/page/client.py +110 -0
- notionary/page/content/page_content_retriever.py +5 -20
- notionary/page/content/page_content_writer.py +3 -4
- notionary/page/formatting/markdown_to_notion_converter.py +1 -3
- notionary/{prompting → page}/markdown_syntax_prompt_generator.py +1 -2
- notionary/page/notion_page.py +354 -317
- notionary/page/notion_to_markdown_converter.py +1 -4
- notionary/page/properites/property_value_extractor.py +0 -64
- notionary/page/{properites/property_formatter.py → property_formatter.py} +7 -4
- notionary/page/search_filter_builder.py +131 -0
- notionary/page/utils.py +60 -0
- notionary/util/__init__.py +12 -3
- notionary/util/factory_decorator.py +33 -0
- notionary/util/fuzzy_matcher.py +82 -0
- notionary/util/page_id_utils.py +0 -21
- notionary/util/singleton_metaclass.py +22 -0
- notionary/workspace.py +69 -0
- {notionary-0.2.12.dist-info → notionary-0.2.14.dist-info}/METADATA +4 -1
- notionary-0.2.14.dist-info/RECORD +72 -0
- notionary/database/database_discovery.py +0 -142
- notionary/database/notion_database_factory.py +0 -193
- notionary/elements/notion_block_element.py +0 -70
- notionary/exceptions/database_exceptions.py +0 -76
- notionary/exceptions/page_creation_exception.py +0 -9
- notionary/page/metadata/metadata_editor.py +0 -150
- notionary/page/metadata/notion_icon_manager.py +0 -77
- notionary/page/metadata/notion_page_cover_manager.py +0 -56
- notionary/page/notion_page_factory.py +0 -332
- notionary/page/properites/database_property_service.py +0 -302
- notionary/page/properites/page_property_manager.py +0 -152
- notionary/page/relations/notion_page_relation_manager.py +0 -350
- notionary/page/relations/notion_page_title_resolver.py +0 -104
- notionary/page/relations/page_database_relation.py +0 -68
- notionary/telemetry/__init__.py +0 -7
- notionary/telemetry/telemetry.py +0 -226
- notionary/telemetry/track_usage_decorator.py +0 -76
- notionary/util/warn_direct_constructor_usage.py +0 -54
- notionary-0.2.12.dist-info/RECORD +0 -70
- /notionary/util/{singleton.py → singleton_decorator.py} +0 -0
- {notionary-0.2.12.dist-info → notionary-0.2.14.dist-info}/WHEEL +0 -0
- {notionary-0.2.12.dist-info → notionary-0.2.14.dist-info}/entry_points.txt +0 -0
- {notionary-0.2.12.dist-info → notionary-0.2.14.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.2.12.dist-info → notionary-0.2.14.dist-info}/top_level.txt +0 -0
notionary/page/notion_page.py
CHANGED
@@ -1,29 +1,24 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
|
3
|
-
import
|
4
|
-
|
5
|
-
|
6
|
-
from notionary.
|
7
|
-
from notionary.
|
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
|
-
|
10
|
-
|
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.
|
22
|
-
from notionary.page.
|
23
|
-
|
24
|
-
from notionary.util import LoggingMixin
|
25
|
-
|
26
|
-
|
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
|
-
@
|
29
|
+
@factory_only("from_page_id", "from_page_name")
|
35
30
|
def __init__(
|
36
31
|
self,
|
37
|
-
page_id:
|
38
|
-
title:
|
39
|
-
url:
|
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
|
-
|
43
|
-
|
40
|
+
"""
|
41
|
+
Initialize the page manager with all metadata.
|
42
|
+
"""
|
43
|
+
self._page_id = page_id
|
44
44
|
self._title = title
|
45
|
-
self.
|
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(
|
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
|
-
|
77
|
+
formatted_id = format_uuid(page_id) or page_id
|
98
78
|
|
99
|
-
|
100
|
-
|
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
|
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
|
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.
|
91
|
+
from notionary.workspace import NotionWorkspace
|
115
92
|
|
116
|
-
|
117
|
-
return NotionPageFactory().from_url(url, token)
|
93
|
+
workspace = NotionWorkspace()
|
118
94
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
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
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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
|
-
|
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
|
142
|
+
def title(self) -> str:
|
147
143
|
"""
|
148
|
-
Get the
|
144
|
+
Get the title of the page.
|
145
|
+
"""
|
146
|
+
return self._title
|
149
147
|
|
150
|
-
|
151
|
-
|
148
|
+
@property
|
149
|
+
def url(self) -> str:
|
152
150
|
"""
|
153
|
-
|
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
|
157
|
+
def emoji_icon(self) -> Optional[str]:
|
158
|
+
"""
|
159
|
+
Get the emoji icon of the page.
|
157
160
|
"""
|
158
|
-
|
161
|
+
return self._emoji_icon
|
159
162
|
|
160
|
-
|
161
|
-
|
163
|
+
@property
|
164
|
+
def properties(self) -> Optional[Dict[str, Any]]:
|
165
|
+
"""
|
166
|
+
Get the properties of the page.
|
162
167
|
"""
|
163
|
-
return self.
|
168
|
+
return self._properties
|
164
169
|
|
165
|
-
@
|
166
|
-
def block_registry(self
|
170
|
+
@property
|
171
|
+
def block_registry(self) -> BlockRegistry:
|
167
172
|
"""
|
168
|
-
|
173
|
+
Get the block element registry associated with this page.
|
169
174
|
|
170
|
-
|
171
|
-
|
175
|
+
Returns:
|
176
|
+
BlockElementRegistry: The registry of block elements.
|
172
177
|
"""
|
173
|
-
self._block_element_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
|
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
|
-
|
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
|
-
|
215
|
-
return result
|
203
|
+
return title
|
216
204
|
|
217
|
-
|
218
|
-
|
219
|
-
|
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
|
-
|
294
|
-
|
261
|
+
self._emoji = page_response.icon.emoji
|
262
|
+
return page_response.icon.emoji
|
263
|
+
except Exception as e:
|
295
264
|
|
296
|
-
|
297
|
-
|
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
|
-
|
306
|
-
|
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
|
-
|
309
|
-
|
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
|
-
|
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
|
-
|
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[
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
359
|
-
prop_type = prop_data.get("type")
|
339
|
+
property_type = database_property_schema.get("type")
|
360
340
|
|
361
|
-
if
|
362
|
-
return await self.
|
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
|
-
|
350
|
+
property_data = self._properties.get(property_name)
|
351
|
+
return extract_property_value(property_data)
|
365
352
|
|
366
|
-
async def
|
367
|
-
self, property_name: str
|
353
|
+
async def _get_relation_property_values_by_name(
|
354
|
+
self, property_name: str
|
368
355
|
) -> List[str]:
|
369
356
|
"""
|
370
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
385
|
-
return await self.
|
386
|
-
property_name
|
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
|
-
|
390
|
-
|
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
|
-
|
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
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
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,
|
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
|
-
|
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
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
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
|
-
|
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
|
-
|
437
|
-
property_name: The name of the relation property.
|
451
|
+
property_formatter = NotionPropertyFormatter()
|
438
452
|
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
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
|
-
|
453
|
-
|
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(
|
459
|
-
|
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
|
474
|
+
async def archive(self) -> bool:
|
462
475
|
"""
|
463
|
-
|
464
|
-
|
465
|
-
Returns:
|
466
|
-
str: The page title.
|
476
|
+
Archive the page by moving it to the trash.
|
467
477
|
"""
|
468
|
-
|
469
|
-
|
470
|
-
|
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
|
-
|
487
|
+
@classmethod
|
488
|
+
async def _create_from_response(
|
489
|
+
cls,
|
490
|
+
page_response: NotionPageResponse,
|
491
|
+
token: Optional[str],
|
492
|
+
) -> NotionPage:
|
474
493
|
"""
|
475
|
-
|
476
|
-
|
477
|
-
Returns:
|
478
|
-
str: The Notion URL for the page.
|
494
|
+
Create NotionPage instance from API response.
|
479
495
|
"""
|
480
|
-
|
496
|
+
from notionary.database.notion_database import NotionDatabase
|
481
497
|
|
482
|
-
|
483
|
-
|
484
|
-
|
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
|
-
|
489
|
-
|
490
|
-
|
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
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
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
|
-
|
498
|
-
|
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
|
-
|
504
|
-
|
505
|
-
|
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
|
-
|
508
|
-
|
509
|
-
return self._db_property_service
|
527
|
+
if not page_response.properties:
|
528
|
+
return ""
|
510
529
|
|
511
|
-
|
512
|
-
|
513
|
-
|
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
|
-
|
516
|
-
|
539
|
+
if not title_property or "title" not in title_property:
|
540
|
+
return ""
|
517
541
|
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
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
|
-
|
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
|
-
|
527
|
-
|
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
|