notionary 0.1.29__py3-none-any.whl → 0.2.1__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 +5 -5
- notionary/database/notion_database.py +50 -59
- notionary/database/notion_database_factory.py +16 -20
- notionary/elements/audio_element.py +1 -1
- notionary/elements/bookmark_element.py +1 -1
- notionary/elements/bulleted_list_element.py +2 -8
- notionary/elements/callout_element.py +1 -1
- notionary/elements/code_block_element.py +1 -1
- notionary/elements/divider_element.py +1 -1
- notionary/elements/embed_element.py +1 -1
- notionary/elements/heading_element.py +2 -8
- notionary/elements/image_element.py +1 -1
- notionary/elements/mention_element.py +1 -1
- notionary/elements/notion_block_element.py +1 -1
- notionary/elements/numbered_list_element.py +2 -7
- notionary/elements/paragraph_element.py +1 -1
- notionary/elements/qoute_element.py +1 -1
- notionary/elements/registry/{block_element_registry.py → block_registry.py} +70 -26
- notionary/elements/registry/{block_element_registry_builder.py → block_registry_builder.py} +48 -32
- notionary/elements/table_element.py +1 -1
- notionary/elements/text_inline_formatter.py +13 -9
- notionary/elements/todo_element.py +1 -1
- notionary/elements/toggle_element.py +1 -1
- notionary/elements/toggleable_heading_element.py +1 -1
- notionary/elements/video_element.py +1 -1
- notionary/models/notion_block_response.py +264 -0
- notionary/models/notion_database_response.py +63 -0
- notionary/models/notion_page_response.py +100 -0
- notionary/notion_client.py +38 -5
- notionary/page/content/page_content_retriever.py +68 -0
- notionary/page/content/page_content_writer.py +103 -0
- notionary/page/markdown_to_notion_converter.py +5 -5
- notionary/page/metadata/metadata_editor.py +91 -63
- notionary/page/metadata/notion_icon_manager.py +55 -28
- notionary/page/metadata/notion_page_cover_manager.py +23 -20
- notionary/page/notion_page.py +223 -218
- notionary/page/notion_page_factory.py +102 -151
- notionary/page/notion_to_markdown_converter.py +5 -5
- notionary/page/properites/database_property_service.py +11 -55
- notionary/page/properites/page_property_manager.py +44 -67
- notionary/page/properites/property_value_extractor.py +3 -3
- notionary/page/relations/notion_page_relation_manager.py +165 -213
- notionary/page/relations/notion_page_title_resolver.py +59 -41
- notionary/page/relations/page_database_relation.py +7 -9
- notionary/{elements/prompts → prompting}/element_prompt_content.py +19 -4
- notionary/prompting/markdown_syntax_prompt_generator.py +92 -0
- notionary/util/logging_mixin.py +17 -8
- notionary/util/warn_direct_constructor_usage.py +54 -0
- {notionary-0.1.29.dist-info → notionary-0.2.1.dist-info}/METADATA +2 -1
- notionary-0.2.1.dist-info/RECORD +60 -0
- {notionary-0.1.29.dist-info → notionary-0.2.1.dist-info}/WHEEL +1 -1
- notionary/database/database_info_service.py +0 -43
- notionary/elements/prompts/synthax_prompt_builder.py +0 -150
- notionary/page/content/page_content_manager.py +0 -211
- notionary/page/properites/property_operation_result.py +0 -116
- notionary/page/relations/relation_operation_result.py +0 -144
- notionary-0.1.29.dist-info/RECORD +0 -58
- {notionary-0.1.29.dist-info → notionary-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.1.29.dist-info → notionary-0.2.1.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,4 @@
|
|
1
|
-
import
|
2
|
-
from typing import List, Optional, Dict, Any
|
1
|
+
from typing import List, Optional, Dict, Any, Tuple
|
3
2
|
from difflib import SequenceMatcher
|
4
3
|
|
5
4
|
from notionary import NotionPage, NotionClient
|
@@ -13,230 +12,182 @@ class NotionPageFactory(LoggingMixin):
|
|
13
12
|
Provides methods for creating page instances by page ID, URL, or name.
|
14
13
|
"""
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
"""Class logger - for class methods"""
|
19
|
-
return logging.getLogger(cls.__name__)
|
15
|
+
MATCH_THRESHOLD = 0.6
|
16
|
+
MAX_SUGGESTIONS = 5
|
20
17
|
|
21
18
|
@classmethod
|
22
|
-
|
23
|
-
|
24
|
-
) -> NotionPage:
|
25
|
-
"""
|
26
|
-
Create a NotionPage from a page ID.
|
27
|
-
|
28
|
-
Args:
|
29
|
-
page_id: The ID of the Notion page
|
30
|
-
token: Optional Notion API token (uses environment variable if not provided)
|
31
|
-
|
32
|
-
Returns:
|
33
|
-
An initialized NotionPage instance
|
34
|
-
|
35
|
-
Raises:
|
36
|
-
NotionError: If there is any error during page creation or connection
|
37
|
-
"""
|
38
|
-
logger = cls.class_logger()
|
39
|
-
|
19
|
+
def from_page_id(cls, page_id: str, token: Optional[str] = None) -> NotionPage:
|
20
|
+
"""Create a NotionPage from a page ID."""
|
40
21
|
try:
|
41
22
|
formatted_id = format_uuid(page_id) or page_id
|
42
|
-
|
43
23
|
page = NotionPage(page_id=formatted_id, token=token)
|
44
|
-
|
45
|
-
|
24
|
+
cls.logger.info(
|
25
|
+
"Successfully created page instance for ID: %s", formatted_id
|
26
|
+
)
|
46
27
|
return page
|
47
|
-
|
48
28
|
except Exception as e:
|
49
|
-
|
50
|
-
|
29
|
+
cls.logger.error("Error connecting to page %s: %s", page_id, str(e))
|
30
|
+
raise
|
51
31
|
|
52
32
|
@classmethod
|
53
|
-
|
54
|
-
"""
|
55
|
-
Create a NotionPage from a Notion URL.
|
56
|
-
|
57
|
-
Args:
|
58
|
-
url: The URL of the Notion page
|
59
|
-
token: Optional Notion API token (uses environment variable if not provided)
|
60
|
-
|
61
|
-
Returns:
|
62
|
-
An initialized NotionPage instance
|
63
|
-
|
64
|
-
Raises:
|
65
|
-
NotionError: If there is any error during page creation or connection
|
66
|
-
"""
|
67
|
-
logger = cls.class_logger()
|
33
|
+
def from_url(cls, url: str, token: Optional[str] = None) -> NotionPage:
|
34
|
+
"""Create a NotionPage from a Notion URL."""
|
68
35
|
|
69
36
|
try:
|
70
37
|
page_id = extract_and_validate_page_id(url=url)
|
71
38
|
if not page_id:
|
72
|
-
|
73
|
-
|
39
|
+
cls.logger.error("Could not extract valid page ID from URL: %s", url)
|
40
|
+
raise ValueError(f"Invalid URL: {url}")
|
74
41
|
|
75
42
|
page = NotionPage(page_id=page_id, url=url, token=token)
|
76
|
-
|
77
|
-
logger.info(
|
43
|
+
cls.logger.info(
|
78
44
|
"Successfully created page instance from URL for ID: %s", page_id
|
79
45
|
)
|
80
46
|
return page
|
81
|
-
|
82
47
|
except Exception as e:
|
83
|
-
|
84
|
-
|
48
|
+
cls.logger.error("Error connecting to page with URL %s: %s", url, str(e))
|
49
|
+
raise
|
85
50
|
|
86
51
|
@classmethod
|
87
52
|
async def from_page_name(
|
88
53
|
cls, page_name: str, token: Optional[str] = None
|
89
54
|
) -> NotionPage:
|
90
|
-
"""
|
91
|
-
|
92
|
-
Uses fuzzy matching to find the closest match to the given name.
|
93
|
-
If no good match is found, suggests closest alternatives ("Did you mean?").
|
94
|
-
|
95
|
-
Args:
|
96
|
-
page_name: The name of the Notion page to search for
|
97
|
-
token: Optional Notion API token (uses environment variable if not provided)
|
98
|
-
|
99
|
-
Returns:
|
100
|
-
An initialized NotionPage instance
|
101
|
-
|
102
|
-
Raises:
|
103
|
-
NotionError: If there is any error during page search or connection
|
104
|
-
NotionPageNotFoundError: If no matching page found, includes suggestions
|
105
|
-
"""
|
106
|
-
logger = cls.class_logger()
|
107
|
-
logger.debug("Searching for page with name: %s", page_name)
|
55
|
+
"""Create a NotionPage by finding a page with a matching name using fuzzy matching."""
|
56
|
+
cls.logger.debug("Searching for page with name: %s", page_name)
|
108
57
|
|
109
58
|
client = NotionClient(token=token)
|
110
59
|
|
111
60
|
try:
|
112
|
-
|
113
|
-
|
114
|
-
search_payload = {
|
115
|
-
"filter": {"property": "object", "value": "page"},
|
116
|
-
"page_size": 100,
|
117
|
-
}
|
118
|
-
|
119
|
-
response = await client.post("search", search_payload)
|
120
|
-
|
121
|
-
if not response or "results" not in response:
|
122
|
-
error_msg = "Failed to fetch pages using search endpoint"
|
123
|
-
logger.error(error_msg)
|
124
|
-
|
125
|
-
pages = response.get("results", [])
|
126
|
-
|
61
|
+
# Fetch pages
|
62
|
+
pages = await cls._search_pages(client)
|
127
63
|
if not pages:
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
logger.debug("Found %d pages, searching for best match", len(pages))
|
132
|
-
|
133
|
-
# Store all matches with their scores for potential suggestions
|
134
|
-
matches = []
|
135
|
-
best_match = None
|
136
|
-
best_score = 0
|
137
|
-
|
138
|
-
for page in pages:
|
139
|
-
title = cls._extract_title_from_page(page)
|
140
|
-
score = SequenceMatcher(None, page_name.lower(), title.lower()).ratio()
|
141
|
-
|
142
|
-
matches.append((page, title, score))
|
64
|
+
cls.logger.warning("No pages found matching '%s'", page_name)
|
65
|
+
raise ValueError(f"No pages found matching '{page_name}'")
|
143
66
|
|
144
|
-
|
145
|
-
|
146
|
-
best_match = page
|
67
|
+
# Find best match
|
68
|
+
best_match, best_score, suggestions = cls._find_best_match(pages, page_name)
|
147
69
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
# Take top N suggestions (adjust as needed)
|
153
|
-
suggestions = [title for _, title, _ in matches[:5]]
|
154
|
-
|
155
|
-
error_msg = f"No good match found for '{page_name}'. Did you mean one of these?\n"
|
156
|
-
error_msg += "\n".join(f"- {suggestion}" for suggestion in suggestions)
|
157
|
-
|
158
|
-
logger.warning(
|
70
|
+
# Check if match is good enough
|
71
|
+
if best_score < cls.MATCH_THRESHOLD or not best_match:
|
72
|
+
suggestion_msg = cls._format_suggestions(suggestions)
|
73
|
+
cls.logger.warning(
|
159
74
|
"No good match found for '%s'. Best score: %.2f",
|
160
75
|
page_name,
|
161
76
|
best_score,
|
162
77
|
)
|
78
|
+
raise ValueError(
|
79
|
+
f"No good match found for '{page_name}'. {suggestion_msg}"
|
80
|
+
)
|
163
81
|
|
82
|
+
# Create page from best match
|
164
83
|
page_id = best_match.get("id")
|
165
|
-
|
166
84
|
if not page_id:
|
167
|
-
|
168
|
-
|
85
|
+
cls.logger.error("Best match page has no ID")
|
86
|
+
raise ValueError("Best match page has no ID")
|
169
87
|
|
170
88
|
matched_name = cls._extract_title_from_page(best_match)
|
171
|
-
|
172
|
-
logger.info(
|
89
|
+
cls.logger.info(
|
173
90
|
"Found matching page: '%s' (ID: %s) with score: %.2f",
|
174
91
|
matched_name,
|
175
92
|
page_id,
|
176
93
|
best_score,
|
177
94
|
)
|
178
95
|
|
179
|
-
page = NotionPage(page_id=page_id,
|
96
|
+
page = NotionPage.from_page_id(page_id=page_id, token=token)
|
97
|
+
cls.logger.info("Successfully created page instance for '%s'", matched_name)
|
180
98
|
|
181
|
-
logger.info("Successfully created page instance for '%s'", matched_name)
|
182
99
|
await client.close()
|
183
100
|
return page
|
184
101
|
|
185
102
|
except Exception as e:
|
186
|
-
|
187
|
-
|
103
|
+
cls.logger.error("Error finding page by name: %s", str(e))
|
104
|
+
await client.close()
|
105
|
+
raise
|
188
106
|
|
189
107
|
@classmethod
|
190
|
-
def
|
191
|
-
"""
|
192
|
-
|
108
|
+
async def _search_pages(cls, client: NotionClient) -> List[Dict[str, Any]]:
|
109
|
+
"""Search for pages using the Notion API."""
|
110
|
+
cls.logger.debug("Using search endpoint to find pages")
|
111
|
+
|
112
|
+
search_payload = {
|
113
|
+
"filter": {"property": "object", "value": "page"},
|
114
|
+
"page_size": 100,
|
115
|
+
}
|
116
|
+
|
117
|
+
response = await client.post("search", search_payload)
|
118
|
+
|
119
|
+
if not response or "results" not in response:
|
120
|
+
cls.logger.error("Failed to fetch pages using search endpoint")
|
121
|
+
raise ValueError("Failed to fetch pages using search endpoint")
|
122
|
+
|
123
|
+
return response.get("results", [])
|
124
|
+
|
125
|
+
@classmethod
|
126
|
+
def _find_best_match(
|
127
|
+
cls, pages: List[Dict[str, Any]], query: str
|
128
|
+
) -> Tuple[Optional[Dict[str, Any]], float, List[str]]:
|
129
|
+
"""Find the best matching page for the given query."""
|
130
|
+
cls.logger.debug("Found %d pages, searching for best match", len(pages))
|
131
|
+
|
132
|
+
matches = []
|
133
|
+
best_match = None
|
134
|
+
best_score = 0
|
135
|
+
|
136
|
+
for page in pages:
|
137
|
+
title = cls._extract_title_from_page(page)
|
138
|
+
score = SequenceMatcher(None, query.lower(), title.lower()).ratio()
|
139
|
+
matches.append((page, title, score))
|
140
|
+
|
141
|
+
if score > best_score:
|
142
|
+
best_score = score
|
143
|
+
best_match = page
|
193
144
|
|
194
|
-
|
195
|
-
|
145
|
+
# Get top suggestions
|
146
|
+
matches.sort(key=lambda x: x[2], reverse=True)
|
147
|
+
suggestions = [title for _, title, _ in matches[: cls.MAX_SUGGESTIONS]]
|
196
148
|
|
197
|
-
|
198
|
-
|
149
|
+
return best_match, best_score, suggestions
|
150
|
+
|
151
|
+
@classmethod
|
152
|
+
def _format_suggestions(cls, suggestions: List[str]) -> str:
|
153
|
+
"""Format suggestions as a readable string."""
|
154
|
+
if not suggestions:
|
155
|
+
return ""
|
199
156
|
|
200
|
-
|
201
|
-
|
202
|
-
|
157
|
+
msg = "Did you mean one of these?\n"
|
158
|
+
msg += "\n".join(f"- {suggestion}" for suggestion in suggestions)
|
159
|
+
return msg
|
160
|
+
|
161
|
+
@classmethod
|
162
|
+
def _extract_title_from_page(cls, page: Dict[str, Any]) -> str:
|
163
|
+
"""Extract the title from a page object."""
|
203
164
|
try:
|
204
165
|
if "properties" in page:
|
205
166
|
for prop_value in page["properties"].values():
|
206
167
|
if prop_value.get("type") != "title":
|
207
168
|
continue
|
208
169
|
title_array = prop_value.get("title", [])
|
209
|
-
if
|
210
|
-
|
211
|
-
return cls._extract_text_from_rich_text(title_array)
|
170
|
+
if title_array:
|
171
|
+
return cls._extract_text_from_rich_text(title_array)
|
212
172
|
|
173
|
+
# Fall back to child_page
|
213
174
|
if "child_page" in page:
|
214
175
|
return page.get("child_page", {}).get("title", "Untitled")
|
215
176
|
|
216
177
|
return "Untitled"
|
217
178
|
|
218
179
|
except Exception as e:
|
219
|
-
|
220
|
-
cls.class_logger().warning(error_msg)
|
180
|
+
cls.logger.warning("Error extracting page title: %s", str(e))
|
221
181
|
return "Untitled"
|
222
182
|
|
223
183
|
@classmethod
|
224
184
|
def _extract_text_from_rich_text(cls, rich_text: List[Dict[str, Any]]) -> str:
|
225
|
-
"""
|
226
|
-
Extract plain text from a rich text array.
|
227
|
-
|
228
|
-
Args:
|
229
|
-
rich_text: A list of rich text objects from the Notion API
|
230
|
-
|
231
|
-
Returns:
|
232
|
-
The combined plain text content
|
233
|
-
"""
|
185
|
+
"""Extract plain text from a rich text array."""
|
234
186
|
if not rich_text:
|
235
187
|
return ""
|
236
188
|
|
237
|
-
text_parts = [
|
238
|
-
|
239
|
-
|
240
|
-
text_parts.append(text_obj["plain_text"])
|
189
|
+
text_parts = [
|
190
|
+
text_obj["plain_text"] for text_obj in rich_text if "plain_text" in text_obj
|
191
|
+
]
|
241
192
|
|
242
193
|
return "".join(text_parts)
|
@@ -1,8 +1,8 @@
|
|
1
1
|
from typing import Dict, Any, List, Optional
|
2
2
|
|
3
|
-
from notionary.elements.registry.
|
4
|
-
from notionary.elements.registry.
|
5
|
-
|
3
|
+
from notionary.elements.registry.block_registry import BlockRegistry
|
4
|
+
from notionary.elements.registry.block_registry_builder import (
|
5
|
+
BlockRegistryBuilder,
|
6
6
|
)
|
7
7
|
|
8
8
|
|
@@ -12,12 +12,12 @@ class NotionToMarkdownConverter:
|
|
12
12
|
TOGGLE_ELEMENT_TYPES = ["toggle", "toggleable_heading"]
|
13
13
|
LIST_ITEM_TYPES = ["numbered_list_item", "bulleted_list_item"]
|
14
14
|
|
15
|
-
def __init__(self, block_registry: Optional[
|
15
|
+
def __init__(self, block_registry: Optional[BlockRegistry] = None):
|
16
16
|
"""
|
17
17
|
Initialize the NotionToMarkdownConverter.
|
18
18
|
"""
|
19
19
|
self._block_registry = (
|
20
|
-
block_registry or
|
20
|
+
block_registry or BlockRegistryBuilder().create_full_registry()
|
21
21
|
)
|
22
22
|
|
23
23
|
def convert(self, blocks: List[Dict[str, Any]]) -> str:
|
@@ -21,7 +21,7 @@ class DatabasePropertyService(LoggingMixin):
|
|
21
21
|
self._client = client
|
22
22
|
self._schema = None
|
23
23
|
|
24
|
-
async def load_schema(self, force_refresh=False) -> bool:
|
24
|
+
async def load_schema(self, force_refresh: bool = False) -> bool:
|
25
25
|
"""
|
26
26
|
Loads the database schema.
|
27
27
|
|
@@ -29,24 +29,22 @@ class DatabasePropertyService(LoggingMixin):
|
|
29
29
|
force_refresh: Whether to force a refresh of the schema
|
30
30
|
|
31
31
|
Returns:
|
32
|
-
|
32
|
+
True if schema loaded successfully, False otherwise.
|
33
33
|
"""
|
34
34
|
if self._schema is not None and not force_refresh:
|
35
35
|
return True
|
36
36
|
|
37
37
|
try:
|
38
|
-
database = await self._client.
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
self.logger.error(
|
45
|
-
"Failed to load schema: missing 'properties' in response"
|
46
|
-
)
|
47
|
-
return False
|
38
|
+
database = await self._client.get_database(self._database_id)
|
39
|
+
|
40
|
+
self._schema = database.properties
|
41
|
+
self.logger.debug("Loaded schema for database %s", self._database_id)
|
42
|
+
return True
|
43
|
+
|
48
44
|
except Exception as e:
|
49
|
-
self.logger.error(
|
45
|
+
self.logger.error(
|
46
|
+
"Error loading database schema for %s: %s", self._database_id, str(e)
|
47
|
+
)
|
50
48
|
return False
|
51
49
|
|
52
50
|
async def _ensure_schema_loaded(self) -> None:
|
@@ -302,45 +300,3 @@ class DatabasePropertyService(LoggingMixin):
|
|
302
300
|
)
|
303
301
|
|
304
302
|
return True, None, None
|
305
|
-
|
306
|
-
async def get_database_metadata(
|
307
|
-
self, include_types: Optional[List[str]] = None
|
308
|
-
) -> Dict[str, Any]:
|
309
|
-
"""
|
310
|
-
Gets the complete metadata of the database, including property options.
|
311
|
-
|
312
|
-
Args:
|
313
|
-
include_types: List of property types to include (if None, include all)
|
314
|
-
|
315
|
-
Returns:
|
316
|
-
Dict[str, Any]: The database metadata
|
317
|
-
"""
|
318
|
-
await self._ensure_schema_loaded()
|
319
|
-
|
320
|
-
if not self._schema:
|
321
|
-
return {"properties": {}}
|
322
|
-
|
323
|
-
metadata = {"properties": {}}
|
324
|
-
|
325
|
-
for prop_name, prop_data in self._schema.items():
|
326
|
-
prop_type = prop_data.get("type")
|
327
|
-
|
328
|
-
# Skip if we're filtering and this type isn't included
|
329
|
-
if include_types and prop_type not in include_types:
|
330
|
-
continue
|
331
|
-
|
332
|
-
prop_metadata = {"type": prop_type, "options": []}
|
333
|
-
|
334
|
-
# Include options for select, multi_select, status
|
335
|
-
if prop_type in ["select", "multi_select", "status"]:
|
336
|
-
prop_metadata["options"] = prop_data.get(prop_type, {}).get(
|
337
|
-
"options", []
|
338
|
-
)
|
339
|
-
|
340
|
-
# For relation properties, we might want to include related database info
|
341
|
-
elif prop_type == "relation":
|
342
|
-
prop_metadata["relation_details"] = prop_data.get("relation", {})
|
343
|
-
|
344
|
-
metadata["properties"][prop_name] = prop_metadata
|
345
|
-
|
346
|
-
return metadata
|
@@ -1,12 +1,7 @@
|
|
1
1
|
from typing import Dict, Any, List, Optional
|
2
|
+
from notionary.models.notion_page_response import NotionPageResponse
|
2
3
|
from notionary.notion_client import NotionClient
|
3
4
|
from notionary.page.metadata.metadata_editor import MetadataEditor
|
4
|
-
from notionary.page.properites.property_operation_result import (
|
5
|
-
PropertyOperationResult,
|
6
|
-
)
|
7
|
-
from notionary.page.relations.notion_page_title_resolver import (
|
8
|
-
NotionPageTitleResolver,
|
9
|
-
)
|
10
5
|
from notionary.page.properites.database_property_service import (
|
11
6
|
DatabasePropertyService,
|
12
7
|
)
|
@@ -34,15 +29,7 @@ class PagePropertyManager(LoggingMixin):
|
|
34
29
|
self._db_relation = db_relation
|
35
30
|
self._db_property_service = None
|
36
31
|
|
37
|
-
self._extractor = PropertyValueExtractor(
|
38
|
-
self._title_resolver = NotionPageTitleResolver(client)
|
39
|
-
|
40
|
-
async def get_properties(self) -> Dict[str, Any]:
|
41
|
-
"""Retrieves all properties of the page."""
|
42
|
-
page_data = await self._get_page_data()
|
43
|
-
if page_data and "properties" in page_data:
|
44
|
-
return page_data["properties"]
|
45
|
-
return {}
|
32
|
+
self._extractor = PropertyValueExtractor()
|
46
33
|
|
47
34
|
async def get_property_value(self, property_name: str, relation_getter=None) -> Any:
|
48
35
|
"""
|
@@ -52,7 +39,7 @@ class PagePropertyManager(LoggingMixin):
|
|
52
39
|
property_name: Name of the property to get
|
53
40
|
relation_getter: Optional callback function to get relation values
|
54
41
|
"""
|
55
|
-
properties = await self.
|
42
|
+
properties = await self._get_properties()
|
56
43
|
if property_name not in properties:
|
57
44
|
return None
|
58
45
|
|
@@ -61,7 +48,7 @@ class PagePropertyManager(LoggingMixin):
|
|
61
48
|
|
62
49
|
async def set_property_by_name(
|
63
50
|
self, property_name: str, value: Any
|
64
|
-
) ->
|
51
|
+
) -> Optional[Any]:
|
65
52
|
"""
|
66
53
|
Set a property value by name, automatically detecting the property type.
|
67
54
|
|
@@ -70,72 +57,57 @@ class PagePropertyManager(LoggingMixin):
|
|
70
57
|
value: Value to set
|
71
58
|
|
72
59
|
Returns:
|
73
|
-
|
74
|
-
and available options if applicable
|
60
|
+
Optional[Any]: The new value if successful, None if failed
|
75
61
|
"""
|
76
62
|
property_type = await self.get_property_type(property_name)
|
77
63
|
|
78
64
|
if property_type == "relation":
|
79
|
-
|
80
|
-
|
65
|
+
self.logger.warning(
|
66
|
+
"Property '%s' is of type 'relation'. Relations must be set using the RelationManager.",
|
67
|
+
property_name,
|
81
68
|
)
|
82
|
-
|
83
|
-
return result
|
69
|
+
return None
|
84
70
|
|
85
|
-
|
86
|
-
|
87
|
-
property_name, value
|
88
|
-
)
|
89
|
-
if api_response:
|
90
|
-
await self.invalidate_cache()
|
91
|
-
return PropertyOperationResult.from_success(
|
92
|
-
property_name, value, api_response
|
93
|
-
)
|
94
|
-
return PropertyOperationResult.from_no_api_response(property_name, value)
|
71
|
+
is_db_page = await self._db_relation.is_database_page()
|
72
|
+
db_service = None
|
95
73
|
|
96
|
-
|
74
|
+
if is_db_page:
|
75
|
+
db_service = await self._init_db_property_service()
|
97
76
|
|
98
|
-
if
|
99
|
-
|
100
|
-
property_name, value
|
77
|
+
if db_service:
|
78
|
+
is_valid, error_message, available_options = (
|
79
|
+
await db_service.validate_property_value(property_name, value)
|
101
80
|
)
|
102
|
-
if api_response:
|
103
|
-
await self.invalidate_cache()
|
104
|
-
return PropertyOperationResult.from_success(
|
105
|
-
property_name, value, api_response
|
106
|
-
)
|
107
|
-
return PropertyOperationResult.from_no_api_response(property_name, value)
|
108
|
-
|
109
|
-
is_valid, error_message, available_options = (
|
110
|
-
await db_service.validate_property_value(property_name, value)
|
111
|
-
)
|
112
81
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
82
|
+
if not is_valid:
|
83
|
+
if available_options:
|
84
|
+
options_str = "', '".join(available_options)
|
85
|
+
self.logger.warning(
|
86
|
+
"%s\nAvailable options for '%s': '%s'",
|
87
|
+
error_message,
|
88
|
+
property_name,
|
89
|
+
options_str,
|
90
|
+
)
|
91
|
+
else:
|
92
|
+
self.logger.warning(
|
93
|
+
"%s\nNo valid options available for '%s'",
|
94
|
+
error_message,
|
95
|
+
property_name,
|
96
|
+
)
|
97
|
+
return None
|
128
98
|
|
129
99
|
api_response = await self._metadata_editor.set_property_by_name(
|
130
100
|
property_name, value
|
131
101
|
)
|
102
|
+
|
132
103
|
if api_response:
|
133
104
|
await self.invalidate_cache()
|
134
|
-
return
|
135
|
-
property_name, value, api_response
|
136
|
-
)
|
105
|
+
return value
|
137
106
|
|
138
|
-
|
107
|
+
self.logger.warning(
|
108
|
+
"Failed to set property '%s' (no API response)", property_name
|
109
|
+
)
|
110
|
+
return None
|
139
111
|
|
140
112
|
async def get_property_type(self, property_name: str) -> Optional[str]:
|
141
113
|
"""Gets the type of a specific property."""
|
@@ -151,7 +123,7 @@ class PagePropertyManager(LoggingMixin):
|
|
151
123
|
return await db_service.get_option_names(property_name)
|
152
124
|
return []
|
153
125
|
|
154
|
-
async def _get_page_data(self, force_refresh=False) ->
|
126
|
+
async def _get_page_data(self, force_refresh=False) -> NotionPageResponse:
|
155
127
|
"""Gets the page data and caches it for future use."""
|
156
128
|
if self._page_data is None or force_refresh:
|
157
129
|
self._page_data = await self._client.get_page(self._page_id)
|
@@ -173,3 +145,8 @@ class PagePropertyManager(LoggingMixin):
|
|
173
145
|
self._db_property_service = DatabasePropertyService(database_id, self._client)
|
174
146
|
await self._db_property_service.load_schema()
|
175
147
|
return self._db_property_service
|
148
|
+
|
149
|
+
async def _get_properties(self) -> Dict[str, Any]:
|
150
|
+
"""Retrieves all properties of the page."""
|
151
|
+
page_data = await self._get_page_data()
|
152
|
+
return page_data.properties if page_data.properties else {}
|
@@ -1,10 +1,10 @@
|
|
1
1
|
import asyncio
|
2
2
|
from typing import Any, Awaitable, Callable
|
3
3
|
|
4
|
+
from notionary.util.logging_mixin import LoggingMixin
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
self.logger = logger
|
6
|
+
|
7
|
+
class PropertyValueExtractor(LoggingMixin):
|
8
8
|
|
9
9
|
async def extract(
|
10
10
|
self,
|