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.
Files changed (83) 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/database/__init__.py +0 -0
  31. notionary/database/client.py +132 -0
  32. notionary/database/database_exceptions.py +13 -0
  33. notionary/database/factory.py +0 -0
  34. notionary/database/filter_builder.py +175 -0
  35. notionary/database/notion_database.py +340 -127
  36. notionary/database/notion_database_provider.py +230 -0
  37. notionary/elements/__init__.py +0 -0
  38. notionary/models/notion_database_response.py +294 -13
  39. notionary/models/notion_page_response.py +9 -31
  40. notionary/models/search_response.py +0 -0
  41. notionary/page/__init__.py +0 -0
  42. notionary/page/client.py +110 -0
  43. notionary/page/content/page_content_retriever.py +5 -20
  44. notionary/page/content/page_content_writer.py +3 -4
  45. notionary/page/formatting/markdown_to_notion_converter.py +1 -3
  46. notionary/{prompting → page}/markdown_syntax_prompt_generator.py +1 -2
  47. notionary/page/notion_page.py +354 -317
  48. notionary/page/notion_to_markdown_converter.py +1 -4
  49. notionary/page/properites/property_value_extractor.py +0 -64
  50. notionary/page/{properites/property_formatter.py → property_formatter.py} +7 -4
  51. notionary/page/search_filter_builder.py +131 -0
  52. notionary/page/utils.py +60 -0
  53. notionary/util/__init__.py +12 -3
  54. notionary/util/factory_decorator.py +33 -0
  55. notionary/util/fuzzy_matcher.py +82 -0
  56. notionary/util/page_id_utils.py +0 -21
  57. notionary/util/singleton_metaclass.py +22 -0
  58. notionary/workspace.py +69 -0
  59. notionary-0.2.15.dist-info/METADATA +223 -0
  60. notionary-0.2.15.dist-info/RECORD +68 -0
  61. {notionary-0.2.13.dist-info → notionary-0.2.15.dist-info}/WHEEL +1 -2
  62. notionary/cli/main.py +0 -347
  63. notionary/cli/onboarding.py +0 -116
  64. notionary/database/database_discovery.py +0 -142
  65. notionary/database/notion_database_factory.py +0 -190
  66. notionary/exceptions/database_exceptions.py +0 -76
  67. notionary/exceptions/page_creation_exception.py +0 -9
  68. notionary/page/metadata/metadata_editor.py +0 -150
  69. notionary/page/metadata/notion_icon_manager.py +0 -77
  70. notionary/page/metadata/notion_page_cover_manager.py +0 -56
  71. notionary/page/notion_page_factory.py +0 -328
  72. notionary/page/properites/database_property_service.py +0 -302
  73. notionary/page/properites/page_property_manager.py +0 -152
  74. notionary/page/relations/notion_page_relation_manager.py +0 -350
  75. notionary/page/relations/notion_page_title_resolver.py +0 -104
  76. notionary/page/relations/page_database_relation.py +0 -68
  77. notionary/util/warn_direct_constructor_usage.py +0 -54
  78. notionary-0.2.13.dist-info/METADATA +0 -273
  79. notionary-0.2.13.dist-info/RECORD +0 -67
  80. notionary-0.2.13.dist-info/entry_points.txt +0 -2
  81. notionary-0.2.13.dist-info/top_level.txt +0 -1
  82. /notionary/util/{singleton.py → singleton_decorator.py} +0 -0
  83. {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
- from notionary.elements.notion_block_element import NotionBlockElement
4
- from notionary.prompting.element_prompt_content import (
5
- ElementPromptBuilder,
6
- ElementPromptContent,
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.elements.notion_block_element import NotionBlockElement
5
- from notionary.prompting.markdown_syntax_prompt_generator import (
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.elements.text_inline_formatter import TextInlineFormatter
8
+ from notionary.blocks.text_inline_formatter import TextInlineFormatter
9
9
 
10
- from notionary.elements.notion_block_element import NotionBlockElement
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.elements.column_element import ColumnElement
6
- from notionary.elements.notion_block_element import NotionBlockElement
7
-
8
- from notionary.elements.audio_element import AudioElement
9
- from notionary.elements.bulleted_list_element import BulletedListElement
10
- from notionary.elements.embed_element import EmbedElement
11
- from notionary.elements.mention_element import MentionElement
12
- from notionary.elements.notion_block_element import NotionBlockElement
13
- from notionary.elements.numbered_list_element import NumberedListElement
14
- from notionary.elements.registry.block_registry import (
15
- BlockRegistry,
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
- from notionary.elements.paragraph_element import ParagraphElement
19
- from notionary.elements.heading_element import HeadingElement
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
- from notionary.elements.notion_block_element import NotionBlockElement
4
- from notionary.elements.text_inline_formatter import TextInlineFormatter
5
- from notionary.prompting.element_prompt_content import (
6
- ElementPromptBuilder,
7
- ElementPromptContent,
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.prompting.element_prompt_content import (
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
- from notionary.elements.notion_block_element import NotionBlockElement
4
- from notionary.prompting.element_prompt_content import (
5
- ElementPromptBuilder,
6
- ElementPromptContent,
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.elements.notion_block_element import NotionBlockElement
5
- from notionary.prompting.element_prompt_content import (
6
- ElementPromptBuilder,
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.elements.notion_block_element import NotionBlockElement
5
- from notionary.prompting.element_prompt_content import (
6
- ElementPromptBuilder,
7
- ElementPromptContent,
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
- from notionary.elements.notion_block_element import NotionBlockElement
4
- from notionary.prompting.element_prompt_content import (
5
- ElementPromptBuilder,
6
- ElementPromptContent,
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