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.
- notionary/__init__.py +5 -5
- notionary/database/notion_database.py +50 -59
- notionary/database/notion_database_factory.py +16 -20
- notionary/elements/audio_element.py +1 -1
- notionary/elements/bookmark_element.py +1 -1
- notionary/elements/bulleted_list_element.py +2 -8
- notionary/elements/callout_element.py +1 -1
- notionary/elements/code_block_element.py +1 -1
- notionary/elements/divider_element.py +1 -1
- notionary/elements/embed_element.py +1 -1
- notionary/elements/heading_element.py +2 -8
- notionary/elements/image_element.py +1 -1
- notionary/elements/mention_element.py +1 -1
- notionary/elements/notion_block_element.py +1 -1
- notionary/elements/numbered_list_element.py +2 -7
- notionary/elements/paragraph_element.py +1 -1
- notionary/elements/qoute_element.py +1 -1
- notionary/elements/registry/{block_element_registry.py → block_registry.py} +70 -26
- notionary/elements/registry/{block_element_registry_builder.py → block_registry_builder.py} +48 -32
- notionary/elements/table_element.py +1 -1
- notionary/elements/text_inline_formatter.py +13 -9
- notionary/elements/todo_element.py +1 -1
- notionary/elements/toggle_element.py +1 -1
- notionary/elements/toggleable_heading_element.py +1 -1
- notionary/elements/video_element.py +1 -1
- notionary/models/notion_block_response.py +264 -0
- notionary/models/notion_database_response.py +63 -0
- notionary/models/notion_page_response.py +100 -0
- notionary/notion_client.py +38 -5
- notionary/page/content/page_content_retriever.py +68 -0
- notionary/page/content/page_content_writer.py +103 -0
- notionary/page/markdown_to_notion_converter.py +5 -5
- notionary/page/metadata/metadata_editor.py +91 -63
- notionary/page/metadata/notion_icon_manager.py +55 -28
- notionary/page/metadata/notion_page_cover_manager.py +23 -20
- notionary/page/notion_page.py +223 -218
- notionary/page/notion_page_factory.py +102 -151
- notionary/page/notion_to_markdown_converter.py +5 -5
- notionary/page/properites/database_property_service.py +11 -55
- notionary/page/properites/page_property_manager.py +44 -67
- notionary/page/properites/property_value_extractor.py +3 -3
- notionary/page/relations/notion_page_relation_manager.py +165 -213
- notionary/page/relations/notion_page_title_resolver.py +59 -41
- notionary/page/relations/page_database_relation.py +7 -9
- notionary/{elements/prompts → prompting}/element_prompt_content.py +19 -4
- notionary/prompting/markdown_syntax_prompt_generator.py +92 -0
- notionary/util/logging_mixin.py +17 -8
- notionary/util/warn_direct_constructor_usage.py +54 -0
- {notionary-0.1.29.dist-info → notionary-0.2.1.dist-info}/METADATA +2 -1
- notionary-0.2.1.dist-info/RECORD +60 -0
- {notionary-0.1.29.dist-info → notionary-0.2.1.dist-info}/WHEEL +1 -1
- notionary/database/database_info_service.py +0 -43
- notionary/elements/prompts/synthax_prompt_builder.py +0 -150
- notionary/page/content/page_content_manager.py +0 -211
- notionary/page/properites/property_operation_result.py +0 -116
- notionary/page/relations/relation_operation_result.py +0 -144
- notionary-0.1.29.dist-info/RECORD +0 -58
- {notionary-0.1.29.dist-info → notionary-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
notionary/notion_client.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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:
|
93
|
+
page_id: Die Notion-Seiten-ID.
|
75
94
|
|
76
95
|
Returns:
|
77
|
-
|
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.
|
4
|
-
from notionary.elements.registry.
|
5
|
-
|
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[
|
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
|
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[
|
14
|
-
|
15
|
-
|
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
|
74
|
+
async def _set_property(
|
24
75
|
self, property_name: str, property_value: Any, property_type: str
|
25
|
-
) -> Optional[
|
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[
|
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
|
-
|
48
|
-
|
49
|
-
|
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
|
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
|
-
|
63
|
-
|
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
|
127
|
+
for prop_name, prop_data in page_data.properties.items():
|
66
128
|
prop_type = prop_data.get("type")
|
67
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
)
|
80
|
-
|
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
|
-
|
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
|
-
|
122
|
-
return await self.set_property(property_name, value, property_type)
|
150
|
+
return property_schema
|
@@ -1,5 +1,7 @@
|
|
1
|
-
|
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
|
13
|
-
|
14
|
-
|
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
|
-
|
18
|
+
Args:
|
19
|
+
emoji (str): The emoji character to set as the icon.
|
23
20
|
|
24
|
-
|
21
|
+
Returns:
|
22
|
+
Optional[str]: The emoji that was set as the icon, or None if the operation failed.
|
25
23
|
"""
|
26
|
-
|
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]:
|
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
|
-
|
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
|
34
|
-
|
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
|
-
|
37
|
-
|
57
|
+
async def get_icon(self) -> Optional[str]:
|
58
|
+
"""
|
59
|
+
Retrieves the page icon - either emoji or external URL.
|
38
60
|
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
68
|
+
icon = page_response.icon
|
44
69
|
|
45
|
-
if
|
46
|
-
return
|
47
|
-
if
|
48
|
-
return
|
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
|