notionary 0.1.11__tar.gz → 0.1.12__tar.gz
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-0.1.11 → notionary-0.1.12}/PKG-INFO +1 -1
- notionary-0.1.12/notionary/__init__.py +20 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/audio_element.py +6 -4
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/embed_element.py +2 -4
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/toggle_element.py +28 -20
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/markdown_to_notion_converter.py +70 -109
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/registry/block_element_registry.py +3 -3
- notionary-0.1.12/notionary/core/database/database_discovery.py +140 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/database/notion_database_manager.py +26 -49
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/database/notion_database_manager_factory.py +10 -4
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/notion_client.py +4 -2
- notionary-0.1.12/notionary/core/page/content/notion_page_content_chunker.py +84 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/content/page_content_manager.py +26 -8
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/metadata/metadata_editor.py +57 -44
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/metadata/notion_icon_manager.py +9 -11
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/metadata/notion_page_cover_manager.py +15 -20
- notionary-0.1.12/notionary/core/page/notion_page_manager.py +312 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/properites/database_property_service.py +114 -98
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/properites/page_property_manager.py +78 -49
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/properites/property_formatter.py +1 -1
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/properites/property_operation_result.py +43 -30
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/properites/property_value_extractor.py +26 -8
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/relations/notion_page_relation_manager.py +71 -52
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/relations/notion_page_title_resolver.py +11 -11
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/relations/page_database_relation.py +14 -14
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/relations/relation_operation_result.py +50 -41
- {notionary-0.1.11 → notionary-0.1.12}/notionary/util/page_id_utils.py +11 -7
- {notionary-0.1.11 → notionary-0.1.12}/notionary.egg-info/PKG-INFO +1 -1
- {notionary-0.1.11 → notionary-0.1.12}/notionary.egg-info/SOURCES.txt +2 -1
- {notionary-0.1.11 → notionary-0.1.12}/setup.py +2 -2
- notionary-0.1.11/notionary/__init__.py +0 -9
- notionary-0.1.11/notionary/core/database/notion_database_schema.py +0 -104
- notionary-0.1.11/notionary/core/page/notion_page_manager.py +0 -322
- {notionary-0.1.11 → notionary-0.1.12}/LICENSE +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/README.md +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/__init__.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/bookmark_element.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/callout_element.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/code_block_element.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/column_element.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/divider_element.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/heading_element.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/image_element.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/list_element.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/notion_block_element.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/paragraph_element.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/qoute_element.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/table_element.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/text_inline_formatter.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/todo_lists.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/video_element.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/notion_to_markdown_converter.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/registry/block_element_registry_builder.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/database/database_info_service.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/core/database/models/page_result.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/exceptions/database_exceptions.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/exceptions/page_creation_exception.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/util/logging_mixin.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary/util/singleton_decorator.py +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary.egg-info/dependency_links.txt +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary.egg-info/requires.txt +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/notionary.egg-info/top_level.txt +0 -0
- {notionary-0.1.11 → notionary-0.1.12}/setup.cfg +0 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
from .core.notion_client import NotionClient
|
2
|
+
|
3
|
+
from .core.database.notion_database_manager import NotionDatabaseManager
|
4
|
+
from .core.database.notion_database_manager_factory import NotionDatabaseFactory
|
5
|
+
from .core.database.database_discovery import DatabaseDiscovery
|
6
|
+
|
7
|
+
from .core.page.notion_page_manager import NotionPageManager
|
8
|
+
|
9
|
+
from .core.converters.registry.block_element_registry import BlockElementRegistry
|
10
|
+
from .core.converters.registry.block_element_registry_builder import BlockElementRegistryBuilder
|
11
|
+
|
12
|
+
__all__ = [
|
13
|
+
"NotionClient",
|
14
|
+
"NotionDatabaseManager",
|
15
|
+
"NotionDatabaseFactory",
|
16
|
+
"DatabaseDiscovery",
|
17
|
+
"NotionPageManager",
|
18
|
+
"BlockElementRegistry",
|
19
|
+
"BlockElementRegistryBuilder",
|
20
|
+
]
|
@@ -39,9 +39,11 @@ class AudioElement(NotionBlockElement):
|
|
39
39
|
@staticmethod
|
40
40
|
def is_audio_url(url: str) -> bool:
|
41
41
|
"""Check if URL points to an audio file."""
|
42
|
-
return
|
43
|
-
|
44
|
-
|
42
|
+
return (
|
43
|
+
any(url.lower().endswith(ext) for ext in AudioElement.AUDIO_EXTENSIONS)
|
44
|
+
or "audio" in url.lower()
|
45
|
+
or "storage.googleapis.com/audio_summaries" in url.lower()
|
46
|
+
)
|
45
47
|
|
46
48
|
@staticmethod
|
47
49
|
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
@@ -138,4 +140,4 @@ class AudioElement(NotionBlockElement):
|
|
138
140
|
"$[Voice recording](https://example.com/audio/recording.mp3)",
|
139
141
|
"$[](https://storage.googleapis.com/audio_summaries/example.mp3)",
|
140
142
|
],
|
141
|
-
}
|
143
|
+
}
|
@@ -16,9 +16,7 @@ class EmbedElement(NotionBlockElement):
|
|
16
16
|
"""
|
17
17
|
|
18
18
|
PATTERN = re.compile(
|
19
|
-
r"^<embed(?:\:(.*?))?>(?:\s*)"
|
20
|
-
+ r'\((https?://[^\s"]+)'
|
21
|
-
+ r"\)$"
|
19
|
+
r"^<embed(?:\:(.*?))?>(?:\s*)" + r'\((https?://[^\s"]+)' + r"\)$"
|
22
20
|
)
|
23
21
|
|
24
22
|
@staticmethod
|
@@ -126,4 +124,4 @@ class EmbedElement(NotionBlockElement):
|
|
126
124
|
"<embed:Project documentation>(https://github.com/username/repo)",
|
127
125
|
"<embed>(https://example.com/important-reference.pdf)",
|
128
126
|
],
|
129
|
-
}
|
127
|
+
}
|
@@ -6,20 +6,15 @@ from notionary.core.converters.elements.notion_block_element import NotionBlockE
|
|
6
6
|
|
7
7
|
class ToggleElement(NotionBlockElement):
|
8
8
|
"""
|
9
|
-
|
10
|
-
|
11
|
-
Markdown toggle syntax:
|
12
|
-
+++ Toggle title
|
13
|
-
Indented content that belongs to the toggle
|
14
|
-
More indented content
|
15
|
-
|
16
|
-
Non-indented content marks the end of the toggle block.
|
9
|
+
Verbesserte ToggleElement-Klasse, die Kontext berücksichtigt.
|
17
10
|
"""
|
18
11
|
|
19
12
|
TOGGLE_PATTERN = re.compile(r"^[+]{3}\s+(.+)$")
|
20
|
-
|
21
13
|
INDENT_PATTERN = re.compile(r"^(\s{2,}|\t+)(.+)$")
|
22
14
|
|
15
|
+
# Ein neues Pattern, um spezifisch nach der "Transcript" Überschrift zu suchen
|
16
|
+
TRANSCRIPT_TOGGLE_PATTERN = re.compile(r"^[+]{3}\s+Transcript$")
|
17
|
+
|
23
18
|
@staticmethod
|
24
19
|
def match_markdown(text: str) -> bool:
|
25
20
|
"""Check if text is a markdown toggle."""
|
@@ -32,11 +27,7 @@ class ToggleElement(NotionBlockElement):
|
|
32
27
|
|
33
28
|
@staticmethod
|
34
29
|
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
35
|
-
"""Convert markdown toggle to Notion toggle block.
|
36
|
-
|
37
|
-
Note: This method only converts the toggle title line.
|
38
|
-
The nested content needs to be processed separately.
|
39
|
-
"""
|
30
|
+
"""Convert markdown toggle to Notion toggle block."""
|
40
31
|
toggle_match = ToggleElement.TOGGLE_PATTERN.match(text.strip())
|
41
32
|
if not toggle_match:
|
42
33
|
return None
|
@@ -149,18 +140,21 @@ class ToggleElement(NotionBlockElement):
|
|
149
140
|
|
150
141
|
@classmethod
|
151
142
|
def find_matches(
|
152
|
-
cls,
|
143
|
+
cls,
|
144
|
+
text: str,
|
145
|
+
process_nested_content: Callable = None,
|
146
|
+
context_aware: bool = True,
|
153
147
|
) -> List[Tuple[int, int, Dict[str, Any]]]:
|
154
148
|
"""
|
155
|
-
|
149
|
+
Verbesserte find_matches-Methode, die Kontext beim Finden von Toggles berücksichtigt.
|
156
150
|
|
157
151
|
Args:
|
158
|
-
text:
|
159
|
-
process_nested_content:
|
160
|
-
|
152
|
+
text: Der zu durchsuchende Text
|
153
|
+
process_nested_content: Optionale Callback-Funktion zur Verarbeitung verschachtelter Inhalte
|
154
|
+
context_aware: Ob der Kontext (vorhergehende Zeilen) beim Finden von Toggles berücksichtigt werden soll
|
161
155
|
|
162
156
|
Returns:
|
163
|
-
|
157
|
+
Liste von (start_pos, end_pos, block) Tupeln
|
164
158
|
"""
|
165
159
|
if not text:
|
166
160
|
return []
|
@@ -177,6 +171,20 @@ class ToggleElement(NotionBlockElement):
|
|
177
171
|
i += 1
|
178
172
|
continue
|
179
173
|
|
174
|
+
# Wenn context_aware aktiviert ist, prüfen wir für "Transcript"-Toggles
|
175
|
+
# ob sie direkt nach einem Bullet Point kommen
|
176
|
+
is_transcript_toggle = cls.TRANSCRIPT_TOGGLE_PATTERN.match(line.strip())
|
177
|
+
|
178
|
+
if context_aware and is_transcript_toggle:
|
179
|
+
# Prüfen, ob der Toggle in einem gültigen Kontext ist (nach Bullet Point)
|
180
|
+
if i > 0 and lines[i - 1].strip().startswith("- "):
|
181
|
+
# Gültiger Kontext, fahre fort
|
182
|
+
pass
|
183
|
+
else:
|
184
|
+
# Ungültiger Kontext für Transcript-Toggle, überspringe ihn
|
185
|
+
i += 1
|
186
|
+
continue
|
187
|
+
|
180
188
|
start_pos = 0
|
181
189
|
for j in range(i):
|
182
190
|
start_pos += len(lines[j]) + 1
|
{notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/markdown_to_notion_converter.py
RENAMED
@@ -12,6 +12,8 @@ class MarkdownToNotionConverter:
|
|
12
12
|
SPACER_MARKER = "<!-- spacer -->"
|
13
13
|
MULTILINE_CONTENT_MARKER = "<!-- REMOVED_MULTILINE_CONTENT -->"
|
14
14
|
TOGGLE_MARKER = "<!-- toggle_content -->"
|
15
|
+
TOGGLE_MARKER_PREFIX = "<!-- toggle_"
|
16
|
+
TOGGLE_MARKER_SUFFIX = " -->"
|
15
17
|
|
16
18
|
def __init__(self, block_registry: Optional[BlockElementRegistry] = None):
|
17
19
|
"""
|
@@ -46,38 +48,48 @@ class MarkdownToNotionConverter:
|
|
46
48
|
if not markdown_text:
|
47
49
|
return []
|
48
50
|
|
49
|
-
#
|
50
|
-
|
51
|
+
# We'll process all blocks in order, preserving their original positions
|
52
|
+
all_blocks = []
|
53
|
+
|
54
|
+
# First, identify all toggle blocks
|
55
|
+
toggle_blocks = self._identify_toggle_blocks(markdown_text)
|
56
|
+
|
57
|
+
# If we have toggles, process them and extract positions
|
58
|
+
if toggle_blocks:
|
59
|
+
all_blocks.extend(toggle_blocks)
|
51
60
|
|
52
61
|
# Process other multiline elements
|
53
|
-
|
54
|
-
|
55
|
-
|
62
|
+
multiline_blocks = self._identify_multiline_blocks(markdown_text, toggle_blocks)
|
63
|
+
if multiline_blocks:
|
64
|
+
all_blocks.extend(multiline_blocks)
|
56
65
|
|
57
66
|
# Process remaining text line by line
|
58
|
-
line_blocks = self._process_text_lines(
|
67
|
+
line_blocks = self._process_text_lines(
|
68
|
+
markdown_text, toggle_blocks + multiline_blocks
|
69
|
+
)
|
70
|
+
if line_blocks:
|
71
|
+
all_blocks.extend(line_blocks)
|
59
72
|
|
60
|
-
#
|
61
|
-
all_blocks = toggle_blocks + multiline_blocks + line_blocks
|
73
|
+
# Sort all blocks by their position in the text
|
62
74
|
all_blocks.sort(key=lambda x: x[0])
|
63
75
|
|
64
|
-
# Extract just the blocks
|
76
|
+
# Extract just the blocks without position information
|
65
77
|
blocks = [block for _, _, block in all_blocks]
|
66
78
|
|
67
79
|
# Process spacing between blocks
|
68
80
|
return self._process_block_spacing(blocks)
|
69
81
|
|
70
|
-
def
|
82
|
+
def _identify_toggle_blocks(
|
71
83
|
self, text: str
|
72
|
-
) ->
|
84
|
+
) -> List[Tuple[int, int, Dict[str, Any]]]:
|
73
85
|
"""
|
74
|
-
|
86
|
+
Identify all toggle blocks in the text without replacing them.
|
75
87
|
|
76
88
|
Args:
|
77
89
|
text: The text to process
|
78
90
|
|
79
91
|
Returns:
|
80
|
-
|
92
|
+
List of (start_pos, end_pos, block) tuples
|
81
93
|
"""
|
82
94
|
# Find toggle element in registry
|
83
95
|
toggle_element = None
|
@@ -91,67 +103,28 @@ class MarkdownToNotionConverter:
|
|
91
103
|
break
|
92
104
|
|
93
105
|
if not toggle_element:
|
94
|
-
|
95
|
-
return text, []
|
106
|
+
return []
|
96
107
|
|
97
|
-
# Use the find_matches method
|
108
|
+
# Use the find_matches method with context awareness
|
98
109
|
# Pass the converter's convert method as a callback to process nested content
|
99
|
-
toggle_blocks = toggle_element.find_matches(
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
# Create a processed text with toggle markers
|
105
|
-
lines = text.split("\n")
|
106
|
-
processed_lines = lines.copy()
|
107
|
-
|
108
|
-
# Replace toggle content with markers
|
109
|
-
for start_pos, end_pos, _ in reversed(toggle_blocks):
|
110
|
-
# Calculate line indices for this toggle
|
111
|
-
start_line_index = 0
|
112
|
-
current_pos = 0
|
113
|
-
for i, line in enumerate(lines):
|
114
|
-
line_length = len(line) + 1 # +1 for newline
|
115
|
-
if current_pos <= start_pos < current_pos + line_length:
|
116
|
-
start_line_index = i
|
117
|
-
break
|
118
|
-
current_pos += line_length
|
119
|
-
|
120
|
-
end_line_index = start_line_index
|
121
|
-
current_pos = 0
|
122
|
-
for i, line in enumerate(lines):
|
123
|
-
line_length = len(line) + 1 # +1 for newline
|
124
|
-
if current_pos <= end_pos < current_pos + line_length:
|
125
|
-
end_line_index = i
|
126
|
-
break
|
127
|
-
current_pos += line_length
|
128
|
-
|
129
|
-
# Replace toggle content with markers
|
130
|
-
num_lines = end_line_index - start_line_index + 1
|
131
|
-
for i in range(start_line_index, start_line_index + num_lines):
|
132
|
-
processed_lines[i] = self.TOGGLE_MARKER
|
133
|
-
|
134
|
-
processed_text = "\n".join(processed_lines)
|
135
|
-
return processed_text, toggle_blocks
|
110
|
+
toggle_blocks = toggle_element.find_matches(
|
111
|
+
text, self.convert, context_aware=True
|
112
|
+
)
|
113
|
+
return toggle_blocks
|
136
114
|
|
137
|
-
def
|
138
|
-
self, text: str
|
139
|
-
) ->
|
115
|
+
def _identify_multiline_blocks(
|
116
|
+
self, text: str, exclude_blocks: List[Tuple[int, int, Dict[str, Any]]]
|
117
|
+
) -> List[Tuple[int, int, Dict[str, Any]]]:
|
140
118
|
"""
|
141
|
-
|
119
|
+
Identify all multiline blocks (except toggle blocks) without altering the text.
|
142
120
|
|
143
121
|
Args:
|
144
122
|
text: The text to process
|
123
|
+
exclude_blocks: Blocks to exclude (e.g., already identified toggle blocks)
|
145
124
|
|
146
125
|
Returns:
|
147
|
-
|
126
|
+
List of (start_pos, end_pos, block) tuples
|
148
127
|
"""
|
149
|
-
if not text:
|
150
|
-
return text, []
|
151
|
-
|
152
|
-
multiline_blocks = []
|
153
|
-
processed_text = text
|
154
|
-
|
155
128
|
# Get all multiline elements except ToggleElement
|
156
129
|
multiline_elements = [
|
157
130
|
element
|
@@ -160,51 +133,45 @@ class MarkdownToNotionConverter:
|
|
160
133
|
]
|
161
134
|
|
162
135
|
if not multiline_elements:
|
163
|
-
return
|
136
|
+
return []
|
164
137
|
|
138
|
+
# Create a set of ranges to exclude
|
139
|
+
exclude_ranges = set()
|
140
|
+
for start, end, _ in exclude_blocks:
|
141
|
+
exclude_ranges.update(range(start, end + 1))
|
142
|
+
|
143
|
+
multiline_blocks = []
|
165
144
|
for element in multiline_elements:
|
166
145
|
if not hasattr(element, "find_matches"):
|
167
146
|
continue
|
168
147
|
|
169
|
-
# Find all matches for this element
|
148
|
+
# Find all matches for this element
|
170
149
|
if hasattr(element, "set_converter_callback"):
|
171
|
-
matches = element.find_matches(
|
150
|
+
matches = element.find_matches(text, self.convert)
|
172
151
|
else:
|
173
|
-
matches = element.find_matches(
|
152
|
+
matches = element.find_matches(text)
|
174
153
|
|
175
154
|
if not matches:
|
176
155
|
continue
|
177
156
|
|
178
|
-
|
157
|
+
# Add only blocks that don't overlap with excluded ranges
|
158
|
+
for start, end, block in matches:
|
159
|
+
# Check if this block overlaps with any excluded range
|
160
|
+
if any(start <= i <= end for i in exclude_ranges):
|
161
|
+
continue
|
162
|
+
multiline_blocks.append((start, end, block))
|
179
163
|
|
180
|
-
|
181
|
-
processed_text = self._replace_matched_content_with_markers(
|
182
|
-
processed_text, matches
|
183
|
-
)
|
184
|
-
|
185
|
-
return processed_text, multiline_blocks
|
186
|
-
|
187
|
-
def _replace_matched_content_with_markers(
|
188
|
-
self, text: str, matches: List[Tuple[int, int, Dict[str, Any]]]
|
189
|
-
) -> str:
|
190
|
-
"""Replace matched content with marker placeholders to preserve line structure."""
|
191
|
-
for start, end, _ in reversed(matches):
|
192
|
-
num_newlines = text[start:end].count("\n")
|
193
|
-
text = (
|
194
|
-
text[:start]
|
195
|
-
+ "\n"
|
196
|
-
+ self.MULTILINE_CONTENT_MARKER
|
197
|
-
+ "\n" * num_newlines
|
198
|
-
+ text[end:]
|
199
|
-
)
|
200
|
-
return text
|
164
|
+
return multiline_blocks
|
201
165
|
|
202
|
-
def _process_text_lines(
|
166
|
+
def _process_text_lines(
|
167
|
+
self, text: str, exclude_blocks: List[Tuple[int, int, Dict[str, Any]]]
|
168
|
+
) -> List[Tuple[int, int, Dict[str, Any]]]:
|
203
169
|
"""
|
204
|
-
Process text line by line
|
170
|
+
Process text line by line, excluding ranges already processed.
|
205
171
|
|
206
172
|
Args:
|
207
173
|
text: The text to process
|
174
|
+
exclude_blocks: Blocks to exclude (e.g., already identified toggle and multiline blocks)
|
208
175
|
|
209
176
|
Returns:
|
210
177
|
List of (start_pos, end_pos, block) tuples
|
@@ -212,6 +179,11 @@ class MarkdownToNotionConverter:
|
|
212
179
|
if not text:
|
213
180
|
return []
|
214
181
|
|
182
|
+
# Create a set of excluded positions
|
183
|
+
exclude_positions = set()
|
184
|
+
for start, end, _ in exclude_blocks:
|
185
|
+
exclude_positions.update(range(start, end + 1))
|
186
|
+
|
215
187
|
line_blocks = []
|
216
188
|
lines = text.split("\n")
|
217
189
|
|
@@ -222,9 +194,10 @@ class MarkdownToNotionConverter:
|
|
222
194
|
|
223
195
|
for line in lines:
|
224
196
|
line_length = len(line) + 1 # +1 for newline
|
197
|
+
line_end = current_pos + line_length - 1
|
225
198
|
|
226
|
-
# Skip
|
227
|
-
if
|
199
|
+
# Skip lines that are part of excluded blocks
|
200
|
+
if any(current_pos <= pos <= line_end for pos in exclude_positions):
|
228
201
|
current_pos += line_length
|
229
202
|
continue
|
230
203
|
|
@@ -233,7 +206,7 @@ class MarkdownToNotionConverter:
|
|
233
206
|
line_blocks.append(
|
234
207
|
(
|
235
208
|
current_pos,
|
236
|
-
current_pos + line_length,
|
209
|
+
current_pos + line_length - 1,
|
237
210
|
self._create_empty_paragraph(),
|
238
211
|
)
|
239
212
|
)
|
@@ -273,7 +246,7 @@ class MarkdownToNotionConverter:
|
|
273
246
|
current_paragraph, paragraph_start, current_pos, line_blocks
|
274
247
|
)
|
275
248
|
line_blocks.append(
|
276
|
-
(current_pos, current_pos + line_length, special_block)
|
249
|
+
(current_pos, current_pos + line_length - 1, special_block)
|
277
250
|
)
|
278
251
|
current_paragraph = []
|
279
252
|
current_pos += line_length
|
@@ -292,18 +265,6 @@ class MarkdownToNotionConverter:
|
|
292
265
|
|
293
266
|
return line_blocks
|
294
267
|
|
295
|
-
def _is_marker_line(self, line: str) -> bool:
|
296
|
-
"""Check if a line is any kind of marker line that should be skipped."""
|
297
|
-
return self._is_multiline_marker(line) or self._is_toggle_marker(line)
|
298
|
-
|
299
|
-
def _is_multiline_marker(self, line: str) -> bool:
|
300
|
-
"""Check if a line is a multiline content marker."""
|
301
|
-
return line.strip() == self.MULTILINE_CONTENT_MARKER
|
302
|
-
|
303
|
-
def _is_toggle_marker(self, line: str) -> bool:
|
304
|
-
"""Check if a line is a toggle content marker."""
|
305
|
-
return line.strip() == self.TOGGLE_MARKER
|
306
|
-
|
307
268
|
def _is_spacer_marker(self, line: str) -> bool:
|
308
269
|
"""Check if a line is a spacer marker."""
|
309
270
|
return line.strip() == self.SPACER_MARKER
|
@@ -343,7 +304,7 @@ class MarkdownToNotionConverter:
|
|
343
304
|
)
|
344
305
|
current_paragraph.clear()
|
345
306
|
|
346
|
-
line_blocks.append((current_pos, current_pos + line_length, todo_block))
|
307
|
+
line_blocks.append((current_pos, current_pos + line_length - 1, todo_block))
|
347
308
|
|
348
309
|
def _extract_special_block(self, line: str) -> Optional[Dict[str, Any]]:
|
349
310
|
"""
|
{notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/registry/block_element_registry.py
RENAMED
@@ -95,8 +95,8 @@ class BlockElementRegistry:
|
|
95
95
|
"""
|
96
96
|
# Create a copy of registered elements
|
97
97
|
element_classes = self._elements.copy()
|
98
|
-
|
99
|
-
# TODO: Das hier besser formattieren und über debug level lösen . )
|
98
|
+
|
99
|
+
# TODO: Das hier besser formattieren und über debug level lösen . )
|
100
100
|
print("Elements in registry:", element_classes)
|
101
101
|
|
102
102
|
formatter_names = [e.__name__ for e in element_classes]
|
@@ -233,4 +233,4 @@ paragraphs, lists, quotes, etc.
|
|
233
233
|
"""
|
234
234
|
element_docs = cls.generate_element_docs(element_classes)
|
235
235
|
|
236
|
-
return cls.SYSTEM_PROMPT_TEMPLATE.format(element_docs=element_docs)
|
236
|
+
return cls.SYSTEM_PROMPT_TEMPLATE.format(element_docs=element_docs)
|
@@ -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)
|