notionary 0.2.10__py3-none-any.whl → 0.2.12__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 (59) hide show
  1. notionary/__init__.py +6 -0
  2. notionary/cli/main.py +184 -0
  3. notionary/cli/onboarding.py +0 -0
  4. notionary/database/database_discovery.py +1 -1
  5. notionary/database/notion_database.py +5 -4
  6. notionary/database/notion_database_factory.py +10 -5
  7. notionary/elements/audio_element.py +2 -2
  8. notionary/elements/bookmark_element.py +2 -2
  9. notionary/elements/bulleted_list_element.py +2 -2
  10. notionary/elements/callout_element.py +2 -2
  11. notionary/elements/code_block_element.py +2 -2
  12. notionary/elements/column_element.py +51 -44
  13. notionary/elements/divider_element.py +2 -2
  14. notionary/elements/embed_element.py +2 -2
  15. notionary/elements/heading_element.py +3 -3
  16. notionary/elements/image_element.py +2 -2
  17. notionary/elements/mention_element.py +2 -2
  18. notionary/elements/notion_block_element.py +36 -0
  19. notionary/elements/numbered_list_element.py +2 -2
  20. notionary/elements/paragraph_element.py +2 -2
  21. notionary/elements/qoute_element.py +2 -2
  22. notionary/elements/table_element.py +2 -2
  23. notionary/elements/text_inline_formatter.py +23 -1
  24. notionary/elements/todo_element.py +2 -2
  25. notionary/elements/toggle_element.py +2 -2
  26. notionary/elements/toggleable_heading_element.py +2 -2
  27. notionary/elements/video_element.py +2 -2
  28. notionary/notion_client.py +1 -1
  29. notionary/page/content/notion_page_content_chunker.py +1 -1
  30. notionary/page/content/page_content_retriever.py +1 -1
  31. notionary/page/content/page_content_writer.py +3 -3
  32. notionary/page/{markdown_to_notion_converter.py → formatting/markdown_to_notion_converter.py} +44 -140
  33. notionary/page/formatting/spacer_rules.py +483 -0
  34. notionary/page/metadata/metadata_editor.py +1 -1
  35. notionary/page/metadata/notion_icon_manager.py +1 -1
  36. notionary/page/metadata/notion_page_cover_manager.py +1 -1
  37. notionary/page/notion_page.py +1 -1
  38. notionary/page/notion_page_factory.py +161 -22
  39. notionary/page/properites/database_property_service.py +1 -1
  40. notionary/page/properites/page_property_manager.py +1 -1
  41. notionary/page/properites/property_formatter.py +1 -1
  42. notionary/page/properites/property_value_extractor.py +1 -1
  43. notionary/page/relations/notion_page_relation_manager.py +1 -1
  44. notionary/page/relations/notion_page_title_resolver.py +1 -1
  45. notionary/page/relations/page_database_relation.py +1 -1
  46. notionary/prompting/element_prompt_content.py +1 -0
  47. notionary/telemetry/__init__.py +7 -0
  48. notionary/telemetry/telemetry.py +226 -0
  49. notionary/telemetry/track_usage_decorator.py +76 -0
  50. notionary/util/__init__.py +5 -0
  51. notionary/util/logging_mixin.py +3 -0
  52. notionary/util/singleton.py +18 -0
  53. {notionary-0.2.10.dist-info → notionary-0.2.12.dist-info}/METADATA +3 -1
  54. notionary-0.2.12.dist-info/RECORD +70 -0
  55. {notionary-0.2.10.dist-info → notionary-0.2.12.dist-info}/WHEEL +1 -1
  56. notionary-0.2.12.dist-info/entry_points.txt +2 -0
  57. notionary-0.2.10.dist-info/RECORD +0 -61
  58. {notionary-0.2.10.dist-info → notionary-0.2.12.dist-info}/licenses/LICENSE +0 -0
  59. {notionary-0.2.10.dist-info → notionary-0.2.12.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,13 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional
3
- from notionary.elements.notion_block_element import NotionBlockElement
3
+ from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
4
4
  from notionary.prompting.element_prompt_content import (
5
5
  ElementPromptBuilder,
6
6
  ElementPromptContent,
7
7
  )
8
8
  from notionary.elements.text_inline_formatter import TextInlineFormatter
9
9
 
10
-
10
+ @auto_track_conversions
11
11
  class NumberedListElement(NotionBlockElement):
12
12
  """Class for converting between Markdown numbered lists and Notion numbered list items."""
13
13
 
@@ -1,13 +1,13 @@
1
1
  from typing import Dict, Any, Optional
2
2
 
3
- from notionary.elements.notion_block_element import NotionBlockElement
3
+ from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
4
4
  from notionary.prompting.element_prompt_content import (
5
5
  ElementPromptBuilder,
6
6
  ElementPromptContent,
7
7
  )
8
8
  from notionary.elements.text_inline_formatter import TextInlineFormatter
9
9
 
10
-
10
+ @auto_track_conversions
11
11
  class ParagraphElement(NotionBlockElement):
12
12
  """Handles conversion between Markdown paragraphs and Notion paragraph blocks."""
13
13
 
@@ -1,12 +1,12 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List, Tuple
3
- from notionary.elements.notion_block_element import NotionBlockElement
3
+ from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
4
4
  from notionary.prompting.element_prompt_content import (
5
5
  ElementPromptBuilder,
6
6
  ElementPromptContent,
7
7
  )
8
8
 
9
-
9
+ @auto_track_conversions
10
10
  class QuoteElement(NotionBlockElement):
11
11
  """Class for converting between Markdown blockquotes and Notion quote blocks."""
12
12
 
@@ -1,13 +1,13 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List, Tuple
3
- from notionary.elements.notion_block_element import NotionBlockElement
3
+ from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
4
4
  from notionary.elements.text_inline_formatter import TextInlineFormatter
5
5
  from notionary.prompting.element_prompt_content import (
6
6
  ElementPromptBuilder,
7
7
  ElementPromptContent,
8
8
  )
9
9
 
10
-
10
+ @auto_track_conversions
11
11
  class TableElement(NotionBlockElement):
12
12
  """
13
13
  Handles conversion between Markdown tables and Notion table blocks.
@@ -29,6 +29,7 @@ class TextInlineFormatter:
29
29
  (r"~~(.+?)~~", {"strikethrough": True}),
30
30
  (r"`(.+?)`", {"code": True}),
31
31
  (r"\[(.+?)\]\((.+?)\)", {"link": True}),
32
+ (r"@\[([0-9a-f-]+)\]", {"mention": True}),
32
33
  ]
33
34
 
34
35
  @classmethod
@@ -87,11 +88,15 @@ class TextInlineFormatter:
87
88
  cls._create_text_element(remaining_text[:earliest_pos], {})
88
89
  )
89
90
 
90
- elif "link" in earliest_format:
91
+ if "link" in earliest_format:
91
92
  content = earliest_match.group(1)
92
93
  url = earliest_match.group(2)
93
94
  segments.append(cls._create_link_element(content, url))
94
95
 
96
+ elif "mention" in earliest_format:
97
+ id = earliest_match.group(1)
98
+ segments.append(cls._create_mention_element(id))
99
+
95
100
  else:
96
101
  content = earliest_match.group(1)
97
102
  segments.append(cls._create_text_element(content, earliest_format))
@@ -152,6 +157,23 @@ class TextInlineFormatter:
152
157
  "plain_text": text,
153
158
  }
154
159
 
160
+ @classmethod
161
+ def _create_mention_element(cls, id: str) -> Dict[str, Any]:
162
+ """
163
+ Create a Notion mention element.
164
+
165
+ Args:
166
+ id: The page ID
167
+
168
+ Returns:
169
+ Notion rich_text element with mention
170
+ """
171
+ return {
172
+ "type": "mention",
173
+ "mention": {"type": "page", "page": {"id": id}},
174
+ "annotations": cls._default_annotations(),
175
+ }
176
+
155
177
  @classmethod
156
178
  def extract_text_with_formatting(cls, rich_text: List[Dict[str, Any]]) -> str:
157
179
  """
@@ -1,13 +1,13 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional
3
- from notionary.elements.notion_block_element import NotionBlockElement
3
+ from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
4
4
  from notionary.prompting.element_prompt_content import (
5
5
  ElementPromptBuilder,
6
6
  ElementPromptContent,
7
7
  )
8
8
  from notionary.elements.text_inline_formatter import TextInlineFormatter
9
9
 
10
-
10
+ @auto_track_conversions
11
11
  class TodoElement(NotionBlockElement):
12
12
  """
13
13
  Handles conversion between Markdown todo items and Notion to_do blocks.
@@ -1,13 +1,13 @@
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
4
+ from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
5
5
  from notionary.prompting.element_prompt_content import (
6
6
  ElementPromptBuilder,
7
7
  ElementPromptContent,
8
8
  )
9
9
 
10
-
10
+ @auto_track_conversions
11
11
  class ToggleElement(NotionBlockElement):
12
12
  """
13
13
  Improved ToggleElement class using pipe syntax instead of indentation.
@@ -1,14 +1,14 @@
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
4
+ from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
5
5
  from notionary.prompting.element_prompt_content import (
6
6
  ElementPromptBuilder,
7
7
  ElementPromptContent,
8
8
  )
9
9
  from notionary.elements.text_inline_formatter import TextInlineFormatter
10
10
 
11
-
11
+ @auto_track_conversions
12
12
  class ToggleableHeadingElement(NotionBlockElement):
13
13
  """Handles conversion between Markdown collapsible headings and Notion toggleable heading blocks with pipe syntax."""
14
14
 
@@ -1,12 +1,12 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List
3
- from notionary.elements.notion_block_element import NotionBlockElement
3
+ from notionary.elements.notion_block_element import NotionBlockElement, auto_track_conversions
4
4
  from notionary.prompting.element_prompt_content import (
5
5
  ElementPromptBuilder,
6
6
  ElementPromptContent,
7
7
  )
8
8
 
9
-
9
+ @auto_track_conversions
10
10
  class VideoElement(NotionBlockElement):
11
11
  """
12
12
  Handles conversion between Markdown video embeds and Notion video blocks.
@@ -7,7 +7,7 @@ import httpx
7
7
  from dotenv import load_dotenv
8
8
  from notionary.models.notion_database_response import NotionDatabaseResponse
9
9
  from notionary.models.notion_page_response import NotionPageResponse
10
- from notionary.util.logging_mixin import LoggingMixin
10
+ from notionary.util import LoggingMixin
11
11
 
12
12
 
13
13
  class HttpMethod(Enum):
@@ -1,6 +1,6 @@
1
1
  import re
2
2
  from typing import Any, Dict, List
3
- from notionary.util.logging_mixin import LoggingMixin
3
+ from notionary.util import LoggingMixin
4
4
 
5
5
 
6
6
  class NotionPageContentChunker(LoggingMixin):
@@ -6,7 +6,7 @@ from notionary.notion_client import NotionClient
6
6
  from notionary.page.notion_to_markdown_converter import (
7
7
  NotionToMarkdownConverter,
8
8
  )
9
- from notionary.util.logging_mixin import LoggingMixin
9
+ from notionary.util import LoggingMixin
10
10
 
11
11
 
12
12
  class PageContentRetriever(LoggingMixin):
@@ -4,7 +4,7 @@ from notionary.elements.divider_element import DividerElement
4
4
  from notionary.elements.registry.block_registry import BlockRegistry
5
5
  from notionary.notion_client import NotionClient
6
6
 
7
- from notionary.page.markdown_to_notion_converter import (
7
+ from notionary.page.formatting.markdown_to_notion_converter import (
8
8
  MarkdownToNotionConverter,
9
9
  )
10
10
  from notionary.page.notion_to_markdown_converter import (
@@ -13,7 +13,7 @@ from notionary.page.notion_to_markdown_converter import (
13
13
  from notionary.page.content.notion_page_content_chunker import (
14
14
  NotionPageContentChunker,
15
15
  )
16
- from notionary.util.logging_mixin import LoggingMixin
16
+ from notionary.util import LoggingMixin
17
17
 
18
18
 
19
19
  class PageContentWriter(LoggingMixin):
@@ -44,7 +44,7 @@ class PageContentWriter(LoggingMixin):
44
44
  )
45
45
  append_divider = False
46
46
 
47
- # Append divider in markdown format as it will be converted to a Notion divider block (eher davor als danach wie ich finde.)
47
+ # Append divider in markdown format as it will be converted to a Notion divider block
48
48
  if append_divider:
49
49
  markdown_text = markdown_text + "---\n"
50
50
 
@@ -1,21 +1,14 @@
1
- from typing import Dict, Any, List, Optional, Tuple
2
1
  import re
2
+ from typing import Dict, Any, List, Optional, Tuple
3
3
 
4
4
  from notionary.elements.column_element import ColumnElement
5
5
  from notionary.elements.registry.block_registry import BlockRegistry
6
- from notionary.elements.registry.block_registry_builder import (
7
- BlockRegistryBuilder,
8
- )
6
+ from notionary.elements.registry.block_registry_builder import BlockRegistryBuilder
7
+ from notionary.page.formatting.spacer_rules import SpacerRule, SpacerRuleEngine
9
8
 
10
9
 
11
10
  class MarkdownToNotionConverter:
12
- """Converts Markdown text to Notion API block format with support for pipe syntax for nested structures."""
13
-
14
- SPACER_MARKER = "---spacer---"
15
- TOGGLE_ELEMENT_TYPES = ["ToggleElement", "ToggleableHeadingElement"]
16
- PIPE_CONTENT_PATTERN = r"^\|\s?(.*)$"
17
- HEADING_PATTERN = r"^(#{1,6})\s+(.+)$"
18
- DIVIDER_PATTERN = r"^-{3,}$"
11
+ """Refactored converter mit expliziten Spacer-Regeln"""
19
12
 
20
13
  def __init__(self, block_registry: Optional[BlockRegistry] = None):
21
14
  """Initialize the converter with an optional custom block registry."""
@@ -23,6 +16,13 @@ class MarkdownToNotionConverter:
23
16
  block_registry or BlockRegistryBuilder().create_full_registry()
24
17
  )
25
18
 
19
+ # Spacer-Engine mit konfigurierbaren Regeln
20
+ self._spacer_engine = SpacerRuleEngine()
21
+
22
+ # Pattern für andere Verarbeitungsschritte
23
+ self.TOGGLE_ELEMENT_TYPES = ["ToggleElement", "ToggleableHeadingElement"]
24
+ self.PIPE_CONTENT_PATTERN = r"^\|\s?(.*)$"
25
+
26
26
  if self._block_registry.contains(ColumnElement):
27
27
  ColumnElement.set_converter_callback(self.convert)
28
28
 
@@ -31,157 +31,61 @@ class MarkdownToNotionConverter:
31
31
  if not markdown_text:
32
32
  return []
33
33
 
34
- # Preprocess markdown to add spacers before headings and dividers
35
- processed_markdown = self._add_spacers_before_elements(markdown_text)
36
- print("Processed Markdown:", processed_markdown)
34
+ # Spacer-Verarbeitung mit expliziten Regeln
35
+ processed_markdown = self._add_spacers_with_rules(markdown_text)
37
36
 
38
- # Collect all blocks with their positions in the text
37
+ # Rest der Pipeline bleibt gleich
39
38
  all_blocks_with_positions = self._collect_all_blocks_with_positions(
40
39
  processed_markdown
41
40
  )
42
-
43
- # Sort all blocks by their position in the text
44
41
  all_blocks_with_positions.sort(key=lambda x: x[0])
45
-
46
- # Extract just the blocks without position information
47
42
  blocks = [block for _, _, block in all_blocks_with_positions]
48
43
 
49
- # Process spacing between blocks
50
44
  return self._process_block_spacing(blocks)
51
45
 
52
- def _add_spacers_before_elements(self, markdown_text: str) -> str:
53
- """Add spacer markers before every heading (except the first one) and before every divider,
54
- but ignore content inside code blocks and consecutive headings."""
46
+ def _add_spacers_with_rules(self, markdown_text: str) -> str:
47
+ """Fügt Spacer mit expliziten Regeln hinzu"""
55
48
  lines = markdown_text.split("\n")
56
49
  processed_lines = []
57
- found_first_heading = False
58
- in_code_block = False
59
- last_line_was_spacer = False
60
- last_non_empty_was_heading = False
61
-
62
- i = 0
63
- while i < len(lines):
64
- line = lines[i]
65
-
66
- # Check for code block boundaries and handle accordingly
67
- if self._is_code_block_marker(line):
68
- in_code_block = not in_code_block
69
- processed_lines.append(line)
70
- if line.strip(): # If not empty
71
- last_non_empty_was_heading = False
72
- last_line_was_spacer = False
73
- i += 1
74
- continue
75
50
 
76
- # Skip processing markdown inside code blocks
77
- if in_code_block:
78
- processed_lines.append(line)
79
- if line.strip(): # If not empty
80
- last_non_empty_was_heading = False
81
- last_line_was_spacer = False
82
- i += 1
83
- continue
51
+ # Initialer State
52
+ state = {
53
+ "in_code_block": False,
54
+ "last_line_was_spacer": False,
55
+ "last_non_empty_was_heading": False,
56
+ "has_content_before": False,
57
+ "processed_lines": processed_lines,
58
+ }
84
59
 
85
- # Process line with context about consecutive headings
86
- result = self._process_line_for_spacers(
87
- line,
88
- processed_lines,
89
- last_line_was_spacer,
90
- last_non_empty_was_heading,
60
+ for line_number, line in enumerate(lines):
61
+ result_lines, state = self._spacer_engine.process_line(
62
+ line, line_number, state
91
63
  )
92
-
93
- last_line_was_spacer = result["added_spacer"]
94
-
95
- # Update tracking of consecutive headings and first heading
96
- if line.strip(): # Not empty line
97
- is_heading = re.match(self.HEADING_PATTERN, line) is not None
98
- if is_heading:
99
- if not found_first_heading:
100
- found_first_heading = True
101
- last_non_empty_was_heading = True
102
- elif line.strip() != self.SPACER_MARKER: # Not a spacer or heading
103
- last_non_empty_was_heading = False
104
-
105
- i += 1
64
+ processed_lines.extend(result_lines)
65
+ state["processed_lines"] = processed_lines
106
66
 
107
67
  return "\n".join(processed_lines)
108
68
 
109
- def _is_code_block_marker(self, line: str) -> bool:
110
- """Check if a line is a code block marker (start or end)."""
111
- return line.strip().startswith("```")
112
-
113
- def _process_line_for_spacers(
114
- self,
115
- line: str,
116
- processed_lines: List[str],
117
- last_line_was_spacer: bool,
118
- last_non_empty_was_heading: bool,
119
- ) -> Dict[str, bool]:
120
- """
121
- Process a single line to add spacers before headings and dividers if needed.
69
+ def add_custom_spacer_rule(self, rule: SpacerRule, priority: int = -1):
70
+ """Fügt eine benutzerdefinierte Spacer-Regel hinzu
122
71
 
123
72
  Args:
124
- line: The line to process
125
- processed_lines: List of already processed lines to append to
126
- found_first_heading: Whether the first heading has been found
127
- last_line_was_spacer: Whether the last added line was a spacer
128
- last_non_empty_was_heading: Whether the last non-empty line was a heading
129
-
130
- Returns:
131
- Dictionary with processing results
73
+ rule: Die hinzuzufügende Regel
74
+ priority: Position in der Regelliste (-1 = am Ende)
132
75
  """
133
- added_spacer = False
134
- line_stripped = line.strip()
135
- is_empty = not line_stripped
136
-
137
- # Skip empty lines
138
- if is_empty:
139
- processed_lines.append(line)
140
- return {"added_spacer": False}
141
-
142
- # Check if line is a heading
143
- if re.match(self.HEADING_PATTERN, line):
144
- # Check if there's content before this heading (excluding spacers)
145
- has_content_before = any(
146
- processed_line.strip() and processed_line.strip() != self.SPACER_MARKER
147
- for processed_line in processed_lines
148
- )
149
-
150
- if (
151
- has_content_before
152
- and not last_line_was_spacer
153
- and not last_non_empty_was_heading
154
- ):
155
- # Add spacer if:
156
- # 1. There's content before this heading
157
- # 2. Last line was not already a spacer
158
- # 3. Last non-empty line was not a heading
159
- processed_lines.append(self.SPACER_MARKER)
160
- added_spacer = True
161
-
162
- processed_lines.append(line)
163
-
164
- # Check if line is a divider
165
- elif re.match(self.DIVIDER_PATTERN, line):
166
- if not last_line_was_spacer:
167
- # Only add a single spacer line before dividers (no extra line breaks)
168
- processed_lines.append(self.SPACER_MARKER)
169
- added_spacer = True
170
-
171
- processed_lines.append(line)
172
-
173
- # Check if this line itself is a spacer
174
- elif line_stripped == self.SPACER_MARKER:
175
- # Never add consecutive spacers
176
- if not last_line_was_spacer:
177
- processed_lines.append(line)
178
- added_spacer = True
179
-
76
+ if priority == -1:
77
+ self._spacer_engine.rules.append(rule)
180
78
  else:
181
- processed_lines.append(line)
79
+ self._spacer_engine.rules.insert(priority, rule)
182
80
 
183
- return {"added_spacer": added_spacer}
81
+ def get_spacer_rules_info(self) -> List[Dict[str, str]]:
82
+ """Gibt Informationen über alle aktiven Spacer-Regeln zurück"""
83
+ return [
84
+ {"name": rule.name, "description": rule.description}
85
+ for rule in self._spacer_engine.rules
86
+ ]
184
87
 
88
+ # Alle anderen Methoden bleiben unverändert...
185
89
  def _collect_all_blocks_with_positions(
186
90
  self, markdown_text: str
187
91
  ) -> List[Tuple[int, int, Dict[str, Any]]]:
@@ -416,7 +320,7 @@ class MarkdownToNotionConverter:
416
320
 
417
321
  def _is_spacer_line(self, line: str) -> bool:
418
322
  """Check if a line is a spacer marker."""
419
- return line.strip() == self.SPACER_MARKER
323
+ return line.strip() == self._spacer_engine.SPACER_MARKER
420
324
 
421
325
  def _process_todo_line(
422
326
  self,