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.
Files changed (63) hide show
  1. {notionary-0.1.11 → notionary-0.1.12}/PKG-INFO +1 -1
  2. notionary-0.1.12/notionary/__init__.py +20 -0
  3. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/audio_element.py +6 -4
  4. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/embed_element.py +2 -4
  5. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/toggle_element.py +28 -20
  6. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/markdown_to_notion_converter.py +70 -109
  7. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/registry/block_element_registry.py +3 -3
  8. notionary-0.1.12/notionary/core/database/database_discovery.py +140 -0
  9. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/database/notion_database_manager.py +26 -49
  10. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/database/notion_database_manager_factory.py +10 -4
  11. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/notion_client.py +4 -2
  12. notionary-0.1.12/notionary/core/page/content/notion_page_content_chunker.py +84 -0
  13. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/content/page_content_manager.py +26 -8
  14. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/metadata/metadata_editor.py +57 -44
  15. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/metadata/notion_icon_manager.py +9 -11
  16. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/metadata/notion_page_cover_manager.py +15 -20
  17. notionary-0.1.12/notionary/core/page/notion_page_manager.py +312 -0
  18. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/properites/database_property_service.py +114 -98
  19. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/properites/page_property_manager.py +78 -49
  20. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/properites/property_formatter.py +1 -1
  21. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/properites/property_operation_result.py +43 -30
  22. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/properites/property_value_extractor.py +26 -8
  23. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/relations/notion_page_relation_manager.py +71 -52
  24. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/relations/notion_page_title_resolver.py +11 -11
  25. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/relations/page_database_relation.py +14 -14
  26. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/page/relations/relation_operation_result.py +50 -41
  27. {notionary-0.1.11 → notionary-0.1.12}/notionary/util/page_id_utils.py +11 -7
  28. {notionary-0.1.11 → notionary-0.1.12}/notionary.egg-info/PKG-INFO +1 -1
  29. {notionary-0.1.11 → notionary-0.1.12}/notionary.egg-info/SOURCES.txt +2 -1
  30. {notionary-0.1.11 → notionary-0.1.12}/setup.py +2 -2
  31. notionary-0.1.11/notionary/__init__.py +0 -9
  32. notionary-0.1.11/notionary/core/database/notion_database_schema.py +0 -104
  33. notionary-0.1.11/notionary/core/page/notion_page_manager.py +0 -322
  34. {notionary-0.1.11 → notionary-0.1.12}/LICENSE +0 -0
  35. {notionary-0.1.11 → notionary-0.1.12}/README.md +0 -0
  36. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/__init__.py +0 -0
  37. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/bookmark_element.py +0 -0
  38. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/callout_element.py +0 -0
  39. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/code_block_element.py +0 -0
  40. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/column_element.py +0 -0
  41. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/divider_element.py +0 -0
  42. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/heading_element.py +0 -0
  43. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/image_element.py +0 -0
  44. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/list_element.py +0 -0
  45. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/notion_block_element.py +0 -0
  46. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/paragraph_element.py +0 -0
  47. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/qoute_element.py +0 -0
  48. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/table_element.py +0 -0
  49. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/text_inline_formatter.py +0 -0
  50. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/todo_lists.py +0 -0
  51. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/elements/video_element.py +0 -0
  52. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/notion_to_markdown_converter.py +0 -0
  53. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/converters/registry/block_element_registry_builder.py +0 -0
  54. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/database/database_info_service.py +0 -0
  55. {notionary-0.1.11 → notionary-0.1.12}/notionary/core/database/models/page_result.py +0 -0
  56. {notionary-0.1.11 → notionary-0.1.12}/notionary/exceptions/database_exceptions.py +0 -0
  57. {notionary-0.1.11 → notionary-0.1.12}/notionary/exceptions/page_creation_exception.py +0 -0
  58. {notionary-0.1.11 → notionary-0.1.12}/notionary/util/logging_mixin.py +0 -0
  59. {notionary-0.1.11 → notionary-0.1.12}/notionary/util/singleton_decorator.py +0 -0
  60. {notionary-0.1.11 → notionary-0.1.12}/notionary.egg-info/dependency_links.txt +0 -0
  61. {notionary-0.1.11 → notionary-0.1.12}/notionary.egg-info/requires.txt +0 -0
  62. {notionary-0.1.11 → notionary-0.1.12}/notionary.egg-info/top_level.txt +0 -0
  63. {notionary-0.1.11 → notionary-0.1.12}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: notionary
3
- Version: 0.1.11
3
+ Version: 0.1.12
4
4
  Summary: A toolkit to convert between Markdown and Notion blocks
5
5
  Home-page: https://github.com/mathisarends/notionary
6
6
  Author: Mathis Arends
@@ -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 any(url.lower().endswith(ext) for ext in AudioElement.AUDIO_EXTENSIONS) or \
43
- "audio" in url.lower() or \
44
- "storage.googleapis.com/audio_summaries" in url.lower()
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
- Handles conversion between Markdown toggle blocks and Notion toggle blocks.
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, text: str, process_nested_content: Callable = None
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
- Find all toggle elements in the text and process them.
149
+ Verbesserte find_matches-Methode, die Kontext beim Finden von Toggles berücksichtigt.
156
150
 
157
151
  Args:
158
- text: The text to search in
159
- process_nested_content: Optional callback function to process nested content
160
- It should accept a string and return a list of Notion blocks
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
- List of (start_pos, end_pos, block) tuples
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
@@ -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
- # Process toggles first
50
- processed_text, toggle_blocks = self._extract_toggle_elements(markdown_text)
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
- processed_text, multiline_blocks = self._extract_multiline_elements(
54
- processed_text
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(processed_text)
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
- # Combine and sort all blocks
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 from position tuples
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 _extract_toggle_elements(
82
+ def _identify_toggle_blocks(
71
83
  self, text: str
72
- ) -> Tuple[str, List[Tuple[int, int, Dict[str, Any]]]]:
84
+ ) -> List[Tuple[int, int, Dict[str, Any]]]:
73
85
  """
74
- Extract toggle elements and their nested content using the ToggleElement class.
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
- Tuple of (processed text, list of (start_pos, end_pos, block) tuples)
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
- # No toggle element found, return text as is
95
- return text, []
106
+ return []
96
107
 
97
- # Use the find_matches method of ToggleElement to find and process all toggles
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(text, self.convert)
100
-
101
- if not toggle_blocks:
102
- return text, []
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 _extract_multiline_elements(
138
- self, text: str
139
- ) -> Tuple[str, List[Tuple[int, int, Dict[str, Any]]]]:
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
- Extract multiline elements and remove them from the text.
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
- Tuple of (processed text, list of (start_pos, end_pos, block) tuples)
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 text, []
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 (pass the convert method as callback if needed)
148
+ # Find all matches for this element
170
149
  if hasattr(element, "set_converter_callback"):
171
- matches = element.find_matches(processed_text, self.convert)
150
+ matches = element.find_matches(text, self.convert)
172
151
  else:
173
- matches = element.find_matches(processed_text)
152
+ matches = element.find_matches(text)
174
153
 
175
154
  if not matches:
176
155
  continue
177
156
 
178
- multiline_blocks.extend(matches)
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
- # Remove matched content from the text to avoid processing it again
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(self, text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
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 for single-line elements.
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 marker lines
227
- if self._is_marker_line(line):
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
  """
@@ -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)