notionary 0.1.10__py3-none-any.whl → 0.1.12__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 (33) hide show
  1. notionary/__init__.py +13 -2
  2. notionary/core/converters/elements/audio_element.py +143 -0
  3. notionary/core/converters/elements/embed_element.py +2 -4
  4. notionary/core/converters/elements/toggle_element.py +28 -20
  5. notionary/core/converters/markdown_to_notion_converter.py +70 -109
  6. notionary/core/converters/registry/block_element_registry.py +2 -6
  7. notionary/core/converters/registry/block_element_registry_builder.py +2 -0
  8. notionary/core/database/database_discovery.py +140 -0
  9. notionary/core/database/notion_database_manager.py +26 -49
  10. notionary/core/database/notion_database_manager_factory.py +10 -4
  11. notionary/core/notion_client.py +4 -2
  12. notionary/core/page/content/notion_page_content_chunker.py +84 -0
  13. notionary/core/page/content/page_content_manager.py +26 -8
  14. notionary/core/page/metadata/metadata_editor.py +57 -44
  15. notionary/core/page/metadata/notion_icon_manager.py +9 -11
  16. notionary/core/page/metadata/notion_page_cover_manager.py +15 -20
  17. notionary/core/page/notion_page_manager.py +137 -156
  18. notionary/core/page/properites/database_property_service.py +114 -98
  19. notionary/core/page/properites/page_property_manager.py +78 -49
  20. notionary/core/page/properites/property_formatter.py +1 -1
  21. notionary/core/page/properites/property_operation_result.py +43 -30
  22. notionary/core/page/properites/property_value_extractor.py +26 -8
  23. notionary/core/page/relations/notion_page_relation_manager.py +71 -52
  24. notionary/core/page/relations/notion_page_title_resolver.py +11 -11
  25. notionary/core/page/relations/page_database_relation.py +14 -14
  26. notionary/core/page/relations/relation_operation_result.py +50 -41
  27. notionary/util/page_id_utils.py +11 -7
  28. {notionary-0.1.10.dist-info → notionary-0.1.12.dist-info}/METADATA +1 -1
  29. {notionary-0.1.10.dist-info → notionary-0.1.12.dist-info}/RECORD +32 -30
  30. notionary/core/database/notion_database_schema.py +0 -104
  31. {notionary-0.1.10.dist-info → notionary-0.1.12.dist-info}/WHEEL +0 -0
  32. {notionary-0.1.10.dist-info → notionary-0.1.12.dist-info}/licenses/LICENSE +0 -0
  33. {notionary-0.1.10.dist-info → notionary-0.1.12.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,140 @@
1
+ from typing import (
2
+ AsyncGenerator,
3
+ Dict,
4
+ List,
5
+ Optional,
6
+ Any,
7
+ Tuple,
8
+ )
9
+ from notionary.core.notion_client import NotionClient
10
+ from notionary.util.logging_mixin import LoggingMixin
11
+
12
+
13
+ class DatabaseDiscovery(LoggingMixin):
14
+ """
15
+ A utility class that discovers Notion databases accessible to your integration.
16
+ Focused on efficiently retrieving essential database information.
17
+ """
18
+
19
+ def __init__(self, client: Optional[NotionClient] = None) -> None:
20
+ """
21
+ Initialize the database discovery with a NotionClient.
22
+
23
+ Args:
24
+ client: NotionClient instance for API communication
25
+ """
26
+ self._client = client if client else NotionClient()
27
+ self.logger.info("DatabaseDiscovery initialized")
28
+
29
+ async def discover(self, page_size: int = 100) -> List[Tuple[str, str]]:
30
+ """
31
+ Discover all accessible databases and return their titles and IDs.
32
+
33
+ Args:
34
+ page_size: The number of databases to fetch per request
35
+
36
+ Returns:
37
+ List of tuples containing (database_title, database_id)
38
+ """
39
+ databases = []
40
+
41
+ async for database in self._iter_databases(page_size):
42
+ db_id = database.get("id")
43
+ if not db_id:
44
+ continue
45
+
46
+ title = self._extract_database_title(database)
47
+ databases.append((title, db_id))
48
+
49
+ return databases
50
+
51
+ async def discover_and_print(self, page_size: int = 100) -> List[Tuple[str, str]]:
52
+ """
53
+ Discover databases and print the results in a nicely formatted way.
54
+
55
+ This is a convenience method that discovers databases and handles
56
+ the formatting and printing of results.
57
+
58
+ Args:
59
+ page_size: The number of databases to fetch per request
60
+
61
+ Returns:
62
+ The same list of databases as discover() for further processing
63
+ """
64
+ databases = await self.discover(page_size)
65
+
66
+ if not databases:
67
+ print("\n⚠️ No databases found!")
68
+ print("Please ensure your Notion integration has access to databases.")
69
+ print("You need to share the databases with your integration in Notion settings.")
70
+ return databases
71
+
72
+ print(f"✅ Found {len(databases)} databases:")
73
+
74
+ for i, (title, db_id) in enumerate(databases, 1):
75
+ print(f"{i}. {title} (ID: {db_id})")
76
+
77
+ return databases
78
+
79
+ async def _iter_databases(
80
+ self, page_size: int = 100
81
+ ) -> AsyncGenerator[Dict[str, Any], None]:
82
+ """
83
+ Asynchronous generator that yields Notion databases one by one.
84
+
85
+ Uses the Notion API to provide paginated access to all databases
86
+ without loading all of them into memory at once.
87
+
88
+ Args:
89
+ page_size: The number of databases to fetch per request
90
+
91
+ Yields:
92
+ Individual database objects from the Notion API
93
+ """
94
+ start_cursor: Optional[str] = None
95
+
96
+ while True:
97
+ body: Dict[str, Any] = {
98
+ "filter": {"value": "database", "property": "object"},
99
+ "page_size": page_size,
100
+ }
101
+
102
+ if start_cursor:
103
+ body["start_cursor"] = start_cursor
104
+
105
+ result = await self._client.post("search", data=body)
106
+
107
+ if not result or "results" not in result:
108
+ self.logger.error("Error fetching databases")
109
+ return
110
+
111
+ for database in result["results"]:
112
+ yield database
113
+
114
+ if not result.get("has_more") or not result.get("next_cursor"):
115
+ return
116
+
117
+ start_cursor = result["next_cursor"]
118
+
119
+ def _extract_database_title(self, database: Dict[str, Any]) -> str:
120
+ """
121
+ Extract the database title from a Notion API response.
122
+
123
+ Args:
124
+ database: The database object from the Notion API
125
+
126
+ Returns:
127
+ The extracted title or "Untitled" if no title is found
128
+ """
129
+ if "title" not in database:
130
+ return "Untitled"
131
+
132
+ title_parts = []
133
+ for text_obj in database["title"]:
134
+ if "plain_text" in text_obj:
135
+ title_parts.append(text_obj["plain_text"])
136
+
137
+ if not title_parts:
138
+ return "Untitled"
139
+
140
+ return "".join(title_parts)
@@ -9,7 +9,7 @@ from notionary.util.page_id_utils import format_uuid
9
9
  class NotionDatabaseManager(LoggingMixin):
10
10
  """
11
11
  Minimal manager for Notion databases.
12
- Focused exclusively on creating basic pages and retrieving page managers
12
+ Focused exclusively on creating basic pages and retrieving page managers
13
13
  for further page operations.
14
14
  """
15
15
 
@@ -24,61 +24,33 @@ class NotionDatabaseManager(LoggingMixin):
24
24
  self.database_id = format_uuid(database_id) or database_id
25
25
  self._client = NotionClient(token=token)
26
26
 
27
-
28
- async def create_blank_page(self) -> Optional[str]:
27
+ async def create_blank_page(self) -> Optional[NotionPageManager]:
29
28
  """
30
29
  Create a new blank page in the database with minimal properties.
31
-
30
+
32
31
  Returns:
33
- Optional[str]: The ID of the created page, or None if creation failed
32
+ NotionPageManager for the created page, or None if creation failed
34
33
  """
35
34
  try:
36
35
  response = await self._client.post(
37
- "pages",
38
- {
39
- "parent": {"database_id": self.database_id},
40
- "properties": {}
41
- }
36
+ "pages", {"parent": {"database_id": self.database_id}, "properties": {}}
42
37
  )
43
-
38
+
44
39
  if response and "id" in response:
45
40
  page_id = response["id"]
46
- self.logger.info("Created blank page %s in database %s", page_id, self.database_id)
47
- return page_id
48
-
41
+ self.logger.info(
42
+ "Created blank page %s in database %s", page_id, self.database_id
43
+ )
44
+
45
+ return NotionPageManager(page_id=page_id)
46
+
49
47
  self.logger.warning("Page creation failed: invalid response")
50
48
  return None
51
-
49
+
52
50
  except Exception as e:
53
51
  self.logger.error("Error creating blank page: %s", str(e))
54
52
  return None
55
-
56
- async def get_page_manager(self, page_id: str) -> Optional[NotionPageManager]:
57
- """
58
- Get a NotionPageManager for a specific page.
59
-
60
- Args:
61
- page_id: The ID of the page
62
-
63
- Returns:
64
- NotionPageManager instance or None if the page wasn't found
65
- """
66
- self.logger.debug("Getting page manager for page %s", page_id)
67
53
 
68
- try:
69
- # Check if the page exists
70
- page_data = await self._client.get_page(page_id)
71
-
72
- if not page_data:
73
- self.logger.error("Page %s not found", page_id)
74
- return None
75
-
76
- return NotionPageManager(page_id=page_id)
77
-
78
- except Exception as e:
79
- self.logger.error("Error getting page manager: %s", str(e))
80
- return None
81
-
82
54
  async def get_pages(
83
55
  self,
84
56
  limit: int = 100,
@@ -174,10 +146,12 @@ class NotionDatabaseManager(LoggingMixin):
174
146
  for page in result["results"]:
175
147
  page_id: str = page.get("id", "")
176
148
  title = self._extract_page_title(page)
177
-
149
+
178
150
  page_url = f"https://notion.so/{page_id.replace('-', '')}"
179
151
 
180
- notion_page_manager = NotionPageManager(page_id=page_id, title=title, url=page_url)
152
+ notion_page_manager = NotionPageManager(
153
+ page_id=page_id, title=title, url=page_url
154
+ )
181
155
  yield notion_page_manager
182
156
 
183
157
  # Update pagination parameters
@@ -222,10 +196,10 @@ class NotionDatabaseManager(LoggingMixin):
222
196
  """
223
197
  try:
224
198
  formatted_page_id = format_uuid(page_id) or page_id
225
-
199
+
226
200
  # Archive the page (Notion's way of deleting)
227
201
  data = {"archived": True}
228
-
202
+
229
203
  result = await self._client.patch(f"pages/{formatted_page_id}", data)
230
204
  if not result:
231
205
  self.logger.error("Error deleting page %s", formatted_page_id)
@@ -233,14 +207,17 @@ class NotionDatabaseManager(LoggingMixin):
233
207
  "success": False,
234
208
  "message": f"Failed to delete page {formatted_page_id}",
235
209
  }
236
-
237
- self.logger.info("Page %s successfully deleted (archived)", formatted_page_id)
210
+
211
+ self.logger.info(
212
+ "Page %s successfully deleted (archived)", formatted_page_id
213
+ )
238
214
  return {"success": True, "page_id": formatted_page_id}
239
-
215
+
240
216
  except Exception as e:
241
217
  self.logger.error("Error in delete_page: %s", str(e))
242
218
  return {"success": False, "message": f"Error: {str(e)}"}
243
219
 
244
220
  async def close(self) -> None:
245
221
  """Close the client connection."""
246
- await self._client.close()
222
+ await self._client.close()
223
+
@@ -44,11 +44,12 @@ class NotionDatabaseFactory(LoggingMixin):
44
44
 
45
45
  try:
46
46
  formatted_id = format_uuid(database_id) or database_id
47
-
47
+
48
48
  manager = NotionDatabaseManager(formatted_id, token)
49
-
50
49
 
51
- logger.info("Successfully created database manager for ID: %s", formatted_id)
50
+ logger.info(
51
+ "Successfully created database manager for ID: %s", formatted_id
52
+ )
52
53
  return manager
53
54
 
54
55
  except DatabaseInitializationError:
@@ -136,7 +137,12 @@ class NotionDatabaseFactory(LoggingMixin):
136
137
 
137
138
  matched_name = cls._extract_title_from_database(best_match)
138
139
 
139
- logger.info("Found matching database: '%s' (ID: %s) with score: %.2f", matched_name, database_id, best_score)
140
+ logger.info(
141
+ "Found matching database: '%s' (ID: %s) with score: %.2f",
142
+ matched_name,
143
+ database_id,
144
+ best_score,
145
+ )
140
146
 
141
147
  manager = NotionDatabaseManager(database_id, token)
142
148
 
@@ -7,6 +7,7 @@ import httpx
7
7
  from dotenv import load_dotenv
8
8
  from notionary.util.logging_mixin import LoggingMixin
9
9
 
10
+
10
11
  class HttpMethod(Enum):
11
12
  """Enum für HTTP-Methoden."""
12
13
 
@@ -15,6 +16,7 @@ class HttpMethod(Enum):
15
16
  PATCH = "patch"
16
17
  DELETE = "delete"
17
18
 
19
+
18
20
  class NotionClient(LoggingMixin):
19
21
  """Verbesserter Notion-Client mit automatischer Ressourcenverwaltung."""
20
22
 
@@ -50,7 +52,7 @@ class NotionClient(LoggingMixin):
50
52
 
51
53
  async def get(self, endpoint: str) -> Optional[Dict[str, Any]]:
52
54
  return await self._make_request(HttpMethod.GET, endpoint)
53
-
55
+
54
56
  async def get_page(self, page_id: str) -> Optional[Dict[str, Any]]:
55
57
  return await self.get(f"pages/{page_id}")
56
58
 
@@ -126,4 +128,4 @@ class NotionClient(LoggingMixin):
126
128
  loop.create_task(self.close())
127
129
  self.logger.debug("Created cleanup task for NotionClient")
128
130
  except RuntimeError:
129
- self.logger.warning("No event loop available for auto-closing NotionClient")
131
+ self.logger.warning("No event loop available for auto-closing NotionClient")
@@ -0,0 +1,84 @@
1
+ import re
2
+ from typing import Any, Dict, List
3
+ from notionary.util.logging_mixin import LoggingMixin
4
+
5
+
6
+ class NotionPageContentChunker(LoggingMixin):
7
+ """
8
+ Handles markdown text processing to comply with Notion API length limitations.
9
+
10
+ This class specifically addresses the Notion API constraint that limits
11
+ rich_text elements to a maximum of 2000 characters. This particularly affects
12
+ paragraph blocks within toggle blocks or other nested structures.
13
+
14
+ Resolves the following typical API error:
15
+ "validation_error - body.children[79].toggle.children[2].paragraph.rich_text[0].text.content.length
16
+ should be ≤ 2000, instead was 2162."
17
+
18
+ The class provides methods for:
19
+ 1. Automatically truncating text that exceeds the limit
20
+ 2. Splitting markdown into smaller units for separate API requests
21
+ """
22
+
23
+ def __init__(self, max_text_length: int = 1900):
24
+ self.max_text_length = max_text_length
25
+
26
+ def fix_blocks_content_length(
27
+ self, blocks: List[Dict[str, Any]]
28
+ ) -> List[Dict[str, Any]]:
29
+ """Check each block and ensure text content doesn't exceed Notion's limit."""
30
+ return [self._fix_single_block_content(block) for block in blocks]
31
+
32
+ def _fix_single_block_content(self, block: Dict[str, Any]) -> Dict[str, Any]:
33
+ """Fix content length in a single block and its children recursively."""
34
+ block_copy = block.copy()
35
+
36
+ block_type = block.get("type")
37
+ if not block_type:
38
+ return block_copy
39
+
40
+ content = block.get(block_type)
41
+ if not content:
42
+ return block_copy
43
+
44
+ if "rich_text" in content:
45
+ self._fix_rich_text_content(block_copy, block_type, content)
46
+
47
+ if "children" in content and content["children"]:
48
+ block_copy[block_type]["children"] = [
49
+ self._fix_single_block_content(child) for child in content["children"]
50
+ ]
51
+
52
+ return block_copy
53
+
54
+ def _fix_rich_text_content(
55
+ self, block_copy: Dict[str, Any], block_type: str, content: Dict[str, Any]
56
+ ) -> None:
57
+ """Fix rich text content that exceeds the length limit."""
58
+ rich_text = content["rich_text"]
59
+ for i, text_item in enumerate(rich_text):
60
+ if "text" not in text_item or "content" not in text_item["text"]:
61
+ continue
62
+
63
+ text_content = text_item["text"]["content"]
64
+ if len(text_content) <= self.max_text_length:
65
+ continue
66
+
67
+ self.logger.warning(
68
+ "Truncating text content from %d to %d chars",
69
+ len(text_content),
70
+ self.max_text_length,
71
+ )
72
+ block_copy[block_type]["rich_text"][i]["text"]["content"] = text_content[
73
+ : self.max_text_length
74
+ ]
75
+
76
+ def split_to_paragraphs(self, markdown_text: str) -> List[str]:
77
+ """Split markdown into paragraphs."""
78
+ paragraphs = re.split(r"\n\s*\n", markdown_text)
79
+ return [p for p in paragraphs if p.strip()]
80
+
81
+ def split_to_sentences(self, paragraph: str) -> List[str]:
82
+ """Split a paragraph into sentences."""
83
+ sentences = re.split(r"(?<=[.!?])\s+", paragraph)
84
+ return [s for s in sentences if s.strip()]
@@ -1,3 +1,4 @@
1
+ import re
1
2
  from typing import Any, Dict, List, Optional
2
3
 
3
4
  from notionary.core.converters.markdown_to_notion_converter import (
@@ -10,6 +11,9 @@ from notionary.core.converters.registry.block_element_registry import (
10
11
  BlockElementRegistry,
11
12
  )
12
13
  from notionary.core.notion_client import NotionClient
14
+ from notionary.core.page.content.notion_page_content_chunker import (
15
+ NotionPageContentChunker,
16
+ )
13
17
  from notionary.util.logging_mixin import LoggingMixin
14
18
 
15
19
 
@@ -28,15 +32,29 @@ class PageContentManager(LoggingMixin):
28
32
  self._notion_to_markdown_converter = NotionToMarkdownConverter(
29
33
  block_registry=block_registry
30
34
  )
35
+ self._chunker = NotionPageContentChunker()
31
36
 
32
37
  async def append_markdown(self, markdown_text: str) -> str:
33
- blocks = self._markdown_to_notion_converter.convert(markdown_text)
34
- result = await self._client.patch(
35
- f"blocks/{self.page_id}/children", {"children": blocks}
36
- )
37
- return (
38
- "Successfully added text to the page." if result else "Failed to add text."
39
- )
38
+ """
39
+ Append markdown text to a Notion page, automatically handling content length limits.
40
+ """
41
+ try:
42
+ blocks = self._markdown_to_notion_converter.convert(markdown_text)
43
+
44
+ # Fix any blocks that exceed Notion's content length limits
45
+ fixed_blocks = self._chunker.fix_blocks_content_length(blocks)
46
+
47
+ result = await self._client.patch(
48
+ f"blocks/{self.page_id}/children", {"children": fixed_blocks}
49
+ )
50
+ return (
51
+ "Successfully added text to the page."
52
+ if result
53
+ else "Failed to add text."
54
+ )
55
+ except Exception as e:
56
+ self.logger.error("Error appending markdown: %s", str(e))
57
+ raise
40
58
 
41
59
  async def clear(self) -> str:
42
60
  blocks = await self._client.get(f"blocks/{self.page_id}/children")
@@ -53,7 +71,7 @@ class PageContentManager(LoggingMixin):
53
71
  if block.get("type") in ["child_database", "database", "linked_database"]:
54
72
  skipped += 1
55
73
  continue
56
-
74
+
57
75
  if await self._client.delete(f"blocks/{block['id']}"):
58
76
  deleted += 1
59
77
 
@@ -20,90 +20,103 @@ class MetadataEditor(LoggingMixin):
20
20
  },
21
21
  )
22
22
 
23
- async def set_property(self, property_name: str, property_value: Any, property_type: str) -> Optional[Dict[str, Any]]:
24
- """
25
- Generic method to set any property on a Notion page.
26
-
27
- Args:
28
- property_name: The name of the property in Notion
29
- property_value: The value to set
30
- property_type: The type of property ('select', 'multi_select', 'status', 'relation', etc.)
31
-
32
- Returns:
33
- Optional[Dict[str, Any]]: The API response or None if the operation fails
34
- """
35
- property_payload = self._property_formatter.format_value(property_type, property_value)
36
-
37
- if not property_payload:
38
- self.logger.warning("Could not create payload for property type: %s", property_type)
39
- return None
40
-
41
- return await self._client.patch(
42
- f"pages/{self.page_id}",
43
- {
44
- "properties": {
45
- property_name: property_payload
46
- }
47
- },
23
+ async def set_property(
24
+ self, property_name: str, property_value: Any, property_type: str
25
+ ) -> Optional[Dict[str, Any]]:
26
+ """
27
+ Generic method to set any property on a Notion page.
28
+
29
+ Args:
30
+ property_name: The name of the property in Notion
31
+ property_value: The value to set
32
+ property_type: The type of property ('select', 'multi_select', 'status', 'relation', etc.)
33
+
34
+ Returns:
35
+ Optional[Dict[str, Any]]: The API response or None if the operation fails
36
+ """
37
+ property_payload = self._property_formatter.format_value(
38
+ property_type, property_value
39
+ )
40
+
41
+ if not property_payload:
42
+ self.logger.warning(
43
+ "Could not create payload for property type: %s", property_type
48
44
  )
45
+ return None
49
46
 
47
+ return await self._client.patch(
48
+ f"pages/{self.page_id}",
49
+ {"properties": {property_name: property_payload}},
50
+ )
50
51
 
51
52
  async def get_property_schema(self) -> Dict[str, Dict[str, Any]]:
52
53
  """
53
54
  Retrieves the schema for all properties of the page.
54
-
55
+
55
56
  Returns:
56
57
  Dict[str, Dict[str, Any]]: A dictionary mapping property names to their schema
57
58
  """
58
59
  page_data = await self._client.get_page(self.page_id)
59
60
  property_schema = {}
60
-
61
+
61
62
  if not page_data or "properties" not in page_data:
62
63
  return property_schema
63
-
64
+
64
65
  for prop_name, prop_data in page_data["properties"].items():
65
66
  prop_type = prop_data.get("type")
66
67
  property_schema[prop_name] = {
67
68
  "id": prop_data.get("id"),
68
69
  "type": prop_type,
69
- "name": prop_name
70
+ "name": prop_name,
70
71
  }
71
-
72
+
72
73
  try:
73
74
  if prop_type == "select" and "select" in prop_data:
74
75
  # Make sure prop_data["select"] is a dictionary before calling .get()
75
76
  if isinstance(prop_data["select"], dict):
76
- property_schema[prop_name]["options"] = prop_data["select"].get("options", [])
77
+ property_schema[prop_name]["options"] = prop_data["select"].get(
78
+ "options", []
79
+ )
77
80
  elif prop_type == "multi_select" and "multi_select" in prop_data:
78
81
  # Make sure prop_data["multi_select"] is a dictionary before calling .get()
79
82
  if isinstance(prop_data["multi_select"], dict):
80
- property_schema[prop_name]["options"] = prop_data["multi_select"].get("options", [])
83
+ property_schema[prop_name]["options"] = prop_data[
84
+ "multi_select"
85
+ ].get("options", [])
81
86
  elif prop_type == "status" and "status" in prop_data:
82
87
  # Make sure prop_data["status"] is a dictionary before calling .get()
83
88
  if isinstance(prop_data["status"], dict):
84
- property_schema[prop_name]["options"] = prop_data["status"].get("options", [])
89
+ property_schema[prop_name]["options"] = prop_data["status"].get(
90
+ "options", []
91
+ )
85
92
  except Exception as e:
86
- if hasattr(self, 'logger') and self.logger:
87
- self.logger.warning("Error processing property schema for '%s': %s", prop_name, e)
88
-
93
+ if hasattr(self, "logger") and self.logger:
94
+ self.logger.warning(
95
+ "Error processing property schema for '%s': %s", prop_name, e
96
+ )
97
+
89
98
  return property_schema
90
-
91
- async def set_property_by_name(self, property_name: str, value: Any) -> Optional[Dict[str, Any]]:
99
+
100
+ async def set_property_by_name(
101
+ self, property_name: str, value: Any
102
+ ) -> Optional[Dict[str, Any]]:
92
103
  """
93
104
  Sets a property value based on the property name, automatically detecting the property type.
94
-
105
+
95
106
  Args:
96
107
  property_name: The name of the property in Notion
97
108
  value: The value to set
98
-
109
+
99
110
  Returns:
100
111
  Optional[Dict[str, Any]]: The API response or None if the operation fails
101
112
  """
102
113
  property_schema = await self.get_property_schema()
103
-
114
+
104
115
  if property_name not in property_schema:
105
- self.logger.warning("Property '%s' not found in database schema", property_name)
116
+ self.logger.warning(
117
+ "Property '%s' not found in database schema", property_name
118
+ )
106
119
  return None
107
-
120
+
108
121
  property_type = property_schema[property_name]["type"]
109
- return await self.set_property(property_name, value, property_type)
122
+ return await self.set_property(property_name, value, property_type)
@@ -3,11 +3,12 @@ from typing import Any, Dict, Optional
3
3
  from notionary.core.notion_client import NotionClient
4
4
  from notionary.util.logging_mixin import LoggingMixin
5
5
 
6
+
6
7
  class NotionPageIconManager(LoggingMixin):
7
8
  def __init__(self, page_id: str, client: NotionClient):
8
9
  self.page_id = page_id
9
10
  self._client = client
10
-
11
+
11
12
  async def set_icon(
12
13
  self, emoji: Optional[str] = None, external_url: Optional[str] = None
13
14
  ) -> Optional[Dict[str, Any]]:
@@ -19,28 +20,25 @@ class NotionPageIconManager(LoggingMixin):
19
20
  return None
20
21
 
21
22
  return await self._client.patch(f"pages/{self.page_id}", {"icon": icon})
22
-
23
-
23
+
24
24
  async def get_icon(self) -> Optional[str]:
25
25
  """
26
26
  Retrieves the page icon - either emoji or external URL.
27
-
27
+
28
28
  Returns:
29
29
  str: Emoji character or URL if set, None if no icon
30
30
  """
31
31
  page_data = await self._client.get_page(self.page_id)
32
-
32
+
33
33
  if not page_data or "icon" not in page_data:
34
34
  return None
35
-
35
+
36
36
  icon_data = page_data.get("icon", {})
37
37
  icon_type = icon_data.get("type")
38
-
38
+
39
39
  if icon_type == "emoji":
40
40
  return icon_data.get("emoji")
41
41
  elif icon_type == "external":
42
42
  return icon_data.get("external", {}).get("url")
43
-
44
- return None
45
-
46
-
43
+
44
+ return None