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.
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.1.dist-info}/METADATA +2 -1
  50. notionary-0.2.1.dist-info/RECORD +60 -0
  51. {notionary-0.1.29.dist-info → notionary-0.2.1.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.1.dist-info}/licenses/LICENSE +0 -0
  59. {notionary-0.1.29.dist-info → notionary-0.2.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,100 @@
1
+ from dataclasses import dataclass
2
+ from typing import Literal, Optional, Dict, Any, Union
3
+
4
+ from pydantic import BaseModel
5
+
6
+
7
+ @dataclass
8
+ class User:
9
+ """Represents a Notion user object."""
10
+
11
+ object: str
12
+ id: str
13
+
14
+
15
+ @dataclass
16
+ class ExternalFile:
17
+ """Represents an external file, e.g., for cover images."""
18
+
19
+ url: str
20
+
21
+
22
+ @dataclass
23
+ class Cover:
24
+ """Cover image for a Notion page."""
25
+
26
+ type: str
27
+ external: ExternalFile
28
+
29
+
30
+ @dataclass
31
+ class EmojiIcon:
32
+ type: Literal["emoji"]
33
+ emoji: str
34
+
35
+
36
+ @dataclass
37
+ class ExternalIcon:
38
+ type: Literal["external"]
39
+ external: ExternalFile
40
+
41
+
42
+ @dataclass
43
+ class FileObject:
44
+ url: str
45
+ expiry_time: str
46
+
47
+
48
+ @dataclass
49
+ class FileIcon:
50
+ type: Literal["file"]
51
+ file: FileObject
52
+
53
+
54
+ Icon = Union[EmojiIcon, ExternalIcon, FileIcon]
55
+
56
+
57
+ @dataclass
58
+ class DatabaseParent:
59
+ type: Literal["database_id"]
60
+ database_id: str
61
+
62
+
63
+ @dataclass
64
+ class PageParent:
65
+ type: Literal["page_id"]
66
+ page_id: str
67
+
68
+
69
+ @dataclass
70
+ class WorkspaceParent:
71
+ type: Literal["workspace"]
72
+ workspace: bool = True
73
+
74
+
75
+ Parent = Union[DatabaseParent, PageParent, WorkspaceParent]
76
+
77
+
78
+ @dataclass
79
+ class NotionPageResponse(BaseModel):
80
+ """
81
+ Represents a full Notion page object as returned by the Notion API.
82
+
83
+ This structure is flexible and designed to work with different database schemas.
84
+ """
85
+
86
+ object: str
87
+ id: str
88
+ created_time: str
89
+ last_edited_time: str
90
+ created_by: User
91
+ last_edited_by: User
92
+ cover: Optional[Cover]
93
+ icon: Optional[Icon]
94
+ parent: Parent
95
+ archived: bool
96
+ in_trash: bool
97
+ properties: Dict[str, Any]
98
+ url: str
99
+ public_url: Optional[str]
100
+ request_id: str
@@ -5,6 +5,8 @@ from enum import Enum
5
5
  from typing import Dict, Any, Optional, Union
6
6
  import httpx
7
7
  from dotenv import load_dotenv
8
+ from notionary.models.notion_database_response import NotionDatabaserResponse
9
+ from notionary.models.notion_page_response import NotionPageResponse
8
10
  from notionary.util.logging_mixin import LoggingMixin
9
11
 
10
12
 
@@ -54,6 +56,7 @@ class NotionClient(LoggingMixin):
54
56
  await self.client.aclose()
55
57
  self.client = None
56
58
 
59
+ # Das hier für die unterschiedlichen responses hier noch richtig typne wäre gut.
57
60
  async def get(self, endpoint: str) -> Optional[Dict[str, Any]]:
58
61
  """
59
62
  Sends a GET request to the specified Notion API endpoint.
@@ -66,17 +69,33 @@ class NotionClient(LoggingMixin):
66
69
  """
67
70
  return await self._make_request(HttpMethod.GET, endpoint)
68
71
 
69
- async def get_page(self, page_id: str) -> Optional[Dict[str, Any]]:
72
+ # TODO: Get Blocks implementeren und Patch Blcoks hierfür das Typing finden:
73
+
74
+ async def get_database(self, database_id: str) -> NotionDatabaserResponse:
75
+ """
76
+ Ruft die Metadaten einer Notion-Datenbank anhand ihrer ID ab und gibt sie als NotionPageResponse zurück.
77
+
78
+ Args:
79
+ database_id: Die Notion-Datenbank-ID.
80
+
81
+ Returns:
82
+ Ein NotionPageResponse-Objekt mit den Datenbankmetadaten.
83
+ """
84
+ return NotionDatabaserResponse.model_validate(
85
+ await self.get(f"databases/{database_id}")
86
+ )
87
+
88
+ async def get_page(self, page_id: str) -> NotionPageResponse:
70
89
  """
71
- Fetches metadata for a Notion page by its ID.
90
+ Ruft die Metadaten einer Notion-Seite anhand ihrer ID ab und gibt sie als NotionPageResponse zurück.
72
91
 
73
92
  Args:
74
- page_id: The Notion page ID.
93
+ page_id: Die Notion-Seiten-ID.
75
94
 
76
95
  Returns:
77
- A dictionary with the page data, or None if the request failed.
96
+ Ein NotionPageResponse-Objekt mit den Seitenmetadaten.
78
97
  """
79
- return await self.get(f"pages/{page_id}")
98
+ return NotionPageResponse.model_validate(await self.get(f"pages/{page_id}"))
80
99
 
81
100
  async def post(
82
101
  self, endpoint: str, data: Optional[Dict[str, Any]] = None
@@ -108,6 +127,20 @@ class NotionClient(LoggingMixin):
108
127
  """
109
128
  return await self._make_request(HttpMethod.PATCH, endpoint, data)
110
129
 
130
+ async def patch_page(
131
+ self, page_id: str, data: Optional[Dict[str, Any]] = None
132
+ ) -> NotionPageResponse:
133
+ """
134
+ Sends a PATCH request to update a Notion page.
135
+
136
+ Args:
137
+ page_id: The ID of the page to update.
138
+ data: Optional dictionary payload to send with the request.
139
+ """
140
+ return NotionPageResponse.model_validate(
141
+ await self.patch(f"pages/{page_id}", data=data)
142
+ )
143
+
111
144
  async def delete(self, endpoint: str) -> bool:
112
145
  """
113
146
  Sends a DELETE request to the specified Notion API endpoint.
@@ -0,0 +1,68 @@
1
+ import json
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from notionary.elements.registry.block_registry import BlockRegistry
5
+ from notionary.notion_client import NotionClient
6
+
7
+ from notionary.page.notion_to_markdown_converter import (
8
+ NotionToMarkdownConverter,
9
+ )
10
+ from notionary.util.logging_mixin import LoggingMixin
11
+
12
+
13
+ class PageContentRetriever(LoggingMixin):
14
+ def __init__(
15
+ self,
16
+ page_id: str,
17
+ client: NotionClient,
18
+ block_registry: BlockRegistry,
19
+ ):
20
+ self.page_id = page_id
21
+ self._client = client
22
+ self._notion_to_markdown_converter = NotionToMarkdownConverter(
23
+ block_registry=block_registry
24
+ )
25
+
26
+ async def get_page_content(self) -> str:
27
+ blocks = await self._get_page_blocks_with_children()
28
+ return self._notion_to_markdown_converter.convert(blocks)
29
+
30
+ async def _get_page_blocks_with_children(
31
+ self, parent_id: Optional[str] = None
32
+ ) -> List[Dict[str, Any]]:
33
+ blocks = (
34
+ await self._get_blocks()
35
+ if parent_id is None
36
+ else await self._get_block_children(parent_id)
37
+ )
38
+
39
+ if not blocks:
40
+ return []
41
+
42
+ for block in blocks:
43
+ if not block.get("has_children"):
44
+ continue
45
+
46
+ block_id = block.get("id")
47
+ if not block_id:
48
+ continue
49
+
50
+ children = await self._get_page_blocks_with_children(block_id)
51
+ if children:
52
+ block["children"] = children
53
+
54
+ return blocks
55
+
56
+ async def _get_blocks(self) -> List[Dict[str, Any]]:
57
+ result = await self._client.get(f"blocks/{self.page_id}/children")
58
+ if not result:
59
+ self.logger.error("Error retrieving page content: %s", result.error)
60
+ return []
61
+ return result.get("results", [])
62
+
63
+ async def _get_block_children(self, block_id: str) -> List[Dict[str, Any]]:
64
+ result = await self._client.get(f"blocks/{block_id}/children")
65
+ if not result:
66
+ self.logger.error("Error retrieving block children: %s", result.error)
67
+ return []
68
+ return result.get("results", [])
@@ -0,0 +1,103 @@
1
+ from typing import Any, Dict
2
+
3
+ from notionary.elements.divider_element import DividerElement
4
+ from notionary.elements.registry.block_registry import BlockRegistry
5
+ from notionary.notion_client import NotionClient
6
+
7
+ from notionary.page.markdown_to_notion_converter import (
8
+ MarkdownToNotionConverter,
9
+ )
10
+ from notionary.page.notion_to_markdown_converter import (
11
+ NotionToMarkdownConverter,
12
+ )
13
+ from notionary.page.content.notion_page_content_chunker import (
14
+ NotionPageContentChunker,
15
+ )
16
+ from notionary.util.logging_mixin import LoggingMixin
17
+
18
+
19
+ class PageContentWriter(LoggingMixin):
20
+ def __init__(
21
+ self,
22
+ page_id: str,
23
+ client: NotionClient,
24
+ block_registry: BlockRegistry,
25
+ ):
26
+ self.page_id = page_id
27
+ self._client = client
28
+ self.block_registry = block_registry
29
+ self._markdown_to_notion_converter = MarkdownToNotionConverter(
30
+ block_registry=block_registry
31
+ )
32
+ self._notion_to_markdown_converter = NotionToMarkdownConverter(
33
+ block_registry=block_registry
34
+ )
35
+ self._chunker = NotionPageContentChunker()
36
+
37
+ async def append_markdown(self, markdown_text: str, append_divider=False) -> bool:
38
+ """
39
+ Append markdown text to a Notion page, automatically handling content length limits.
40
+
41
+ """
42
+ if append_divider and not self.block_registry.contains(DividerElement):
43
+ self.logger.warning(
44
+ "DividerElement not registered. Appending divider skipped."
45
+ )
46
+ append_divider = False
47
+
48
+ # Append divider in markdonw format as it will be converted to a Notion divider block
49
+ if append_divider:
50
+ markdown_text = markdown_text + "\n\n---\n\n"
51
+
52
+ try:
53
+ blocks = self._markdown_to_notion_converter.convert(markdown_text)
54
+ fixed_blocks = self._chunker.fix_blocks_content_length(blocks)
55
+
56
+ result = await self._client.patch(
57
+ f"blocks/{self.page_id}/children", {"children": fixed_blocks}
58
+ )
59
+ return bool(result)
60
+ except Exception as e:
61
+ self.logger.error("Error appending markdown: %s", str(e))
62
+ return False
63
+
64
+ async def clear_page_content(self) -> bool:
65
+ """
66
+ Clear all content of the page.
67
+ """
68
+ try:
69
+ blocks_resp = await self._client.get(f"blocks/{self.page_id}/children")
70
+ results = blocks_resp.get("results", []) if blocks_resp else []
71
+
72
+ if not results:
73
+ return True
74
+
75
+ success = True
76
+ for block in results:
77
+ block_success = await self._delete_block_with_children(block)
78
+ if not block_success:
79
+ success = False
80
+
81
+ return success
82
+ except Exception as e:
83
+ self.logger.error("Error clearing page content: %s", str(e))
84
+ return False
85
+
86
+ async def _delete_block_with_children(self, block: Dict[str, Any]) -> bool:
87
+ """
88
+ Delete a block and all its children.
89
+ """
90
+ try:
91
+ if block.get("has_children", False):
92
+ children_resp = await self._client.get(f"blocks/{block['id']}/children")
93
+ child_results = children_resp.get("results", [])
94
+
95
+ for child in child_results:
96
+ child_success = await self._delete_block_with_children(child)
97
+ if not child_success:
98
+ return False
99
+
100
+ return await self._client.delete(f"blocks/{block['id']}")
101
+ except Exception as e:
102
+ self.logger.error("Failed to delete block: %s", str(e))
103
+ return False
@@ -1,8 +1,8 @@
1
1
  from typing import Dict, Any, List, Optional, Tuple
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
 
@@ -13,10 +13,10 @@ class MarkdownToNotionConverter:
13
13
  TOGGLE_ELEMENT_TYPES = ["ToggleElement", "ToggleableHeadingElement"]
14
14
  PIPE_CONTENT_PATTERN = r"^\|\s?(.*)$"
15
15
 
16
- def __init__(self, block_registry: Optional[BlockElementRegistry] = None):
16
+ def __init__(self, block_registry: Optional[BlockRegistry] = None):
17
17
  """Initialize the converter with an optional custom block registry."""
18
18
  self._block_registry = (
19
- block_registry or BlockElementRegistryBuilder().create_full_registry()
19
+ block_registry or BlockRegistryBuilder().create_full_registry()
20
20
  )
21
21
 
22
22
  def convert(self, markdown_text: str) -> List[Dict[str, Any]]:
@@ -5,24 +5,75 @@ from notionary.util.logging_mixin import LoggingMixin
5
5
 
6
6
 
7
7
  class MetadataEditor(LoggingMixin):
8
+ """
9
+ Manages and edits the metadata and properties of a Notion page.
10
+ """
11
+
8
12
  def __init__(self, page_id: str, client: NotionClient):
13
+ """
14
+ Initialize the metadata editor.
15
+
16
+ Args:
17
+ page_id: The ID of the Notion page
18
+ client: The Notion API client
19
+ """
9
20
  self.page_id = page_id
10
21
  self._client = client
11
22
  self._property_formatter = NotionPropertyFormatter()
12
23
 
13
- async def set_title(self, title: str) -> Optional[Dict[str, Any]]:
14
- return await self._client.patch(
15
- f"pages/{self.page_id}",
16
- {
24
+ async def set_title(self, title: str) -> Optional[str]:
25
+ """
26
+ Sets the title of the page.
27
+
28
+ Args:
29
+ title: The new title for the page.
30
+
31
+ Returns:
32
+ Optional[str]: The new title if successful, None otherwise.
33
+ """
34
+ try:
35
+ data = {
17
36
  "properties": {
18
37
  "title": {"title": [{"type": "text", "text": {"content": title}}]}
19
38
  }
20
- },
21
- )
39
+ }
40
+
41
+ result = await self._client.patch_page(self.page_id, data)
42
+
43
+ if result:
44
+ return title
45
+ return None
46
+ except Exception as e:
47
+ self.logger.error("Error setting page title: %s", str(e))
48
+ return None
49
+
50
+ async def set_property_by_name(
51
+ self, property_name: str, value: Any
52
+ ) -> Optional[str]:
53
+ """
54
+ Sets a property value based on the property name, automatically detecting the property type.
55
+
56
+ Args:
57
+ property_name: The name of the property in Notion
58
+ value: The value to set
59
+
60
+ Returns:
61
+ Optional[str]: The property name if successful, None if operation fails
62
+ """
63
+ property_schema = await self._get_property_schema()
64
+
65
+ if property_name not in property_schema:
66
+ self.logger.warning(
67
+ "Property '%s' not found in database schema", property_name
68
+ )
69
+ return None
70
+
71
+ property_type = property_schema[property_name]["type"]
72
+ return await self._set_property(property_name, value, property_type)
22
73
 
23
- async def set_property(
74
+ async def _set_property(
24
75
  self, property_name: str, property_value: Any, property_type: str
25
- ) -> Optional[Dict[str, Any]]:
76
+ ) -> Optional[str]:
26
77
  """
27
78
  Generic method to set any property on a Notion page.
28
79
 
@@ -32,7 +83,7 @@ class MetadataEditor(LoggingMixin):
32
83
  property_type: The type of property ('select', 'multi_select', 'status', 'relation', etc.)
33
84
 
34
85
  Returns:
35
- Optional[Dict[str, Any]]: The API response or None if the operation fails
86
+ Optional[str]: The property name if successful, None if operation fails
36
87
  """
37
88
  property_payload = self._property_formatter.format_value(
38
89
  property_type, property_value
@@ -44,12 +95,19 @@ class MetadataEditor(LoggingMixin):
44
95
  )
45
96
  return None
46
97
 
47
- return await self._client.patch(
48
- f"pages/{self.page_id}",
49
- {"properties": {property_name: property_payload}},
50
- )
98
+ try:
99
+ result = await self._client.patch_page(
100
+ self.page_id, {"properties": {property_name: property_payload}}
101
+ )
102
+
103
+ if result:
104
+ return property_name
105
+ return None
106
+ except Exception as e:
107
+ self.logger.error("Error setting property '%s': %s", property_name, str(e))
108
+ return None
51
109
 
52
- async def get_property_schema(self) -> Dict[str, Dict[str, Any]]:
110
+ async def _get_property_schema(self) -> Dict[str, Dict[str, Any]]:
53
111
  """
54
112
  Retrieves the schema for all properties of the page.
55
113
 
@@ -59,64 +117,34 @@ class MetadataEditor(LoggingMixin):
59
117
  page_data = await self._client.get_page(self.page_id)
60
118
  property_schema = {}
61
119
 
62
- if not page_data or "properties" not in page_data:
63
- return property_schema
120
+ # Property types that can have options
121
+ option_types = {
122
+ "select": "select",
123
+ "multi_select": "multi_select",
124
+ "status": "status",
125
+ }
64
126
 
65
- for prop_name, prop_data in page_data["properties"].items():
127
+ for prop_name, prop_data in page_data.properties.items():
66
128
  prop_type = prop_data.get("type")
67
- property_schema[prop_name] = {
129
+
130
+ schema_entry = {
68
131
  "id": prop_data.get("id"),
69
132
  "type": prop_type,
70
133
  "name": prop_name,
71
134
  }
72
135
 
73
- try:
74
- if prop_type == "select" and "select" in prop_data:
75
- # Make sure prop_data["select"] is a dictionary before calling .get()
76
- if isinstance(prop_data["select"], dict):
77
- property_schema[prop_name]["options"] = prop_data["select"].get(
78
- "options", []
79
- )
80
- elif prop_type == "multi_select" and "multi_select" in prop_data:
81
- # Make sure prop_data["multi_select"] is a dictionary before calling .get()
82
- if isinstance(prop_data["multi_select"], dict):
83
- property_schema[prop_name]["options"] = prop_data[
84
- "multi_select"
85
- ].get("options", [])
86
- elif prop_type == "status" and "status" in prop_data:
87
- # Make sure prop_data["status"] is a dictionary before calling .get()
88
- if isinstance(prop_data["status"], dict):
89
- property_schema[prop_name]["options"] = prop_data["status"].get(
90
- "options", []
91
- )
92
- except Exception as e:
93
- if hasattr(self, "logger") and self.logger:
136
+ # Check if this property type can have options
137
+ if prop_type in option_types:
138
+ option_key = option_types[prop_type]
139
+ try:
140
+ prop_type_data = prop_data.get(option_key, {})
141
+ if isinstance(prop_type_data, dict):
142
+ schema_entry["options"] = prop_type_data.get("options", [])
143
+ except Exception as e:
94
144
  self.logger.warning(
95
145
  "Error processing property schema for '%s': %s", prop_name, e
96
146
  )
97
147
 
98
- return property_schema
99
-
100
- async def set_property_by_name(
101
- self, property_name: str, value: Any
102
- ) -> Optional[Dict[str, Any]]:
103
- """
104
- Sets a property value based on the property name, automatically detecting the property type.
105
-
106
- Args:
107
- property_name: The name of the property in Notion
108
- value: The value to set
109
-
110
- Returns:
111
- Optional[Dict[str, Any]]: The API response or None if the operation fails
112
- """
113
- property_schema = await self.get_property_schema()
114
-
115
- if property_name not in property_schema:
116
- self.logger.warning(
117
- "Property '%s' not found in database schema", property_name
118
- )
119
- return None
148
+ property_schema[prop_name] = schema_entry
120
149
 
121
- property_type = property_schema[property_name]["type"]
122
- return await self.set_property(property_name, value, property_type)
150
+ return property_schema
@@ -1,5 +1,7 @@
1
- from typing import Any, Dict, Optional
1
+ import json
2
+ from typing import Optional
2
3
 
4
+ from notionary.models.notion_page_response import EmojiIcon, ExternalIcon, FileIcon
3
5
  from notionary.notion_client import NotionClient
4
6
  from notionary.util.logging_mixin import LoggingMixin
5
7
 
@@ -9,42 +11,67 @@ class NotionPageIconManager(LoggingMixin):
9
11
  self.page_id = page_id
10
12
  self._client = client
11
13
 
12
- async def set_icon(
13
- self, emoji: Optional[str] = None, external_url: Optional[str] = None
14
- ) -> Optional[Dict[str, Any]]:
15
- if emoji:
16
- icon = {"type": "emoji", "emoji": emoji}
17
- elif external_url:
18
- icon = {"type": "external", "external": {"url": external_url}}
19
- else:
20
- return None
14
+ async def set_emoji_icon(self, emoji: str) -> Optional[str]:
15
+ """
16
+ Sets the page icon to an emoji.
21
17
 
22
- return await self._client.patch(f"pages/{self.page_id}", {"icon": icon})
18
+ Args:
19
+ emoji (str): The emoji character to set as the icon.
23
20
 
24
- async def get_icon(self) -> Optional[str]:
21
+ Returns:
22
+ Optional[str]: The emoji that was set as the icon, or None if the operation failed.
25
23
  """
26
- Retrieves the page icon - either emoji or external URL.
24
+ icon = {"type": "emoji", "emoji": emoji}
25
+ page_response = await self._client.patch_page(
26
+ page_id=self.page_id, data={"icon": icon}
27
+ )
28
+
29
+ if page_response and page_response.icon and page_response.icon.type == "emoji":
30
+ return page_response.icon.emoji
31
+ return None
32
+
33
+ async def set_external_icon(self, external_icon_url: str) -> Optional[str]:
34
+ """
35
+ Sets the page icon to an external image.
36
+
37
+ Args:
38
+ url (str): The URL of the external image to set as the icon.
27
39
 
28
40
  Returns:
29
- Optional[str]: Emoji character or URL if set, None if no icon
41
+ Optional[str]: The URL of the external image that was set as the icon,
42
+ or None if the operation failed.
30
43
  """
31
- page_data = await self._client.get_page(self.page_id)
44
+ icon = {"type": "external", "external": {"url": external_icon_url}}
45
+ page_response = await self._client.patch_page(
46
+ page_id=self.page_id, data={"icon": icon}
47
+ )
32
48
 
33
- if not page_data:
34
- return ""
49
+ if (
50
+ page_response
51
+ and page_response.icon
52
+ and page_response.icon.type == "external"
53
+ ):
54
+ return page_response.icon.external.url
55
+ return None
35
56
 
36
- # Get icon data, default to empty dict if not present or None
37
- icon_data = page_data.get("icon")
57
+ async def get_icon(self) -> Optional[str]:
58
+ """
59
+ Retrieves the page icon - either emoji or external URL.
38
60
 
39
- # If icon is None or not present, return None
40
- if not icon_data:
41
- return ""
61
+ Returns:
62
+ Optional[str]: Emoji character or URL if set, None if no icon.
63
+ """
64
+ page_response = await self._client.get_page(self.page_id)
65
+ if not page_response or not page_response.icon:
66
+ return None
42
67
 
43
- icon_type = icon_data.get("type")
68
+ icon = page_response.icon
44
69
 
45
- if icon_type == "emoji":
46
- return icon_data.get("emoji")
47
- if icon_type == "external":
48
- return icon_data.get("external", {}).get("url")
70
+ if isinstance(icon, EmojiIcon):
71
+ return icon.emoji
72
+ if isinstance(icon, ExternalIcon):
73
+ return icon.external.url
74
+ if isinstance(icon, FileIcon):
75
+ return icon.file.url
49
76
 
50
- return ""
77
+ return None