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.
- notionary/__init__.py +13 -2
- notionary/core/converters/elements/audio_element.py +143 -0
- notionary/core/converters/elements/embed_element.py +2 -4
- notionary/core/converters/elements/toggle_element.py +28 -20
- notionary/core/converters/markdown_to_notion_converter.py +70 -109
- notionary/core/converters/registry/block_element_registry.py +2 -6
- notionary/core/converters/registry/block_element_registry_builder.py +2 -0
- notionary/core/database/database_discovery.py +140 -0
- notionary/core/database/notion_database_manager.py +26 -49
- notionary/core/database/notion_database_manager_factory.py +10 -4
- notionary/core/notion_client.py +4 -2
- notionary/core/page/content/notion_page_content_chunker.py +84 -0
- notionary/core/page/content/page_content_manager.py +26 -8
- notionary/core/page/metadata/metadata_editor.py +57 -44
- notionary/core/page/metadata/notion_icon_manager.py +9 -11
- notionary/core/page/metadata/notion_page_cover_manager.py +15 -20
- notionary/core/page/notion_page_manager.py +137 -156
- notionary/core/page/properites/database_property_service.py +114 -98
- notionary/core/page/properites/page_property_manager.py +78 -49
- notionary/core/page/properites/property_formatter.py +1 -1
- notionary/core/page/properites/property_operation_result.py +43 -30
- notionary/core/page/properites/property_value_extractor.py +26 -8
- notionary/core/page/relations/notion_page_relation_manager.py +71 -52
- notionary/core/page/relations/notion_page_title_resolver.py +11 -11
- notionary/core/page/relations/page_database_relation.py +14 -14
- notionary/core/page/relations/relation_operation_result.py +50 -41
- notionary/util/page_id_utils.py +11 -7
- {notionary-0.1.10.dist-info → notionary-0.1.12.dist-info}/METADATA +1 -1
- {notionary-0.1.10.dist-info → notionary-0.1.12.dist-info}/RECORD +32 -30
- notionary/core/database/notion_database_schema.py +0 -104
- {notionary-0.1.10.dist-info → notionary-0.1.12.dist-info}/WHEEL +0 -0
- {notionary-0.1.10.dist-info → notionary-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
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(
|
47
|
-
|
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(
|
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(
|
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(
|
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(
|
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
|
|
notionary/core/notion_client.py
CHANGED
@@ -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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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(
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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(
|
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[
|
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(
|
89
|
+
property_schema[prop_name]["options"] = prop_data["status"].get(
|
90
|
+
"options", []
|
91
|
+
)
|
85
92
|
except Exception as e:
|
86
|
-
if hasattr(self,
|
87
|
-
self.logger.warning(
|
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(
|
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(
|
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
|