notionary 0.2.13__py3-none-any.whl → 0.2.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. notionary/__init__.py +3 -16
  2. notionary/{notion_client.py → base_notion_client.py} +92 -98
  3. notionary/blocks/__init__.py +61 -0
  4. notionary/{elements → blocks}/audio_element.py +6 -3
  5. notionary/{elements → blocks}/bookmark_element.py +3 -5
  6. notionary/{elements → blocks}/bulleted_list_element.py +5 -6
  7. notionary/{elements → blocks}/callout_element.py +4 -6
  8. notionary/{elements → blocks}/code_block_element.py +4 -5
  9. notionary/{elements → blocks}/column_element.py +3 -5
  10. notionary/{elements → blocks}/divider_element.py +3 -5
  11. notionary/{elements → blocks}/embed_element.py +4 -5
  12. notionary/{elements → blocks}/heading_element.py +4 -7
  13. notionary/{elements → blocks}/image_element.py +4 -5
  14. notionary/{elements → blocks}/mention_element.py +3 -6
  15. notionary/blocks/notion_block_client.py +26 -0
  16. notionary/{elements → blocks}/notion_block_element.py +2 -3
  17. notionary/{elements → blocks}/numbered_list_element.py +4 -6
  18. notionary/{elements → blocks}/paragraph_element.py +4 -6
  19. notionary/{prompting/element_prompt_content.py → blocks/prompts/element_prompt_builder.py} +1 -40
  20. notionary/blocks/prompts/element_prompt_content.py +41 -0
  21. notionary/{elements → blocks}/qoute_element.py +4 -5
  22. notionary/{elements → blocks}/registry/block_registry.py +4 -26
  23. notionary/{elements → blocks}/registry/block_registry_builder.py +26 -25
  24. notionary/{elements → blocks}/table_element.py +5 -6
  25. notionary/{elements → blocks}/text_inline_formatter.py +1 -4
  26. notionary/{elements → blocks}/todo_element.py +5 -6
  27. notionary/{elements → blocks}/toggle_element.py +3 -5
  28. notionary/{elements → blocks}/toggleable_heading_element.py +4 -6
  29. notionary/{elements → blocks}/video_element.py +4 -5
  30. notionary/cli/main.py +157 -128
  31. notionary/cli/onboarding.py +10 -9
  32. notionary/database/__init__.py +0 -0
  33. notionary/database/client.py +132 -0
  34. notionary/database/database_exceptions.py +13 -0
  35. notionary/database/factory.py +0 -0
  36. notionary/database/filter_builder.py +175 -0
  37. notionary/database/notion_database.py +339 -126
  38. notionary/database/notion_database_provider.py +230 -0
  39. notionary/elements/__init__.py +0 -0
  40. notionary/models/notion_database_response.py +294 -13
  41. notionary/models/notion_page_response.py +9 -31
  42. notionary/models/search_response.py +0 -0
  43. notionary/page/__init__.py +0 -0
  44. notionary/page/client.py +110 -0
  45. notionary/page/content/page_content_retriever.py +5 -20
  46. notionary/page/content/page_content_writer.py +3 -4
  47. notionary/page/formatting/markdown_to_notion_converter.py +1 -3
  48. notionary/{prompting → page}/markdown_syntax_prompt_generator.py +1 -2
  49. notionary/page/notion_page.py +354 -317
  50. notionary/page/notion_to_markdown_converter.py +1 -4
  51. notionary/page/properites/property_value_extractor.py +0 -64
  52. notionary/page/{properites/property_formatter.py → property_formatter.py} +7 -4
  53. notionary/page/search_filter_builder.py +131 -0
  54. notionary/page/utils.py +60 -0
  55. notionary/util/__init__.py +12 -3
  56. notionary/util/factory_decorator.py +33 -0
  57. notionary/util/fuzzy_matcher.py +82 -0
  58. notionary/util/page_id_utils.py +0 -21
  59. notionary/util/singleton_metaclass.py +22 -0
  60. notionary/workspace.py +69 -0
  61. {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/METADATA +4 -1
  62. notionary-0.2.14.dist-info/RECORD +72 -0
  63. notionary/database/database_discovery.py +0 -142
  64. notionary/database/notion_database_factory.py +0 -190
  65. notionary/exceptions/database_exceptions.py +0 -76
  66. notionary/exceptions/page_creation_exception.py +0 -9
  67. notionary/page/metadata/metadata_editor.py +0 -150
  68. notionary/page/metadata/notion_icon_manager.py +0 -77
  69. notionary/page/metadata/notion_page_cover_manager.py +0 -56
  70. notionary/page/notion_page_factory.py +0 -328
  71. notionary/page/properites/database_property_service.py +0 -302
  72. notionary/page/properites/page_property_manager.py +0 -152
  73. notionary/page/relations/notion_page_relation_manager.py +0 -350
  74. notionary/page/relations/notion_page_title_resolver.py +0 -104
  75. notionary/page/relations/page_database_relation.py +0 -68
  76. notionary/util/warn_direct_constructor_usage.py +0 -54
  77. notionary-0.2.13.dist-info/RECORD +0 -67
  78. /notionary/util/{singleton.py → singleton_decorator.py} +0 -0
  79. {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/WHEEL +0 -0
  80. {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/entry_points.txt +0 -0
  81. {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/licenses/LICENSE +0 -0
  82. {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/top_level.txt +0 -0
@@ -1,142 +0,0 @@
1
- from typing import (
2
- AsyncGenerator,
3
- Dict,
4
- List,
5
- Optional,
6
- Any,
7
- Tuple,
8
- )
9
- from notionary.notion_client import NotionClient
10
- from notionary.util 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 __call__(self, page_size: int = 100) -> List[Tuple[str, str]]:
30
- """
31
- Discover databases and print the results in a nicely formatted way.
32
-
33
- This is a convenience method that discovers databases and handles
34
- the formatting and printing of results.
35
-
36
- Args:
37
- page_size: The number of databases to fetch per request
38
-
39
- Returns:
40
- The same list of databases as discover() for further processing
41
- """
42
- databases = await self._discover(page_size)
43
-
44
- if not databases:
45
- print("\n⚠️ No databases found!")
46
- print("Please ensure your Notion integration has access to databases.")
47
- print(
48
- "You need to share the databases with your integration in Notion settings."
49
- )
50
- return databases
51
-
52
- print(f"✅ Found {len(databases)} databases:")
53
-
54
- for i, (title, db_id) in enumerate(databases, 1):
55
- print(f"{i}. {title} (ID: {db_id})")
56
-
57
- return databases
58
-
59
- async def _discover(self, page_size: int = 100) -> List[Tuple[str, str]]:
60
- """
61
- Discover all accessible databases and return their titles and IDs.
62
-
63
- Args:
64
- page_size: The number of databases to fetch per request
65
-
66
- Returns:
67
- List of tuples containing (database_title, database_id)
68
- """
69
- databases = []
70
-
71
- async for database in self._iter_databases(page_size):
72
- db_id = database.get("id")
73
- if not db_id:
74
- continue
75
-
76
- title = self._extract_database_title(database)
77
- databases.append((title, db_id))
78
-
79
- return databases
80
-
81
- async def _iter_databases(
82
- self, page_size: int = 100
83
- ) -> AsyncGenerator[Dict[str, Any], None]:
84
- """
85
- Asynchronous generator that yields Notion databases one by one.
86
-
87
- Uses the Notion API to provide paginated access to all databases
88
- without loading all of them into memory at once.
89
-
90
- Args:
91
- page_size: The number of databases to fetch per request
92
-
93
- Yields:
94
- Individual database objects from the Notion API
95
- """
96
- start_cursor: Optional[str] = None
97
-
98
- while True:
99
- body: Dict[str, Any] = {
100
- "filter": {"value": "database", "property": "object"},
101
- "page_size": page_size,
102
- }
103
-
104
- if start_cursor:
105
- body["start_cursor"] = start_cursor
106
-
107
- result = await self._client.post("search", data=body)
108
-
109
- if not result or "results" not in result:
110
- self.logger.error("Error fetching databases")
111
- return
112
-
113
- for database in result["results"]:
114
- yield database
115
-
116
- if not result.get("has_more") or not result.get("next_cursor"):
117
- return
118
-
119
- start_cursor = result["next_cursor"]
120
-
121
- def _extract_database_title(self, database: Dict[str, Any]) -> str:
122
- """
123
- Extract the database title from a Notion API response.
124
-
125
- Args:
126
- database: The database object from the Notion API
127
-
128
- Returns:
129
- The extracted title or "Untitled" if no title is found
130
- """
131
- if "title" not in database:
132
- return "Untitled"
133
-
134
- title_parts = []
135
- for text_obj in database["title"]:
136
- if "plain_text" in text_obj:
137
- title_parts.append(text_obj["plain_text"])
138
-
139
- if not title_parts:
140
- return "Untitled"
141
-
142
- return "".join(title_parts)
@@ -1,190 +0,0 @@
1
- from typing import List, Optional, Dict, Any
2
- from difflib import SequenceMatcher
3
-
4
- from notionary.database.notion_database import NotionDatabase
5
- from notionary.notion_client import NotionClient
6
- from notionary.exceptions.database_exceptions import (
7
- DatabaseConnectionError,
8
- DatabaseInitializationError,
9
- DatabaseNotFoundException,
10
- DatabaseParsingError,
11
- NotionDatabaseException,
12
- )
13
- from notionary.util import LoggingMixin
14
- from notionary.util import format_uuid
15
- from notionary.util import singleton
16
-
17
-
18
- @singleton
19
- class NotionDatabaseFactory(LoggingMixin):
20
- """
21
- Factory class for creating NotionDatabaseManager instances.
22
- Provides methods for creating managers by database ID or name.
23
- """
24
-
25
- @classmethod
26
- def from_database_id(
27
- cls, database_id: str, token: Optional[str] = None
28
- ) -> NotionDatabase:
29
- """
30
- Create a NotionDatabaseManager from a database ID.
31
-
32
- Args:
33
- database_id: The ID of the Notion database
34
- token: Optional Notion API token (uses environment variable if not provided)
35
-
36
- Returns:
37
- An initialized NotionDatabaseManager instance
38
- """
39
-
40
- try:
41
- formatted_id = format_uuid(database_id) or database_id
42
-
43
- manager = NotionDatabase(formatted_id, token)
44
-
45
- cls.logger.info(
46
- "Successfully created database manager for ID: %s", formatted_id
47
- )
48
- return manager
49
-
50
- except DatabaseInitializationError:
51
- raise
52
- except NotionDatabaseException:
53
- raise
54
- except Exception as e:
55
- error_msg = f"Error connecting to database {database_id}: {str(e)}"
56
- cls.logger.error(error_msg)
57
- raise DatabaseConnectionError(error_msg) from e
58
-
59
- @classmethod
60
- async def from_database_name(
61
- cls, database_name: str, token: Optional[str] = None
62
- ) -> NotionDatabase:
63
- """
64
- Create a NotionDatabaseManager by finding a database with a matching name.
65
- Uses fuzzy matching to find the closest match to the given name.
66
-
67
- Args:
68
- database_name: The name of the Notion database to search for
69
- token: Optional Notion API token (uses environment variable if not provided)
70
-
71
- Returns:
72
- An initialized NotionDatabaseManager instance
73
- """
74
- cls.logger.debug("Searching for database with name: %s", database_name)
75
-
76
- client = NotionClient(token=token)
77
-
78
- try:
79
- cls.logger.debug("Using search endpoint to find databases")
80
-
81
- search_payload = {
82
- "filter": {"property": "object", "value": "database"},
83
- "page_size": 100,
84
- }
85
-
86
- response = await client.post("search", search_payload)
87
-
88
- if not response or "results" not in response:
89
- error_msg = "Failed to fetch databases using search endpoint"
90
- cls.logger.error(error_msg)
91
- raise DatabaseConnectionError(error_msg)
92
-
93
- databases = response.get("results", [])
94
-
95
- if not databases:
96
- error_msg = "No databases found"
97
- cls.logger.warning(error_msg)
98
- raise DatabaseNotFoundException(database_name, error_msg)
99
-
100
- cls.logger.debug(
101
- "Found %d databases, searching for best match", len(databases)
102
- )
103
-
104
- best_match = None
105
- best_score = 0
106
-
107
- for db in databases:
108
- title = cls._extract_title_from_database(db)
109
-
110
- score = SequenceMatcher(
111
- None, database_name.lower(), title.lower()
112
- ).ratio()
113
-
114
- if score > best_score:
115
- best_score = score
116
- best_match = db
117
-
118
- if best_score < 0.6 or not best_match:
119
- error_msg = f"No good database name match found for '{database_name}'. Best match had score {best_score:.2f}"
120
- cls.logger.warning(error_msg)
121
- raise DatabaseNotFoundException(database_name, error_msg)
122
-
123
- database_id = best_match.get("id")
124
-
125
- if not database_id:
126
- error_msg = "Best match database has no ID"
127
- cls.logger.error(error_msg)
128
- raise DatabaseParsingError(error_msg)
129
-
130
- matched_name = cls._extract_title_from_database(best_match)
131
-
132
- cls.logger.info(
133
- "Found matching database: '%s' (ID: %s) with score: %.2f",
134
- matched_name,
135
- database_id,
136
- best_score,
137
- )
138
-
139
- manager = NotionDatabase(database_id, token)
140
-
141
- cls.logger.info(
142
- "Successfully created database manager for '%s'", matched_name
143
- )
144
- await client.close()
145
- return manager
146
-
147
- except NotionDatabaseException:
148
- await client.close()
149
- raise
150
- except Exception as e:
151
- error_msg = f"Error finding database by name: {str(e)}"
152
- cls.logger.error(error_msg)
153
- await client.close()
154
- raise DatabaseConnectionError(error_msg) from e
155
-
156
- @classmethod
157
- def _extract_title_from_database(cls, database: Dict[str, Any]) -> str:
158
- """
159
- Extract the title from a database object.
160
- """
161
- try:
162
- if "title" in database:
163
- return cls._extract_text_from_rich_text(database["title"])
164
-
165
- if "properties" in database and "title" in database["properties"]:
166
- title_prop = database["properties"]["title"]
167
- if "title" in title_prop:
168
- return cls._extract_text_from_rich_text(title_prop["title"])
169
-
170
- return "Untitled"
171
-
172
- except Exception as e:
173
- error_msg = f"Error extracting database title: {str(e)}"
174
- cls.class_logger().warning(error_msg)
175
- raise DatabaseParsingError(error_msg) from e
176
-
177
- @classmethod
178
- def _extract_text_from_rich_text(cls, rich_text: List[Dict[str, Any]]) -> str:
179
- """
180
- Extract plain text from a rich text array.
181
- """
182
- if not rich_text:
183
- return ""
184
-
185
- text_parts = []
186
- for text_obj in rich_text:
187
- if "plain_text" in text_obj:
188
- text_parts.append(text_obj["plain_text"])
189
-
190
- return "".join(text_parts)
@@ -1,76 +0,0 @@
1
- from typing import Optional
2
-
3
-
4
- class NotionDatabaseException(Exception):
5
- """Base exception for all Notion database operations."""
6
-
7
- pass
8
-
9
-
10
- class DatabaseNotFoundException(NotionDatabaseException):
11
- """Exception raised when a database is not found."""
12
-
13
- def __init__(self, identifier: str, message: str = None):
14
- self.identifier = identifier
15
- self.message = message or f"Database not found: {identifier}"
16
- super().__init__(self.message)
17
-
18
-
19
- class DatabaseInitializationError(NotionDatabaseException):
20
- """Exception raised when a database manager fails to initialize."""
21
-
22
- def __init__(self, database_id: str, message: str = None):
23
- self.database_id = database_id
24
- self.message = (
25
- message or f"Failed to initialize database manager for ID: {database_id}"
26
- )
27
- super().__init__(self.message)
28
-
29
-
30
- class DatabaseConnectionError(NotionDatabaseException):
31
- """Exception raised when there's an error connecting to Notion API."""
32
-
33
- def __init__(self, message: str = None):
34
- self.message = message or "Error connecting to Notion API"
35
- super().__init__(self.message)
36
-
37
-
38
- class DatabaseParsingError(NotionDatabaseException):
39
- """Exception raised when there's an error parsing database data."""
40
-
41
- def __init__(self, message: str = None):
42
- self.message = message or "Error parsing database data"
43
- super().__init__(self.message)
44
-
45
-
46
- class PageNotFoundException(NotionDatabaseException):
47
- """Raised when a page is not found or cannot be accessed."""
48
-
49
- def __init__(self, page_id: str, message: Optional[str] = None):
50
- self.page_id = page_id
51
- self.message = message or f"Page not found: {page_id}"
52
- super().__init__(self.message)
53
-
54
-
55
- class PageOperationError(NotionDatabaseException):
56
- """Raised when an operation on a page fails."""
57
-
58
- def __init__(self, page_id: str, operation: str, message: Optional[str] = None):
59
- self.page_id = page_id
60
- self.operation = operation
61
- self.message = message or f"Failed to {operation} page {page_id}"
62
- super().__init__(self.message)
63
-
64
-
65
- class PropertyError(NotionDatabaseException):
66
- """Raised when there's an error with database properties."""
67
-
68
- def __init__(
69
- self, property_name: Optional[str] = None, message: Optional[str] = None
70
- ):
71
- self.property_name = property_name
72
- self.message = (
73
- message
74
- or f"Error with property{' ' + property_name if property_name else ''}"
75
- )
76
- super().__init__(self.message)
@@ -1,9 +0,0 @@
1
- from typing import Optional
2
-
3
-
4
- class PageCreationException(Exception):
5
- """Exception raised when page creation in Notion fails."""
6
-
7
- def __init__(self, message: str, response: Optional[dict] = None):
8
- super().__init__(message)
9
- self.response = response
@@ -1,150 +0,0 @@
1
- from typing import Any, Dict, Optional
2
- from notionary.notion_client import NotionClient
3
- from notionary.page.properites.property_formatter import NotionPropertyFormatter
4
- from notionary.util import LoggingMixin
5
-
6
-
7
- class MetadataEditor(LoggingMixin):
8
- """
9
- Manages and edits the metadata and properties of a Notion page.
10
- """
11
-
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
- """
20
- self.page_id = page_id
21
- self._client = client
22
- self._property_formatter = NotionPropertyFormatter()
23
-
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 = {
36
- "properties": {
37
- "title": {"title": [{"type": "text", "text": {"content": title}}]}
38
- }
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)
73
-
74
- async def _set_property(
75
- self, property_name: str, property_value: Any, property_type: str
76
- ) -> Optional[str]:
77
- """
78
- Generic method to set any property on a Notion page.
79
-
80
- Args:
81
- property_name: The name of the property in Notion
82
- property_value: The value to set
83
- property_type: The type of property ('select', 'multi_select', 'status', 'relation', etc.)
84
-
85
- Returns:
86
- Optional[str]: The property name if successful, None if operation fails
87
- """
88
- property_payload = self._property_formatter.format_value(
89
- property_type, property_value
90
- )
91
-
92
- if not property_payload:
93
- self.logger.warning(
94
- "Could not create payload for property type: %s", property_type
95
- )
96
- return None
97
-
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
109
-
110
- async def _get_property_schema(self) -> Dict[str, Dict[str, Any]]:
111
- """
112
- Retrieves the schema for all properties of the page.
113
-
114
- Returns:
115
- Dict[str, Dict[str, Any]]: A dictionary mapping property names to their schema
116
- """
117
- page_data = await self._client.get_page(self.page_id)
118
- property_schema = {}
119
-
120
- # Property types that can have options
121
- option_types = {
122
- "select": "select",
123
- "multi_select": "multi_select",
124
- "status": "status",
125
- }
126
-
127
- for prop_name, prop_data in page_data.properties.items():
128
- prop_type = prop_data.get("type")
129
-
130
- schema_entry = {
131
- "id": prop_data.get("id"),
132
- "type": prop_type,
133
- "name": prop_name,
134
- }
135
-
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:
144
- self.logger.warning(
145
- "Error processing property schema for '%s': %s", prop_name, e
146
- )
147
-
148
- property_schema[prop_name] = schema_entry
149
-
150
- return property_schema
@@ -1,77 +0,0 @@
1
- import json
2
- from typing import Optional
3
-
4
- from notionary.models.notion_page_response import EmojiIcon, ExternalIcon, FileIcon
5
- from notionary.notion_client import NotionClient
6
- from notionary.util import LoggingMixin
7
-
8
-
9
- class NotionPageIconManager(LoggingMixin):
10
- def __init__(self, page_id: str, client: NotionClient):
11
- self.page_id = page_id
12
- self._client = client
13
-
14
- async def set_emoji_icon(self, emoji: str) -> Optional[str]:
15
- """
16
- Sets the page icon to an emoji.
17
-
18
- Args:
19
- emoji (str): The emoji character to set as the icon.
20
-
21
- Returns:
22
- Optional[str]: The emoji that was set as the icon, or None if the operation failed.
23
- """
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.
39
-
40
- Returns:
41
- Optional[str]: The URL of the external image that was set as the icon,
42
- or None if the operation failed.
43
- """
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
- )
48
-
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
56
-
57
- async def get_icon(self) -> Optional[str]:
58
- """
59
- Retrieves the page icon - either emoji or external URL.
60
-
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
67
-
68
- icon = page_response.icon
69
-
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
76
-
77
- return None