notionary 0.1.29__py3-none-any.whl → 0.2.0__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 (59) hide show
  1. notionary/__init__.py +5 -5
  2. notionary/database/notion_database.py +50 -59
  3. notionary/database/notion_database_factory.py +16 -20
  4. notionary/elements/audio_element.py +1 -1
  5. notionary/elements/bookmark_element.py +1 -1
  6. notionary/elements/bulleted_list_element.py +2 -8
  7. notionary/elements/callout_element.py +1 -1
  8. notionary/elements/code_block_element.py +1 -1
  9. notionary/elements/divider_element.py +1 -1
  10. notionary/elements/embed_element.py +1 -1
  11. notionary/elements/heading_element.py +2 -8
  12. notionary/elements/image_element.py +1 -1
  13. notionary/elements/mention_element.py +1 -1
  14. notionary/elements/notion_block_element.py +1 -1
  15. notionary/elements/numbered_list_element.py +2 -7
  16. notionary/elements/paragraph_element.py +1 -1
  17. notionary/elements/qoute_element.py +1 -1
  18. notionary/elements/registry/{block_element_registry.py → block_registry.py} +70 -26
  19. notionary/elements/registry/{block_element_registry_builder.py → block_registry_builder.py} +48 -32
  20. notionary/elements/table_element.py +1 -1
  21. notionary/elements/text_inline_formatter.py +13 -9
  22. notionary/elements/todo_element.py +1 -1
  23. notionary/elements/toggle_element.py +1 -1
  24. notionary/elements/toggleable_heading_element.py +1 -1
  25. notionary/elements/video_element.py +1 -1
  26. notionary/models/notion_block_response.py +264 -0
  27. notionary/models/notion_database_response.py +63 -0
  28. notionary/models/notion_page_response.py +100 -0
  29. notionary/notion_client.py +38 -5
  30. notionary/page/content/page_content_retriever.py +68 -0
  31. notionary/page/content/page_content_writer.py +103 -0
  32. notionary/page/markdown_to_notion_converter.py +5 -5
  33. notionary/page/metadata/metadata_editor.py +91 -63
  34. notionary/page/metadata/notion_icon_manager.py +55 -28
  35. notionary/page/metadata/notion_page_cover_manager.py +23 -20
  36. notionary/page/notion_page.py +223 -218
  37. notionary/page/notion_page_factory.py +102 -151
  38. notionary/page/notion_to_markdown_converter.py +5 -5
  39. notionary/page/properites/database_property_service.py +11 -55
  40. notionary/page/properites/page_property_manager.py +44 -67
  41. notionary/page/properites/property_value_extractor.py +3 -3
  42. notionary/page/relations/notion_page_relation_manager.py +165 -213
  43. notionary/page/relations/notion_page_title_resolver.py +59 -41
  44. notionary/page/relations/page_database_relation.py +7 -9
  45. notionary/{elements/prompts → prompting}/element_prompt_content.py +19 -4
  46. notionary/prompting/markdown_syntax_prompt_generator.py +92 -0
  47. notionary/util/logging_mixin.py +17 -8
  48. notionary/util/warn_direct_constructor_usage.py +54 -0
  49. {notionary-0.1.29.dist-info → notionary-0.2.0.dist-info}/METADATA +2 -1
  50. notionary-0.2.0.dist-info/RECORD +60 -0
  51. {notionary-0.1.29.dist-info → notionary-0.2.0.dist-info}/WHEEL +1 -1
  52. notionary/database/database_info_service.py +0 -43
  53. notionary/elements/prompts/synthax_prompt_builder.py +0 -150
  54. notionary/page/content/page_content_manager.py +0 -211
  55. notionary/page/properites/property_operation_result.py +0 -116
  56. notionary/page/relations/relation_operation_result.py +0 -144
  57. notionary-0.1.29.dist-info/RECORD +0 -58
  58. {notionary-0.1.29.dist-info → notionary-0.2.0.dist-info}/licenses/LICENSE +0 -0
  59. {notionary-0.1.29.dist-info → notionary-0.2.0.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,4 @@
1
- import logging
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
- @classmethod
17
- def class_logger(cls):
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
- async def from_page_id(
23
- cls, page_id: str, token: Optional[str] = None
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
- logger.info("Successfully created page instance for ID: %s", formatted_id)
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
- error_msg = f"Error connecting to page {page_id}: {str(e)}"
50
- logger.error(error_msg)
29
+ cls.logger.error("Error connecting to page %s: %s", page_id, str(e))
30
+ raise
51
31
 
52
32
  @classmethod
53
- async def from_url(cls, url: str, token: Optional[str] = None) -> NotionPage:
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
- error_msg = f"Could not extract valid page ID from URL: {url}"
73
- logger.error(error_msg)
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
- error_msg = f"Error connecting to page with URL {url}: {str(e)}"
84
- logger.error(error_msg)
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
- Create a NotionPage by finding a page with a matching name.
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
- logger.debug("Using search endpoint to find pages")
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
- error_msg = f"No pages found matching '{page_name}'"
129
- logger.warning(error_msg)
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
- if score > best_score:
145
- best_score = score
146
- best_match = page
67
+ # Find best match
68
+ best_match, best_score, suggestions = cls._find_best_match(pages, page_name)
147
69
 
148
- if best_score < 0.6 or not best_match:
149
- # Sort matches by score in descending order
150
- matches.sort(key=lambda x: x[2], reverse=True)
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
- error_msg = "Best match page has no ID"
168
- logger.error(error_msg)
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, title=matched_name, token=token)
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
- error_msg = f"Error finding page by name: {str(e)}"
187
- logger.error(error_msg)
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 _extract_title_from_page(cls, page: Dict[str, Any]) -> str:
191
- """
192
- Extract the title from a page object.
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
- Args:
195
- page: The page object returned from the Notion API
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
- Returns:
198
- The title of the page
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
- Raises:
201
- NotionError: If the title cannot be extracted
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 not title_array:
210
- continue
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
- error_msg = f"Error extracting page title: {str(e)}"
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
- for text_obj in rich_text:
239
- if "plain_text" in text_obj:
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.block_element_registry import BlockElementRegistry
4
- from notionary.elements.registry.block_element_registry_builder import (
5
- BlockElementRegistryBuilder,
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[BlockElementRegistry] = None):
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 BlockElementRegistryBuilder().create_full_registry()
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
- bool: True if schema loaded successfully, False otherwise
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.get(f"databases/{self._database_id}")
39
- if database and "properties" in database:
40
- self._schema = database["properties"]
41
- self.logger.debug("Loaded schema for database %s", self._database_id)
42
- return True
43
- else:
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("Error loading database schema: %s", str(e))
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(self.logger)
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.get_properties()
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
- ) -> PropertyOperationResult:
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
- PropertyOperationResult: Result of the operation with status, error messages,
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
- result = PropertyOperationResult.from_relation_type_error(
80
- property_name, value
65
+ self.logger.warning(
66
+ "Property '%s' is of type 'relation'. Relations must be set using the RelationManager.",
67
+ property_name,
81
68
  )
82
- self.logger.warning(result.error)
83
- return result
69
+ return None
84
70
 
85
- if not await self._db_relation.is_database_page():
86
- api_response = await self._metadata_editor.set_property_by_name(
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
- db_service = await self._init_db_property_service()
74
+ if is_db_page:
75
+ db_service = await self._init_db_property_service()
97
76
 
98
- if not db_service:
99
- api_response = await self._metadata_editor.set_property_by_name(
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
- if not is_valid:
114
- if available_options:
115
- options_str = "', '".join(available_options)
116
- detailed_error = f"{error_message}\nAvailable options for '{property_name}': '{options_str}'"
117
- self.logger.warning(detailed_error)
118
- else:
119
- self.logger.warning(
120
- "%s\nNo valid options available for '%s'",
121
- error_message,
122
- property_name,
123
- )
124
-
125
- return PropertyOperationResult.from_error(
126
- property_name, error_message, value, available_options
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 PropertyOperationResult.from_success(
135
- property_name, value, api_response
136
- )
105
+ return value
137
106
 
138
- return PropertyOperationResult.from_no_api_response(property_name, value)
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) -> Dict[str, Any]:
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
- class PropertyValueExtractor:
6
- def __init__(self, logger=None):
7
- self.logger = logger
6
+
7
+ class PropertyValueExtractor(LoggingMixin):
8
8
 
9
9
  async def extract(
10
10
  self,