notionary 0.2.16__py3-none-any.whl → 0.2.18__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 (137) hide show
  1. notionary/__init__.py +10 -5
  2. notionary/base_notion_client.py +18 -7
  3. notionary/blocks/__init__.py +55 -24
  4. notionary/blocks/audio/__init__.py +7 -0
  5. notionary/blocks/audio/audio_element.py +152 -0
  6. notionary/blocks/audio/audio_markdown_node.py +29 -0
  7. notionary/blocks/audio/audio_models.py +59 -0
  8. notionary/blocks/bookmark/__init__.py +7 -0
  9. notionary/blocks/{bookmark_element.py → bookmark/bookmark_element.py} +20 -65
  10. notionary/blocks/bookmark/bookmark_markdown_node.py +43 -0
  11. notionary/blocks/bulleted_list/__init__.py +7 -0
  12. notionary/blocks/{bulleted_list_element.py → bulleted_list/bulleted_list_element.py} +7 -3
  13. notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +33 -0
  14. notionary/blocks/bulleted_list/bulleted_list_models.py +0 -0
  15. notionary/blocks/callout/__init__.py +7 -0
  16. notionary/blocks/callout/callout_element.py +132 -0
  17. notionary/blocks/callout/callout_markdown_node.py +31 -0
  18. notionary/blocks/callout/callout_models.py +0 -0
  19. notionary/blocks/code/__init__.py +7 -0
  20. notionary/blocks/{code_block_element.py → code/code_element.py} +72 -40
  21. notionary/blocks/code/code_markdown_node.py +43 -0
  22. notionary/blocks/code/code_models.py +0 -0
  23. notionary/blocks/column/__init__.py +5 -0
  24. notionary/blocks/{column_element.py → column/column_element.py} +24 -55
  25. notionary/blocks/column/column_models.py +0 -0
  26. notionary/blocks/divider/__init__.py +7 -0
  27. notionary/blocks/{divider_element.py → divider/divider_element.py} +11 -3
  28. notionary/blocks/divider/divider_markdown_node.py +24 -0
  29. notionary/blocks/divider/divider_models.py +0 -0
  30. notionary/blocks/document/__init__.py +7 -0
  31. notionary/blocks/document/document_element.py +102 -0
  32. notionary/blocks/document/document_markdown_node.py +31 -0
  33. notionary/blocks/document/document_models.py +0 -0
  34. notionary/blocks/embed/__init__.py +7 -0
  35. notionary/blocks/{embed_element.py → embed/embed_element.py} +50 -32
  36. notionary/blocks/embed/embed_markdown_node.py +30 -0
  37. notionary/blocks/embed/embed_models.py +0 -0
  38. notionary/blocks/heading/__init__.py +7 -0
  39. notionary/blocks/{heading_element.py → heading/heading_element.py} +25 -17
  40. notionary/blocks/heading/heading_markdown_node.py +29 -0
  41. notionary/blocks/heading/heading_models.py +0 -0
  42. notionary/blocks/image/__init__.py +7 -0
  43. notionary/blocks/{image_element.py → image/image_element.py} +62 -42
  44. notionary/blocks/image/image_markdown_node.py +33 -0
  45. notionary/blocks/image/image_models.py +0 -0
  46. notionary/blocks/markdown_builder.py +356 -0
  47. notionary/blocks/markdown_node.py +29 -0
  48. notionary/blocks/mention/__init__.py +7 -0
  49. notionary/blocks/{mention_element.py → mention/mention_element.py} +6 -2
  50. notionary/blocks/mention/mention_markdown_node.py +38 -0
  51. notionary/blocks/mention/mention_models.py +0 -0
  52. notionary/blocks/numbered_list/__init__.py +7 -0
  53. notionary/blocks/{numbered_list_element.py → numbered_list/numbered_list_element.py} +10 -6
  54. notionary/blocks/numbered_list/numbered_list_markdown_node.py +29 -0
  55. notionary/blocks/numbered_list/numbered_list_models.py +0 -0
  56. notionary/blocks/paragraph/__init__.py +7 -0
  57. notionary/blocks/{paragraph_element.py → paragraph/paragraph_element.py} +7 -3
  58. notionary/blocks/paragraph/paragraph_markdown_node.py +25 -0
  59. notionary/blocks/paragraph/paragraph_models.py +0 -0
  60. notionary/blocks/quote/__init__.py +7 -0
  61. notionary/blocks/quote/quote_element.py +92 -0
  62. notionary/blocks/quote/quote_markdown_node.py +23 -0
  63. notionary/blocks/quote/quote_models.py +0 -0
  64. notionary/blocks/registry/block_registry.py +17 -3
  65. notionary/blocks/registry/block_registry_builder.py +90 -178
  66. notionary/blocks/shared/__init__.py +0 -0
  67. notionary/blocks/shared/block_client.py +256 -0
  68. notionary/blocks/shared/models.py +710 -0
  69. notionary/blocks/{notion_block_element.py → shared/notion_block_element.py} +8 -5
  70. notionary/blocks/{text_inline_formatter.py → shared/text_inline_formatter.py} +14 -14
  71. notionary/blocks/shared/text_inline_formatter_new.py +139 -0
  72. notionary/blocks/table/__init__.py +7 -0
  73. notionary/blocks/{table_element.py → table/table_element.py} +23 -11
  74. notionary/blocks/table/table_markdown_node.py +40 -0
  75. notionary/blocks/table/table_models.py +0 -0
  76. notionary/blocks/todo/__init__.py +7 -0
  77. notionary/blocks/{todo_element.py → todo/todo_element.py} +8 -4
  78. notionary/blocks/todo/todo_markdown_node.py +31 -0
  79. notionary/blocks/todo/todo_models.py +0 -0
  80. notionary/blocks/toggle/__init__.py +4 -0
  81. notionary/blocks/{toggle_element.py → toggle/toggle_element.py} +7 -3
  82. notionary/blocks/toggle/toggle_markdown_node.py +35 -0
  83. notionary/blocks/toggle/toggle_models.py +0 -0
  84. notionary/blocks/toggleable_heading/__init__.py +9 -0
  85. notionary/blocks/{toggleable_heading_element.py → toggleable_heading/toggleable_heading_element.py} +8 -4
  86. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +43 -0
  87. notionary/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
  88. notionary/blocks/video/__init__.py +7 -0
  89. notionary/blocks/{video_element.py → video/video_element.py} +82 -57
  90. notionary/blocks/video/video_markdown_node.py +30 -0
  91. notionary/database/__init__.py +4 -0
  92. notionary/database/database.py +481 -0
  93. notionary/database/{filter_builder.py → database_filter_builder.py} +27 -29
  94. notionary/database/{notion_database_provider.py → database_provider.py} +4 -4
  95. notionary/database/notion_database.py +45 -18
  96. notionary/file_upload/__init__.py +7 -0
  97. notionary/file_upload/client.py +254 -0
  98. notionary/file_upload/models.py +60 -0
  99. notionary/file_upload/notion_file_upload.py +387 -0
  100. notionary/page/content/markdown_whitespace_processor.py +80 -0
  101. notionary/page/content/notion_text_length_utils.py +87 -0
  102. notionary/page/content/page_content_retriever.py +2 -2
  103. notionary/page/content/page_content_writer.py +97 -148
  104. notionary/page/formatting/line_processor.py +153 -0
  105. notionary/page/formatting/markdown_to_notion_converter.py +103 -424
  106. notionary/page/notion_page.py +13 -14
  107. notionary/page/notion_to_markdown_converter.py +9 -13
  108. notionary/telemetry/views.py +15 -6
  109. notionary/user/__init__.py +11 -0
  110. notionary/user/base_notion_user.py +52 -0
  111. notionary/user/client.py +129 -0
  112. notionary/user/models.py +83 -0
  113. notionary/user/notion_bot_user.py +227 -0
  114. notionary/user/notion_user.py +256 -0
  115. notionary/user/notion_user_manager.py +173 -0
  116. notionary/user/notion_user_provider.py +1 -0
  117. notionary/util/__init__.py +3 -5
  118. notionary/util/factory_decorator.py +0 -33
  119. notionary/util/factory_only.py +37 -0
  120. notionary/util/fuzzy.py +74 -0
  121. notionary/util/logging_mixin.py +12 -12
  122. notionary/workspace.py +38 -3
  123. {notionary-0.2.16.dist-info → notionary-0.2.18.dist-info}/METADATA +2 -1
  124. notionary-0.2.18.dist-info/RECORD +149 -0
  125. notionary/blocks/audio_element.py +0 -144
  126. notionary/blocks/callout_element.py +0 -122
  127. notionary/blocks/notion_block_client.py +0 -26
  128. notionary/blocks/qoute_element.py +0 -169
  129. notionary/page/content/notion_page_content_chunker.py +0 -84
  130. notionary/page/formatting/spacer_rules.py +0 -483
  131. notionary/util/fuzzy_matcher.py +0 -82
  132. notionary-0.2.16.dist-info/RECORD +0 -71
  133. /notionary/{elements/__init__.py → blocks/bookmark/bookmark_models.py} +0 -0
  134. /notionary/database/{database_exceptions.py → exceptions.py} +0 -0
  135. /notionary/util/{singleton_decorator.py → singleton.py} +0 -0
  136. {notionary-0.2.16.dist-info → notionary-0.2.18.dist-info}/LICENSE +0 -0
  137. {notionary-0.2.16.dist-info → notionary-0.2.18.dist-info}/WHEEL +0 -0
@@ -1,29 +1,38 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List
3
3
 
4
- from notionary.blocks import NotionBlockElement
5
- from notionary.blocks import ElementPromptContent, ElementPromptBuilder
4
+ from notionary.blocks import (
5
+ ElementPromptContent,
6
+ ElementPromptBuilder,
7
+ NotionBlockResult,
8
+ NotionBlockElement,
9
+ )
6
10
 
7
11
 
8
12
  class VideoElement(NotionBlockElement):
9
13
  """
10
14
  Handles conversion between Markdown video embeds and Notion video blocks.
11
15
 
12
- Markdown video syntax (custom format since standard Markdown doesn't support videos):
13
- - @[Caption](https://example.com/video.mp4) - Basic video with caption
14
- - @[](https://example.com/video.mp4) - Video without caption
15
- - @[Caption](https://www.youtube.com/watch?v=dQw4w9WgXcQ) - YouTube video
16
- - @[Caption](https://youtu.be/dQw4w9WgXcQ) - YouTube shortened URL
16
+ Markdown video syntax:
17
+ - [video](https://example.com/video.mp4) - Simple video with URL only
18
+ - [video](https://example.com/video.mp4 "Caption") - Video with URL and caption
19
+
20
+ Where:
21
+ - URL is the required video URL
22
+ - Caption is an optional descriptive text (enclosed in quotes)
17
23
 
18
24
  Supports various video URLs including YouTube, Vimeo, and direct video file links.
19
25
  """
20
26
 
27
+ # Regex pattern for video syntax with optional caption
21
28
  PATTERN = re.compile(
22
- r"^\@\[(.*?)\]" # @[Caption] part
23
- + r'\((https?://[^\s"]+)' # (URL part
29
+ r"^\[video\]\(" # [video]( prefix
30
+ + r'(https?://[^\s"]+)' # URL (required)
31
+ + r'(?:\s+"([^"]+)")?' # Optional caption in quotes
24
32
  + r"\)$" # closing parenthesis
25
33
  )
26
34
 
35
+ # YouTube URL patterns
27
36
  YOUTUBE_PATTERNS = [
28
37
  re.compile(
29
38
  r"(?:https?://)?(?:www\.)?youtube\.com/watch\?v=([a-zA-Z0-9_-]{11})"
@@ -34,8 +43,9 @@ class VideoElement(NotionBlockElement):
34
43
  @classmethod
35
44
  def match_markdown(cls, text: str) -> bool:
36
45
  """Check if text is a markdown video embed."""
37
- text = text.strip()
38
- return text.startswith("@[") and bool(VideoElement.PATTERN.match(text))
46
+ return text.strip().startswith("[video]") and bool(
47
+ VideoElement.PATTERN.match(text.strip())
48
+ )
39
49
 
40
50
  @classmethod
41
51
  def match_notion(cls, block: Dict[str, Any]) -> bool:
@@ -43,51 +53,38 @@ class VideoElement(NotionBlockElement):
43
53
  return block.get("type") == "video"
44
54
 
45
55
  @classmethod
46
- def is_youtube_url(cls, url: str) -> bool:
47
- """Check if URL is a YouTube video and return video ID if it is."""
48
- for pattern in VideoElement.YOUTUBE_PATTERNS:
49
- match = pattern.match(url)
50
- if match:
51
- return True
52
- return False
53
-
54
- @classmethod
55
- def get_youtube_id(cls, url: str) -> Optional[str]:
56
- """Extract YouTube video ID from URL."""
57
- for pattern in VideoElement.YOUTUBE_PATTERNS:
58
- match = pattern.match(url)
59
- if match:
60
- return match.group(1)
61
- return None
62
-
63
- @classmethod
64
- def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
56
+ def markdown_to_notion(cls, text: str) -> NotionBlockResult:
65
57
  """Convert markdown video embed to Notion video block."""
66
58
  video_match = VideoElement.PATTERN.match(text.strip())
67
59
  if not video_match:
68
60
  return None
69
61
 
70
- caption = video_match.group(1)
71
- url = video_match.group(2)
62
+ url = video_match.group(1)
63
+ caption = video_match.group(2)
72
64
 
73
65
  if not url:
74
66
  return None
75
67
 
76
- youtube_id = VideoElement.get_youtube_id(url)
68
+ # Normalize YouTube URLs
69
+ youtube_id = VideoElement._get_youtube_id(url)
77
70
  if youtube_id:
78
71
  url = f"https://www.youtube.com/watch?v={youtube_id}"
79
72
 
80
- video_block = {
81
- "type": "video",
82
- "video": {"type": "external", "external": {"url": url}},
83
- }
73
+ video_data = {"type": "external", "external": {"url": url}}
84
74
 
75
+ # Add caption if provided
85
76
  if caption:
86
- video_block["video"]["caption"] = [
87
- {"type": "text", "text": {"content": caption}}
88
- ]
77
+ video_data["caption"] = [{"type": "text", "text": {"content": caption}}]
78
+ else:
79
+ video_data["caption"] = []
89
80
 
90
- return video_block
81
+ # Prepare the video block
82
+ video_block = {"type": "video", "video": video_data}
83
+
84
+ # Add empty paragraph after video
85
+ empty_paragraph = {"type": "paragraph", "paragraph": {"rich_text": []}}
86
+
87
+ return [video_block, empty_paragraph]
91
88
 
92
89
  @classmethod
93
90
  def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
@@ -97,29 +94,56 @@ class VideoElement(NotionBlockElement):
97
94
 
98
95
  video_data = block.get("video", {})
99
96
 
100
- # Handle both external and file (uploaded) videos
101
- if video_data.get("type") == "external":
102
- url = video_data.get("external", {}).get("url", "")
103
- elif video_data.get("type") == "file":
104
- url = video_data.get("file", {}).get("url", "")
105
- else:
106
- return None
107
-
97
+ # Extract URL from video data
98
+ url = VideoElement._extract_video_url(video_data)
108
99
  if not url:
109
100
  return None
110
101
 
111
- caption = ""
112
102
  caption_rich_text = video_data.get("caption", [])
113
- if caption_rich_text:
114
- caption = VideoElement._extract_text_content(caption_rich_text)
115
103
 
116
- return f"@[{caption}]({url})"
104
+ if not caption_rich_text:
105
+ # Simple video with URL only
106
+ return f"[video]({url})"
107
+
108
+ # Extract caption text
109
+ caption = VideoElement._extract_text_content(caption_rich_text)
110
+
111
+ if caption:
112
+ return f'[video]({url} "{caption}")'
113
+
114
+ return f"[video]({url})"
117
115
 
118
116
  @classmethod
119
117
  def is_multiline(cls) -> bool:
120
118
  """Videos are single-line elements."""
121
119
  return False
122
120
 
121
+ @classmethod
122
+ def _is_youtube_url(cls, url: str) -> bool:
123
+ """Check if URL is a YouTube video."""
124
+ for pattern in VideoElement.YOUTUBE_PATTERNS:
125
+ if pattern.match(url):
126
+ return True
127
+ return False
128
+
129
+ @classmethod
130
+ def _get_youtube_id(cls, url: str) -> Optional[str]:
131
+ """Extract YouTube video ID from URL."""
132
+ for pattern in VideoElement.YOUTUBE_PATTERNS:
133
+ match = pattern.match(url)
134
+ if match:
135
+ return match.group(1)
136
+ return None
137
+
138
+ @classmethod
139
+ def _extract_video_url(cls, video_data: Dict[str, Any]) -> str:
140
+ """Extract URL from video data, handling both external and uploaded videos."""
141
+ if video_data.get("type") == "external":
142
+ return video_data.get("external", {}).get("url", "")
143
+ elif video_data.get("type") == "file":
144
+ return video_data.get("file", {}).get("url", "")
145
+ return ""
146
+
123
147
  @classmethod
124
148
  def _extract_text_content(cls, rich_text: List[Dict[str, Any]]) -> str:
125
149
  """Extract plain text content from Notion rich_text elements."""
@@ -145,12 +169,13 @@ class VideoElement(NotionBlockElement):
145
169
  "Use video embeds when you want to include multimedia content directly in your document. "
146
170
  "Videos are useful for tutorials, demonstrations, presentations, or any content that benefits from visual explanation."
147
171
  )
148
- .with_syntax("@[Caption](https://example.com/video.mp4)")
172
+ .with_syntax('[video](https://example.com/video.mp4 "Optional caption")')
149
173
  .with_examples(
150
174
  [
151
- "@[How to use this feature](https://www.youtube.com/watch?v=dQw4w9WgXcQ)",
152
- "@[Product demo](https://example.com/videos/demo.mp4)",
153
- "@[](https://youtu.be/dQw4w9WgXcQ)",
175
+ "[video](https://www.youtube.com/watch?v=dQw4w9WgXcQ)",
176
+ '[video](https://example.com/videos/demo.mp4 "Product demo")',
177
+ '[video](https://youtu.be/dQw4w9WgXcQ "How to use this feature")',
178
+ '[video](https://example.com/tutorial.mp4 "Step-by-step tutorial")',
154
179
  ]
155
180
  )
156
181
  .build()
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+ from pydantic import BaseModel
5
+ from notionary.blocks.markdown_node import MarkdownNode
6
+
7
+
8
+ class VideoMarkdownBlockParams(BaseModel):
9
+ url: str
10
+ caption: Optional[str] = None
11
+
12
+
13
+ class VideoMarkdownNode(MarkdownNode):
14
+ """
15
+ Programmatic interface for creating Notion-style video blocks.
16
+ Example: [video](https://example.com/video.mp4 "Optional caption")
17
+ """
18
+
19
+ def __init__(self, url: str, caption: Optional[str] = None):
20
+ self.url = url
21
+ self.caption = caption
22
+
23
+ @classmethod
24
+ def from_params(cls, params: VideoMarkdownBlockParams) -> VideoMarkdownNode:
25
+ return cls(url=params.url, caption=params.caption)
26
+
27
+ def to_markdown(self) -> str:
28
+ if self.caption:
29
+ return f'[video]({self.url} "{self.caption}")'
30
+ return f"[video]({self.url})"
@@ -0,0 +1,4 @@
1
+ from .database import NotionDatabase
2
+ from .database_filter_builder import DatabaseFilterBuilder
3
+
4
+ __all__ = ["NotionDatabase", "DatabaseFilterBuilder"]