notionary 0.2.13__py3-none-any.whl → 0.2.15__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 +3 -16
- notionary/{notion_client.py → base_notion_client.py} +92 -98
- notionary/blocks/__init__.py +61 -0
- notionary/{elements → blocks}/audio_element.py +6 -3
- notionary/{elements → blocks}/bookmark_element.py +3 -5
- notionary/{elements → blocks}/bulleted_list_element.py +5 -6
- notionary/{elements → blocks}/callout_element.py +4 -6
- notionary/{elements → blocks}/code_block_element.py +4 -5
- notionary/{elements → blocks}/column_element.py +3 -5
- notionary/{elements → blocks}/divider_element.py +3 -5
- notionary/{elements → blocks}/embed_element.py +4 -5
- notionary/{elements → blocks}/heading_element.py +4 -7
- notionary/{elements → blocks}/image_element.py +4 -5
- notionary/{elements → blocks}/mention_element.py +3 -6
- notionary/blocks/notion_block_client.py +26 -0
- notionary/{elements → blocks}/notion_block_element.py +2 -3
- notionary/{elements → blocks}/numbered_list_element.py +4 -6
- notionary/{elements → blocks}/paragraph_element.py +4 -6
- notionary/{prompting/element_prompt_content.py → blocks/prompts/element_prompt_builder.py} +1 -40
- notionary/blocks/prompts/element_prompt_content.py +41 -0
- notionary/{elements → blocks}/qoute_element.py +4 -5
- notionary/{elements → blocks}/registry/block_registry.py +4 -26
- notionary/{elements → blocks}/registry/block_registry_builder.py +26 -25
- notionary/{elements → blocks}/table_element.py +5 -6
- notionary/{elements → blocks}/text_inline_formatter.py +1 -4
- notionary/{elements → blocks}/todo_element.py +5 -6
- notionary/{elements → blocks}/toggle_element.py +3 -5
- notionary/{elements → blocks}/toggleable_heading_element.py +4 -6
- notionary/{elements → blocks}/video_element.py +4 -5
- notionary/database/__init__.py +0 -0
- notionary/database/client.py +132 -0
- notionary/database/database_exceptions.py +13 -0
- notionary/database/factory.py +0 -0
- notionary/database/filter_builder.py +175 -0
- notionary/database/notion_database.py +340 -127
- notionary/database/notion_database_provider.py +230 -0
- notionary/elements/__init__.py +0 -0
- notionary/models/notion_database_response.py +294 -13
- notionary/models/notion_page_response.py +9 -31
- notionary/models/search_response.py +0 -0
- notionary/page/__init__.py +0 -0
- notionary/page/client.py +110 -0
- notionary/page/content/page_content_retriever.py +5 -20
- notionary/page/content/page_content_writer.py +3 -4
- notionary/page/formatting/markdown_to_notion_converter.py +1 -3
- notionary/{prompting → page}/markdown_syntax_prompt_generator.py +1 -2
- notionary/page/notion_page.py +354 -317
- notionary/page/notion_to_markdown_converter.py +1 -4
- notionary/page/properites/property_value_extractor.py +0 -64
- notionary/page/{properites/property_formatter.py → property_formatter.py} +7 -4
- notionary/page/search_filter_builder.py +131 -0
- notionary/page/utils.py +60 -0
- notionary/util/__init__.py +12 -3
- notionary/util/factory_decorator.py +33 -0
- notionary/util/fuzzy_matcher.py +82 -0
- notionary/util/page_id_utils.py +0 -21
- notionary/util/singleton_metaclass.py +22 -0
- notionary/workspace.py +69 -0
- notionary-0.2.15.dist-info/METADATA +223 -0
- notionary-0.2.15.dist-info/RECORD +68 -0
- {notionary-0.2.13.dist-info → notionary-0.2.15.dist-info}/WHEEL +1 -2
- notionary/cli/main.py +0 -347
- notionary/cli/onboarding.py +0 -116
- notionary/database/database_discovery.py +0 -142
- notionary/database/notion_database_factory.py +0 -190
- notionary/exceptions/database_exceptions.py +0 -76
- notionary/exceptions/page_creation_exception.py +0 -9
- notionary/page/metadata/metadata_editor.py +0 -150
- notionary/page/metadata/notion_icon_manager.py +0 -77
- notionary/page/metadata/notion_page_cover_manager.py +0 -56
- notionary/page/notion_page_factory.py +0 -328
- notionary/page/properites/database_property_service.py +0 -302
- notionary/page/properites/page_property_manager.py +0 -152
- notionary/page/relations/notion_page_relation_manager.py +0 -350
- notionary/page/relations/notion_page_title_resolver.py +0 -104
- notionary/page/relations/page_database_relation.py +0 -68
- notionary/util/warn_direct_constructor_usage.py +0 -54
- notionary-0.2.13.dist-info/METADATA +0 -273
- notionary-0.2.13.dist-info/RECORD +0 -67
- notionary-0.2.13.dist-info/entry_points.txt +0 -2
- notionary-0.2.13.dist-info/top_level.txt +0 -1
- /notionary/util/{singleton.py → singleton_decorator.py} +0 -0
- {notionary-0.2.13.dist-info/licenses → notionary-0.2.15.dist-info}/LICENSE +0 -0
@@ -1,44 +1,5 @@
|
|
1
|
-
from dataclasses import field, dataclass
|
2
1
|
from typing import Optional, List, Self
|
3
|
-
|
4
|
-
|
5
|
-
@dataclass
|
6
|
-
class ElementPromptContent:
|
7
|
-
"""
|
8
|
-
Dataclass defining the standardized structure for element prompt content.
|
9
|
-
This ensures consistent formatting across all Notion block elements.
|
10
|
-
"""
|
11
|
-
|
12
|
-
description: str
|
13
|
-
"""Concise explanation of what the element is and its purpose in Notion."""
|
14
|
-
|
15
|
-
syntax: str
|
16
|
-
"""The exact markdown syntax pattern used to create this element."""
|
17
|
-
|
18
|
-
when_to_use: str
|
19
|
-
"""Guidelines explaining the appropriate scenarios for using this element."""
|
20
|
-
|
21
|
-
examples: List[str] = field(default_factory=list)
|
22
|
-
"""List of practical usage examples showing the element in context."""
|
23
|
-
|
24
|
-
avoid: Optional[str] = None
|
25
|
-
"""Optional field listing scenarios when this element should be avoided."""
|
26
|
-
|
27
|
-
is_standard_markdown: bool = False
|
28
|
-
"""Indicates whether this element follows standard Markdown syntax (and does not require full examples)."""
|
29
|
-
|
30
|
-
def __post_init__(self):
|
31
|
-
"""Validates that the content meets minimum requirements."""
|
32
|
-
if not self.description:
|
33
|
-
raise ValueError("Description is required")
|
34
|
-
if not self.syntax:
|
35
|
-
raise ValueError("Syntax is required")
|
36
|
-
if not self.examples and not self.is_standard_markdown:
|
37
|
-
raise ValueError(
|
38
|
-
"At least one example is required unless it's standard markdown."
|
39
|
-
)
|
40
|
-
if not self.when_to_use:
|
41
|
-
raise ValueError("Usage guidelines are required")
|
2
|
+
from notionary.blocks.prompts.element_prompt_content import ElementPromptContent
|
42
3
|
|
43
4
|
|
44
5
|
class ElementPromptBuilder:
|
@@ -0,0 +1,41 @@
|
|
1
|
+
from dataclasses import field, dataclass
|
2
|
+
from typing import Optional, List
|
3
|
+
|
4
|
+
|
5
|
+
@dataclass
|
6
|
+
class ElementPromptContent:
|
7
|
+
"""
|
8
|
+
Dataclass defining the standardized structure for element prompt content.
|
9
|
+
This ensures consistent formatting across all Notion block elements.
|
10
|
+
"""
|
11
|
+
|
12
|
+
description: str
|
13
|
+
"""Concise explanation of what the element is and its purpose in Notion."""
|
14
|
+
|
15
|
+
syntax: str
|
16
|
+
"""The exact markdown syntax pattern used to create this element."""
|
17
|
+
|
18
|
+
when_to_use: str
|
19
|
+
"""Guidelines explaining the appropriate scenarios for using this element."""
|
20
|
+
|
21
|
+
examples: List[str] = field(default_factory=list)
|
22
|
+
"""List of practical usage examples showing the element in context."""
|
23
|
+
|
24
|
+
avoid: Optional[str] = None
|
25
|
+
"""Optional field listing scenarios when this element should be avoided."""
|
26
|
+
|
27
|
+
is_standard_markdown: bool = False
|
28
|
+
"""Indicates whether this element follows standard Markdown syntax (and does not require full examples)."""
|
29
|
+
|
30
|
+
def __post_init__(self):
|
31
|
+
"""Validates that the content meets minimum requirements."""
|
32
|
+
if not self.description:
|
33
|
+
raise ValueError("Description is required")
|
34
|
+
if not self.syntax:
|
35
|
+
raise ValueError("Syntax is required")
|
36
|
+
if not self.examples and not self.is_standard_markdown:
|
37
|
+
raise ValueError(
|
38
|
+
"At least one example is required unless it's standard markdown."
|
39
|
+
)
|
40
|
+
if not self.when_to_use:
|
41
|
+
raise ValueError("Usage guidelines are required")
|
@@ -1,10 +1,9 @@
|
|
1
1
|
import re
|
2
2
|
from typing import Dict, Any, Optional, List, Tuple
|
3
|
-
|
4
|
-
from notionary.
|
5
|
-
|
6
|
-
|
7
|
-
)
|
3
|
+
|
4
|
+
from notionary.blocks import NotionBlockElement
|
5
|
+
from notionary.blocks import ElementPromptContent, ElementPromptBuilder
|
6
|
+
|
8
7
|
|
9
8
|
class QuoteElement(NotionBlockElement):
|
10
9
|
"""Class for converting between Markdown blockquotes and Notion quote blocks."""
|
@@ -1,13 +1,13 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
from typing import Dict, Any, Optional, List, Set, Type
|
3
3
|
|
4
|
-
from notionary.
|
5
|
-
from notionary.
|
4
|
+
from notionary.blocks.notion_block_element import NotionBlockElement
|
5
|
+
from notionary.page.markdown_syntax_prompt_generator import (
|
6
6
|
MarkdownSyntaxPromptGenerator,
|
7
7
|
)
|
8
|
-
from notionary.
|
8
|
+
from notionary.blocks.text_inline_formatter import TextInlineFormatter
|
9
9
|
|
10
|
-
from notionary.
|
10
|
+
from notionary.blocks import NotionBlockElement
|
11
11
|
|
12
12
|
|
13
13
|
class BlockRegistry:
|
@@ -28,28 +28,6 @@ class BlockRegistry:
|
|
28
28
|
for element in elements:
|
29
29
|
self.register(element)
|
30
30
|
|
31
|
-
def to_builder(self):
|
32
|
-
"""
|
33
|
-
Convert this registry to a builder for modifications.
|
34
|
-
Imports only when needed to avoid circular imports.
|
35
|
-
"""
|
36
|
-
from notionary.elements.registry.block_registry_builder import (
|
37
|
-
BlockRegistryBuilder,
|
38
|
-
)
|
39
|
-
|
40
|
-
builder = BlockRegistryBuilder()
|
41
|
-
for element in self._elements:
|
42
|
-
builder.add_element(element)
|
43
|
-
return builder
|
44
|
-
|
45
|
-
@property
|
46
|
-
def builder(self):
|
47
|
-
"""
|
48
|
-
Returns a new builder pre-configured with the current registry elements.
|
49
|
-
Uses lazy import to avoid circular dependencies.
|
50
|
-
"""
|
51
|
-
return self.to_builder()
|
52
|
-
|
53
31
|
def register(self, element_class: Type[NotionBlockElement]) -> bool:
|
54
32
|
"""
|
55
33
|
Register an element class.
|
@@ -1,33 +1,32 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
from typing import List, Type
|
2
|
+
from typing import List, Type, TYPE_CHECKING
|
3
3
|
from collections import OrderedDict
|
4
4
|
|
5
|
-
from notionary.
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
5
|
+
from notionary.blocks import (
|
6
|
+
ParagraphElement,
|
7
|
+
AudioElement,
|
8
|
+
BulletedListElement,
|
9
|
+
CalloutElement,
|
10
|
+
CodeBlockElement,
|
11
|
+
ColumnElement,
|
12
|
+
DividerElement,
|
13
|
+
EmbedElement,
|
14
|
+
HeadingElement,
|
15
|
+
ImageElement,
|
16
|
+
MentionElement,
|
17
|
+
NumberedListElement,
|
18
|
+
TableElement,
|
19
|
+
TodoElement,
|
20
|
+
ToggleElement,
|
21
|
+
ToggleableHeadingElement,
|
22
|
+
VideoElement,
|
23
|
+
BookmarkElement,
|
24
|
+
QuoteElement,
|
25
|
+
NotionBlockElement,
|
16
26
|
)
|
17
27
|
|
18
|
-
|
19
|
-
from notionary.
|
20
|
-
from notionary.elements.callout_element import CalloutElement
|
21
|
-
from notionary.elements.code_block_element import CodeBlockElement
|
22
|
-
from notionary.elements.divider_element import DividerElement
|
23
|
-
from notionary.elements.table_element import TableElement
|
24
|
-
from notionary.elements.todo_element import TodoElement
|
25
|
-
from notionary.elements.qoute_element import QuoteElement
|
26
|
-
from notionary.elements.image_element import ImageElement
|
27
|
-
from notionary.elements.toggleable_heading_element import ToggleableHeadingElement
|
28
|
-
from notionary.elements.video_element import VideoElement
|
29
|
-
from notionary.elements.toggle_element import ToggleElement
|
30
|
-
from notionary.elements.bookmark_element import BookmarkElement
|
28
|
+
if TYPE_CHECKING:
|
29
|
+
from notionary.blocks import BlockRegistry
|
31
30
|
|
32
31
|
|
33
32
|
class BlockRegistryBuilder:
|
@@ -281,6 +280,8 @@ class BlockRegistryBuilder:
|
|
281
280
|
Returns:
|
282
281
|
A configured BlockRegistry instance
|
283
282
|
"""
|
283
|
+
from notionary.blocks import BlockRegistry
|
284
|
+
|
284
285
|
if ParagraphElement.__name__ not in self._elements:
|
285
286
|
self.add_element(ParagraphElement)
|
286
287
|
else:
|
@@ -1,11 +1,10 @@
|
|
1
1
|
import re
|
2
2
|
from typing import Dict, Any, Optional, List, Tuple
|
3
|
-
|
4
|
-
from notionary.
|
5
|
-
from notionary.
|
6
|
-
|
7
|
-
|
8
|
-
)
|
3
|
+
|
4
|
+
from notionary.blocks import NotionBlockElement
|
5
|
+
from notionary.blocks.text_inline_formatter import TextInlineFormatter
|
6
|
+
from notionary.blocks import ElementPromptContent, ElementPromptBuilder
|
7
|
+
|
9
8
|
|
10
9
|
class TableElement(NotionBlockElement):
|
11
10
|
"""
|
@@ -1,10 +1,7 @@
|
|
1
1
|
from typing import Dict, Any, List, Tuple
|
2
2
|
import re
|
3
3
|
|
4
|
-
from notionary.
|
5
|
-
ElementPromptBuilder,
|
6
|
-
ElementPromptContent,
|
7
|
-
)
|
4
|
+
from notionary.blocks import ElementPromptBuilder, ElementPromptContent
|
8
5
|
|
9
6
|
|
10
7
|
class TextInlineFormatter:
|
@@ -1,11 +1,10 @@
|
|
1
1
|
import re
|
2
2
|
from typing import Dict, Any, Optional
|
3
|
-
|
4
|
-
from notionary.
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
from notionary.elements.text_inline_formatter import TextInlineFormatter
|
3
|
+
|
4
|
+
from notionary.blocks import NotionBlockElement
|
5
|
+
from notionary.blocks import ElementPromptContent, ElementPromptBuilder
|
6
|
+
from notionary.blocks.text_inline_formatter import TextInlineFormatter
|
7
|
+
|
9
8
|
|
10
9
|
class TodoElement(NotionBlockElement):
|
11
10
|
"""
|
@@ -1,11 +1,9 @@
|
|
1
1
|
import re
|
2
2
|
from typing import Dict, Any, Optional, List, Tuple, Callable
|
3
3
|
|
4
|
-
from notionary.
|
5
|
-
from notionary.
|
6
|
-
|
7
|
-
ElementPromptContent,
|
8
|
-
)
|
4
|
+
from notionary.blocks import NotionBlockElement
|
5
|
+
from notionary.blocks import ElementPromptContent, ElementPromptBuilder
|
6
|
+
|
9
7
|
|
10
8
|
class ToggleElement(NotionBlockElement):
|
11
9
|
"""
|
@@ -1,12 +1,10 @@
|
|
1
1
|
import re
|
2
2
|
from typing import Dict, Any, Optional, List, Tuple, Callable
|
3
3
|
|
4
|
-
from notionary.
|
5
|
-
from notionary.
|
6
|
-
|
7
|
-
|
8
|
-
)
|
9
|
-
from notionary.elements.text_inline_formatter import TextInlineFormatter
|
4
|
+
from notionary.blocks import NotionBlockElement
|
5
|
+
from notionary.blocks import ElementPromptContent, ElementPromptBuilder
|
6
|
+
from notionary.blocks.text_inline_formatter import TextInlineFormatter
|
7
|
+
|
10
8
|
|
11
9
|
class ToggleableHeadingElement(NotionBlockElement):
|
12
10
|
"""Handles conversion between Markdown collapsible headings and Notion toggleable heading blocks with pipe syntax."""
|
@@ -1,10 +1,9 @@
|
|
1
1
|
import re
|
2
2
|
from typing import Dict, Any, Optional, List
|
3
|
-
|
4
|
-
from notionary.
|
5
|
-
|
6
|
-
|
7
|
-
)
|
3
|
+
|
4
|
+
from notionary.blocks import NotionBlockElement
|
5
|
+
from notionary.blocks import ElementPromptContent, ElementPromptBuilder
|
6
|
+
|
8
7
|
|
9
8
|
class VideoElement(NotionBlockElement):
|
10
9
|
"""
|
File without changes
|
@@ -0,0 +1,132 @@
|
|
1
|
+
from typing import Dict, Any, Optional
|
2
|
+
from dotenv import load_dotenv
|
3
|
+
from notionary.base_notion_client import BaseNotionClient
|
4
|
+
|
5
|
+
from notionary.models.notion_database_response import (
|
6
|
+
NotionDatabaseResponse,
|
7
|
+
NotionDatabaseSearchResponse,
|
8
|
+
NotionPageResponse,
|
9
|
+
NotionQueryDatabaseResponse,
|
10
|
+
)
|
11
|
+
from notionary.util import singleton
|
12
|
+
|
13
|
+
load_dotenv()
|
14
|
+
|
15
|
+
|
16
|
+
@singleton
|
17
|
+
class NotionDatabaseClient(BaseNotionClient):
|
18
|
+
"""
|
19
|
+
Specialized Notion client for database operations.
|
20
|
+
Inherits connection management and HTTP methods from BaseNotionClient.
|
21
|
+
"""
|
22
|
+
|
23
|
+
def __init__(self, token: Optional[str] = None, timeout: int = 30):
|
24
|
+
super().__init__(token, timeout)
|
25
|
+
|
26
|
+
async def get_database(self, database_id: str) -> NotionDatabaseResponse:
|
27
|
+
"""
|
28
|
+
Gets metadata for a Notion database by its ID.
|
29
|
+
"""
|
30
|
+
response = await self.get(f"databases/{database_id}")
|
31
|
+
return NotionDatabaseResponse.model_validate(response)
|
32
|
+
|
33
|
+
async def patch_database(
|
34
|
+
self, database_id: str, data: Dict[str, Any]
|
35
|
+
) -> NotionDatabaseResponse:
|
36
|
+
"""
|
37
|
+
Updates a Notion database with the provided data.
|
38
|
+
"""
|
39
|
+
response = await self.patch(f"databases/{database_id}", data=data)
|
40
|
+
return NotionDatabaseResponse.model_validate(response)
|
41
|
+
|
42
|
+
async def query_database(
|
43
|
+
self, database_id: str, query_data: Dict[str, Any] = None
|
44
|
+
) -> NotionQueryDatabaseResponse:
|
45
|
+
"""
|
46
|
+
Queries a Notion database with the provided filter and sorts.
|
47
|
+
"""
|
48
|
+
response = await self.post(f"databases/{database_id}/query", data=query_data)
|
49
|
+
return NotionQueryDatabaseResponse.model_validate(response)
|
50
|
+
|
51
|
+
async def query_database_by_title(
|
52
|
+
self, database_id: str, page_title: str
|
53
|
+
) -> NotionQueryDatabaseResponse:
|
54
|
+
"""
|
55
|
+
Queries a Notion database by title.
|
56
|
+
"""
|
57
|
+
query_data = {
|
58
|
+
"filter": {"property": "title", "title": {"contains": page_title}}
|
59
|
+
}
|
60
|
+
|
61
|
+
return await self.query_database(database_id=database_id, query_data=query_data)
|
62
|
+
|
63
|
+
async def search_databases(
|
64
|
+
self, query: str = "", sort_ascending: bool = True, limit: int = 100
|
65
|
+
) -> NotionDatabaseSearchResponse:
|
66
|
+
"""
|
67
|
+
Searches for databases in Notion using the search endpoint.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
query: Search query string
|
71
|
+
sort_ascending: Whether to sort in ascending order
|
72
|
+
limit: Maximum number of results to return
|
73
|
+
"""
|
74
|
+
search_data = {
|
75
|
+
"query": query,
|
76
|
+
"filter": {"value": "database", "property": "object"},
|
77
|
+
"sort": {
|
78
|
+
"direction": "ascending" if sort_ascending else "descending",
|
79
|
+
"timestamp": "last_edited_time",
|
80
|
+
},
|
81
|
+
"page_size": limit,
|
82
|
+
}
|
83
|
+
|
84
|
+
response = await self.post("search", search_data)
|
85
|
+
return NotionDatabaseSearchResponse.model_validate(response)
|
86
|
+
|
87
|
+
async def create_page(self, parent_database_id: str) -> NotionPageResponse:
|
88
|
+
"""
|
89
|
+
Creates a new blank page in the given database with minimal properties.
|
90
|
+
"""
|
91
|
+
page_data = {
|
92
|
+
"parent": {"database_id": parent_database_id},
|
93
|
+
"properties": {},
|
94
|
+
}
|
95
|
+
response = await self.post("pages", page_data)
|
96
|
+
return NotionPageResponse.model_validate(response)
|
97
|
+
|
98
|
+
async def update_database_title(
|
99
|
+
self, database_id: str, title: str
|
100
|
+
) -> NotionDatabaseResponse:
|
101
|
+
"""
|
102
|
+
Updates the title of a database.
|
103
|
+
"""
|
104
|
+
data = {"title": [{"text": {"content": title}}]}
|
105
|
+
return await self.patch_database(database_id, data)
|
106
|
+
|
107
|
+
async def update_database_emoji(
|
108
|
+
self, database_id: str, emoji: str
|
109
|
+
) -> NotionDatabaseResponse:
|
110
|
+
"""
|
111
|
+
Updates the emoji/icon of a database.
|
112
|
+
"""
|
113
|
+
data = {"icon": {"type": "emoji", "emoji": emoji}}
|
114
|
+
return await self.patch_database(database_id, data)
|
115
|
+
|
116
|
+
async def update_database_cover_image(
|
117
|
+
self, database_id: str, image_url: str
|
118
|
+
) -> NotionDatabaseResponse:
|
119
|
+
"""
|
120
|
+
Updates the cover image of a database.
|
121
|
+
"""
|
122
|
+
data = {"cover": {"type": "external", "external": {"url": image_url}}}
|
123
|
+
return await self.patch_database(database_id, data)
|
124
|
+
|
125
|
+
async def update_database_external_icon(
|
126
|
+
self, database_id: str, icon_url: str
|
127
|
+
) -> NotionDatabaseResponse:
|
128
|
+
"""
|
129
|
+
Updates the database icon with an external image URL.
|
130
|
+
"""
|
131
|
+
data = {"icon": {"type": "external", "external": {"url": icon_url}}}
|
132
|
+
return await self.patch_database(database_id, data)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class NotionDatabaseException(Exception):
|
2
|
+
"""Base exception for all Notion database operations."""
|
3
|
+
|
4
|
+
pass
|
5
|
+
|
6
|
+
|
7
|
+
class DatabaseNotFoundException(NotionDatabaseException):
|
8
|
+
"""Exception raised when a database is not found."""
|
9
|
+
|
10
|
+
def __init__(self, identifier: str, message: str = None):
|
11
|
+
self.identifier = identifier
|
12
|
+
self.message = message or f"Database not found: {identifier}"
|
13
|
+
super().__init__(self.message)
|
File without changes
|
@@ -0,0 +1,175 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Any, Dict, List
|
4
|
+
from datetime import datetime, timedelta
|
5
|
+
from dataclasses import dataclass, field
|
6
|
+
|
7
|
+
|
8
|
+
@dataclass
|
9
|
+
class FilterConfig:
|
10
|
+
"""Einfache Konfiguration für Notion Database Filter."""
|
11
|
+
|
12
|
+
conditions: List[Dict[str, Any]] = field(default_factory=list)
|
13
|
+
page_size: int = 100
|
14
|
+
|
15
|
+
def to_filter_dict(self) -> Dict[str, Any]:
|
16
|
+
"""Konvertiert zu einem Notion-Filter-Dictionary."""
|
17
|
+
if len(self.conditions) == 0:
|
18
|
+
return {}
|
19
|
+
if len(self.conditions) == 1:
|
20
|
+
return self.conditions[0]
|
21
|
+
|
22
|
+
return {"and": self.conditions}
|
23
|
+
|
24
|
+
|
25
|
+
class FilterBuilder:
|
26
|
+
"""
|
27
|
+
Builder class for creating complex Notion filters with comprehensive property type support.
|
28
|
+
"""
|
29
|
+
|
30
|
+
def __init__(self, config: FilterConfig = None):
|
31
|
+
self.config = config or FilterConfig()
|
32
|
+
|
33
|
+
def with_page_object_filter(self) -> FilterBuilder:
|
34
|
+
"""Filter: Nur Datenbank-Objekte (Notion API search)."""
|
35
|
+
self.config.conditions.append({"value": "page", "property": "object"})
|
36
|
+
return self
|
37
|
+
|
38
|
+
def with_database_object_filter(self) -> FilterBuilder:
|
39
|
+
"""Filter: Nur Datenbank-Objekte (Notion API search)."""
|
40
|
+
self.config.conditions.append({"value": "database", "property": "object"})
|
41
|
+
return self
|
42
|
+
|
43
|
+
# TIMESTAMP FILTERS (Created/Updated)
|
44
|
+
def with_created_after(self, date: datetime) -> FilterBuilder:
|
45
|
+
"""Add condition: created after specific date."""
|
46
|
+
self.config.conditions.append(
|
47
|
+
{"timestamp": "created_time", "created_time": {"after": date.isoformat()}}
|
48
|
+
)
|
49
|
+
return self
|
50
|
+
|
51
|
+
def with_created_before(self, date: datetime) -> FilterBuilder:
|
52
|
+
"""Add condition: created before specific date."""
|
53
|
+
self.config.conditions.append(
|
54
|
+
{"timestamp": "created_time", "created_time": {"before": date.isoformat()}}
|
55
|
+
)
|
56
|
+
return self
|
57
|
+
|
58
|
+
def with_updated_after(self, date: datetime) -> FilterBuilder:
|
59
|
+
"""Add condition: updated after specific date."""
|
60
|
+
self.config.conditions.append(
|
61
|
+
{
|
62
|
+
"timestamp": "last_edited_time",
|
63
|
+
"last_edited_time": {"after": date.isoformat()},
|
64
|
+
}
|
65
|
+
)
|
66
|
+
return self
|
67
|
+
|
68
|
+
def with_created_last_n_days(self, days: int) -> FilterBuilder:
|
69
|
+
"""In den letzten N Tagen erstellt."""
|
70
|
+
cutoff = datetime.now() - timedelta(days=days)
|
71
|
+
return self.with_created_after(cutoff)
|
72
|
+
|
73
|
+
def with_updated_last_n_hours(self, hours: int) -> FilterBuilder:
|
74
|
+
"""In den letzten N Stunden bearbeitet."""
|
75
|
+
cutoff = datetime.now() - timedelta(hours=hours)
|
76
|
+
return self.with_updated_after(cutoff)
|
77
|
+
|
78
|
+
# RICH TEXT FILTERS
|
79
|
+
def with_text_contains(self, property_name: str, value: str) -> FilterBuilder:
|
80
|
+
"""Rich text contains value."""
|
81
|
+
self.config.conditions.append(
|
82
|
+
{"property": property_name, "rich_text": {"contains": value}}
|
83
|
+
)
|
84
|
+
return self
|
85
|
+
|
86
|
+
def with_text_equals(self, property_name: str, value: str) -> FilterBuilder:
|
87
|
+
"""Rich text equals value."""
|
88
|
+
self.config.conditions.append(
|
89
|
+
{"property": property_name, "rich_text": {"equals": value}}
|
90
|
+
)
|
91
|
+
return self
|
92
|
+
|
93
|
+
# TITLE FILTERS
|
94
|
+
def with_title_contains(self, value: str) -> FilterBuilder:
|
95
|
+
"""Title contains value."""
|
96
|
+
self.config.conditions.append(
|
97
|
+
{"property": "title", "title": {"contains": value}}
|
98
|
+
)
|
99
|
+
return self
|
100
|
+
|
101
|
+
def with_title_equals(self, value: str) -> FilterBuilder:
|
102
|
+
"""Title equals value."""
|
103
|
+
self.config.conditions.append({"property": "title", "title": {"equals": value}})
|
104
|
+
return self
|
105
|
+
|
106
|
+
# SELECT FILTERS (Single Select)
|
107
|
+
def with_select_equals(self, property_name: str, value: str) -> FilterBuilder:
|
108
|
+
"""Select equals value."""
|
109
|
+
self.config.conditions.append(
|
110
|
+
{"property": property_name, "select": {"equals": value}}
|
111
|
+
)
|
112
|
+
return self
|
113
|
+
|
114
|
+
def with_select_is_empty(self, property_name: str) -> FilterBuilder:
|
115
|
+
"""Select is empty."""
|
116
|
+
self.config.conditions.append(
|
117
|
+
{"property": property_name, "select": {"is_empty": True}}
|
118
|
+
)
|
119
|
+
return self
|
120
|
+
|
121
|
+
def with_multi_select_contains(
|
122
|
+
self, property_name: str, value: str
|
123
|
+
) -> FilterBuilder:
|
124
|
+
"""Multi-select contains value."""
|
125
|
+
self.config.conditions.append(
|
126
|
+
{"property": property_name, "multi_select": {"contains": value}}
|
127
|
+
)
|
128
|
+
return self
|
129
|
+
|
130
|
+
def with_status_equals(self, property_name: str, value: str) -> FilterBuilder:
|
131
|
+
"""Status equals value."""
|
132
|
+
self.config.conditions.append(
|
133
|
+
{"property": property_name, "status": {"equals": value}}
|
134
|
+
)
|
135
|
+
return self
|
136
|
+
|
137
|
+
def with_page_size(self, size: int) -> FilterBuilder:
|
138
|
+
"""Set page size for pagination."""
|
139
|
+
self.config.page_size = size
|
140
|
+
return self
|
141
|
+
|
142
|
+
def with_or_condition(self, *builders: FilterBuilder) -> FilterBuilder:
|
143
|
+
"""Add OR condition with multiple sub-conditions."""
|
144
|
+
or_conditions = []
|
145
|
+
for builder in builders:
|
146
|
+
filter_dict = builder.build()
|
147
|
+
if filter_dict:
|
148
|
+
or_conditions.append(filter_dict)
|
149
|
+
|
150
|
+
if len(or_conditions) > 1:
|
151
|
+
self.config.conditions.append({"or": or_conditions})
|
152
|
+
elif len(or_conditions) == 1:
|
153
|
+
self.config.conditions.append(or_conditions[0])
|
154
|
+
|
155
|
+
return self
|
156
|
+
|
157
|
+
def build(self) -> Dict[str, Any]:
|
158
|
+
"""Build the final filter dictionary."""
|
159
|
+
return self.config.to_filter_dict()
|
160
|
+
|
161
|
+
def get_config(self) -> FilterConfig:
|
162
|
+
"""Get the underlying FilterConfig."""
|
163
|
+
return self.config
|
164
|
+
|
165
|
+
def copy(self) -> FilterBuilder:
|
166
|
+
"""Create a copy of the builder."""
|
167
|
+
new_config = FilterConfig(
|
168
|
+
conditions=self.config.conditions.copy(), page_size=self.config.page_size
|
169
|
+
)
|
170
|
+
return FilterBuilder(new_config)
|
171
|
+
|
172
|
+
def reset(self) -> FilterBuilder:
|
173
|
+
"""Reset all conditions."""
|
174
|
+
self.config = FilterConfig()
|
175
|
+
return self
|