notionary 0.2.21__py3-none-any.whl → 0.2.23__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 (100) hide show
  1. notionary/blocks/_bootstrap.py +9 -1
  2. notionary/blocks/audio/audio_element.py +53 -28
  3. notionary/blocks/audio/audio_markdown_node.py +10 -4
  4. notionary/blocks/base_block_element.py +15 -3
  5. notionary/blocks/bookmark/bookmark_element.py +39 -36
  6. notionary/blocks/bookmark/bookmark_markdown_node.py +16 -17
  7. notionary/blocks/breadcrumbs/breadcrumb_element.py +2 -2
  8. notionary/blocks/bulleted_list/bulleted_list_element.py +21 -4
  9. notionary/blocks/callout/callout_element.py +20 -4
  10. notionary/blocks/child_database/__init__.py +11 -4
  11. notionary/blocks/child_database/child_database_element.py +59 -0
  12. notionary/blocks/child_database/child_database_models.py +7 -14
  13. notionary/blocks/child_page/child_page_element.py +94 -0
  14. notionary/blocks/client.py +0 -1
  15. notionary/blocks/code/code_element.py +51 -2
  16. notionary/blocks/code/code_markdown_node.py +52 -1
  17. notionary/blocks/column/column_element.py +9 -3
  18. notionary/blocks/column/column_list_element.py +18 -3
  19. notionary/blocks/divider/divider_element.py +3 -11
  20. notionary/blocks/embed/embed_element.py +27 -6
  21. notionary/blocks/equation/equation_element.py +94 -41
  22. notionary/blocks/equation/equation_element_markdown_node.py +8 -9
  23. notionary/blocks/file/file_element.py +56 -37
  24. notionary/blocks/file/file_element_markdown_node.py +9 -7
  25. notionary/blocks/guards.py +22 -0
  26. notionary/blocks/heading/heading_element.py +23 -4
  27. notionary/blocks/image_block/image_element.py +43 -38
  28. notionary/blocks/image_block/image_markdown_node.py +10 -5
  29. notionary/blocks/mixins/captions/__init__.py +4 -0
  30. notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +31 -0
  31. notionary/blocks/mixins/captions/caption_mixin.py +92 -0
  32. notionary/blocks/models.py +3 -1
  33. notionary/blocks/numbered_list/numbered_list_element.py +21 -4
  34. notionary/blocks/paragraph/paragraph_element.py +21 -5
  35. notionary/blocks/pdf/pdf_element.py +47 -41
  36. notionary/blocks/pdf/pdf_markdown_node.py +9 -7
  37. notionary/blocks/quote/quote_element.py +26 -9
  38. notionary/blocks/quote/quote_markdown_node.py +2 -2
  39. notionary/blocks/registry/block_registry.py +1 -46
  40. notionary/blocks/registry/block_registry_builder.py +8 -0
  41. notionary/blocks/rich_text/rich_text_models.py +62 -29
  42. notionary/blocks/rich_text/text_inline_formatter.py +432 -101
  43. notionary/blocks/syntax_prompt_builder.py +137 -0
  44. notionary/blocks/table/table_element.py +110 -9
  45. notionary/blocks/table_of_contents/table_of_contents_element.py +19 -2
  46. notionary/blocks/todo/todo_element.py +21 -4
  47. notionary/blocks/toggle/toggle_element.py +19 -3
  48. notionary/blocks/toggle/toggle_markdown_node.py +1 -1
  49. notionary/blocks/toggleable_heading/toggleable_heading_element.py +19 -4
  50. notionary/blocks/types.py +69 -0
  51. notionary/blocks/video/video_element.py +44 -39
  52. notionary/blocks/video/video_markdown_node.py +10 -5
  53. notionary/comments/__init__.py +26 -0
  54. notionary/comments/client.py +211 -0
  55. notionary/comments/models.py +129 -0
  56. notionary/database/client.py +23 -0
  57. notionary/file_upload/models.py +2 -2
  58. notionary/markdown/markdown_builder.py +34 -27
  59. notionary/page/client.py +21 -6
  60. notionary/page/notion_page.py +77 -2
  61. notionary/page/page_content_deleting_service.py +117 -0
  62. notionary/page/page_content_writer.py +89 -113
  63. notionary/page/page_context.py +64 -0
  64. notionary/page/reader/handler/__init__.py +2 -0
  65. notionary/page/reader/handler/base_block_renderer.py +4 -4
  66. notionary/page/reader/handler/block_rendering_context.py +5 -0
  67. notionary/page/reader/handler/line_renderer.py +16 -3
  68. notionary/page/reader/handler/numbered_list_renderer.py +85 -0
  69. notionary/page/reader/page_content_retriever.py +17 -5
  70. notionary/page/writer/handler/__init__.py +2 -0
  71. notionary/page/writer/handler/code_handler.py +12 -40
  72. notionary/page/writer/handler/column_handler.py +12 -12
  73. notionary/page/writer/handler/column_list_handler.py +13 -13
  74. notionary/page/writer/handler/equation_handler.py +74 -0
  75. notionary/page/writer/handler/line_handler.py +4 -4
  76. notionary/page/writer/handler/regular_line_handler.py +31 -37
  77. notionary/page/writer/handler/table_handler.py +8 -72
  78. notionary/page/writer/handler/toggle_handler.py +14 -12
  79. notionary/page/writer/handler/toggleable_heading_handler.py +22 -16
  80. notionary/page/writer/markdown_to_notion_converter.py +28 -9
  81. notionary/page/writer/markdown_to_notion_converter_context.py +30 -0
  82. notionary/page/writer/markdown_to_notion_formatting_post_processor.py +73 -0
  83. notionary/page/writer/markdown_to_notion_post_processor.py +0 -0
  84. notionary/page/writer/markdown_to_notion_text_length_post_processor.py +0 -0
  85. notionary/page/writer/notion_text_length_processor.py +150 -0
  86. notionary/shared/__init__.py +5 -0
  87. notionary/shared/name_to_id_resolver.py +203 -0
  88. notionary/telemetry/service.py +0 -1
  89. notionary/user/notion_user_manager.py +22 -95
  90. notionary/util/concurrency_limiter.py +0 -0
  91. notionary/workspace.py +4 -4
  92. notionary-0.2.23.dist-info/METADATA +235 -0
  93. {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/RECORD +96 -77
  94. notionary/page/markdown_whitespace_processor.py +0 -80
  95. notionary/page/notion_text_length_utils.py +0 -119
  96. notionary/user/notion_user_provider.py +0 -1
  97. notionary-0.2.21.dist-info/METADATA +0 -229
  98. /notionary/page/reader/handler/{context.py → equation_renderer.py} +0 -0
  99. {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/LICENSE +0 -0
  100. {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/WHEEL +0 -0
@@ -5,35 +5,28 @@ from typing import Optional
5
5
 
6
6
  from notionary.blocks.base_block_element import BaseBlockElement
7
7
  from notionary.blocks.file.file_element_models import ExternalFile, FileBlock, FileType
8
+ from notionary.blocks.mixins.captions import CaptionMixin
9
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
8
10
  from notionary.blocks.models import Block, BlockCreateResult, BlockType
9
- from notionary.blocks.paragraph.paragraph_models import (
10
- CreateParagraphBlock,
11
- ParagraphBlock,
12
- )
13
11
  from notionary.blocks.pdf.pdf_models import CreatePdfBlock
14
- from notionary.blocks.rich_text.rich_text_models import RichTextObject
15
- from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
16
12
 
17
13
 
18
- class PdfElement(BaseBlockElement):
14
+ class PdfElement(BaseBlockElement, CaptionMixin):
19
15
  """
20
16
  Handles conversion between Markdown PDF embeds and Notion PDF blocks.
21
17
 
22
18
  Markdown PDF syntax:
23
- - [pdf](https://example.com/document.pdf "Caption") # External URL
24
- - [pdf](notion://file_id_here "Caption") # Notion hosted file
25
- - [pdf](upload://upload_id_here "Caption") # File upload
26
- - [pdf](https://example.com/document.pdf) # Without caption
19
+ - [pdf](https://example.com/document.pdf) - External URL
20
+ - [pdf](https://example.com/document.pdf)(caption:Annual Report 2024) - URL with caption
21
+ - (caption:User Manual)[pdf](https://example.com/manual.pdf) - caption before URL
22
+ - [pdf](notion://file_id_here)(caption:Notion hosted file) - Notion hosted file
23
+ - [pdf](upload://upload_id_here)(caption:File upload) - File upload
27
24
 
28
25
  Supports all three PDF types: external, notion-hosted, and file uploads.
29
26
  """
30
27
 
31
- PATTERN = re.compile(
32
- r"^\[pdf\]\(" # prefix
33
- r'((?:https?://|notion://|upload://)[^\s\)"]+)' # URL with protocol
34
- r'(?:\s+"([^"]*)")?' # optional caption
35
- r"\)$"
36
- )
28
+ # Flexible pattern that can handle caption in any position
29
+ PDF_PATTERN = re.compile(r"\[pdf\]\(((?:https?://|notion://|upload://)[^\s\"]+)\)")
37
30
 
38
31
  @classmethod
39
32
  def match_notion(cls, block: Block) -> bool:
@@ -41,51 +34,64 @@ class PdfElement(BaseBlockElement):
41
34
  return block.type == BlockType.PDF and block.pdf
42
35
 
43
36
  @classmethod
44
- def markdown_to_notion(cls, text: str) -> BlockCreateResult:
45
- """Convert markdown PDF link to Notion FileBlock (used for PDF) followed by an empty paragraph."""
46
- match = cls.PATTERN.match(text.strip())
47
- if not match:
37
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
38
+ """Convert markdown PDF link to Notion FileBlock (used for PDF)."""
39
+ # Use our own regex to find the PDF URL
40
+ pdf_match = cls.PDF_PATTERN.search(text.strip())
41
+ if not pdf_match:
48
42
  return None
49
43
 
50
- url, caption_text = match.group(1), match.group(2) or ""
44
+ url = pdf_match.group(1)
45
+
46
+ # Use mixin to extract caption (if present anywhere in text)
47
+ caption_text = cls.extract_caption(text.strip())
48
+ caption_rich_text = cls.build_caption_rich_text(caption_text or "")
51
49
 
52
50
  # Build FileBlock using FileType enum (reused for PDF)
53
51
  pdf_block = FileBlock(
54
- type=FileType.EXTERNAL, external=ExternalFile(url=url), caption=[]
52
+ type=FileType.EXTERNAL,
53
+ external=ExternalFile(url=url),
54
+ caption=caption_rich_text,
55
55
  )
56
- if caption_text.strip():
57
- rt = RichTextObject.from_plain_text(caption_text)
58
- pdf_block.caption = [rt]
59
-
60
- empty_para = ParagraphBlock(rich_text=[])
61
56
 
62
- return [
63
- CreatePdfBlock(pdf=pdf_block),
64
- CreateParagraphBlock(paragraph=empty_para),
65
- ]
57
+ return CreatePdfBlock(pdf=pdf_block)
66
58
 
67
59
  @classmethod
68
- def notion_to_markdown(cls, block: Block) -> Optional[str]:
60
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
69
61
  if block.type != BlockType.PDF or not block.pdf:
70
62
  return None
71
63
 
72
64
  pb: FileBlock = block.pdf
73
65
 
74
- # Determine URL (only external and file types are valid for Markdown)
75
66
  if pb.type == FileType.EXTERNAL and pb.external:
76
67
  url = pb.external.url
77
68
  elif pb.type == FileType.FILE and pb.file:
78
69
  url = pb.file.url
79
70
  elif pb.type == FileType.FILE_UPLOAD:
80
- # Uploaded PDF has no stable URL for Markdown
81
71
  return None
82
72
  else:
83
73
  return None
84
74
 
85
- if not pb.caption:
86
- return f"[pdf]({url})"
75
+ result = f"[pdf]({url})"
76
+
77
+ # Add caption if present
78
+ caption_markdown = await cls.format_caption_for_markdown(pb.caption or [])
79
+ if caption_markdown:
80
+ result += caption_markdown
87
81
 
88
- caption_md = TextInlineFormatter.extract_text_with_formatting(pb.caption)
89
- if caption_md:
90
- return f'[pdf]({url} "{caption_md}")'
91
- return f"[pdf]({url})"
82
+ return result
83
+
84
+ @classmethod
85
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
86
+ """Get system prompt information for PDF blocks."""
87
+ return BlockElementMarkdownInformation(
88
+ block_type=cls.__name__,
89
+ description="PDF blocks embed and display PDF documents from external URLs with optional captions",
90
+ syntax_examples=[
91
+ "[pdf](https://example.com/document.pdf)",
92
+ "[pdf](https://example.com/report.pdf)(caption:Annual Report 2024)",
93
+ "(caption:User Manual)[pdf](https://example.com/manual.pdf)",
94
+ "[pdf](https://example.com/guide.pdf)(caption:**Important** documentation)",
95
+ ],
96
+ usage_guidelines="Use for embedding PDF documents that can be viewed inline. Supports external URLs and Notion-hosted files. Caption supports rich text formatting and should describe the PDF content.",
97
+ )
@@ -5,6 +5,7 @@ from typing import Optional
5
5
  from pydantic import BaseModel
6
6
 
7
7
  from notionary.markdown.markdown_node import MarkdownNode
8
+ from notionary.blocks.mixins.captions import CaptionMarkdownNodeMixin
8
9
 
9
10
 
10
11
  class PdfMarkdownNodeParams(BaseModel):
@@ -12,10 +13,9 @@ class PdfMarkdownNodeParams(BaseModel):
12
13
  caption: Optional[str] = None
13
14
 
14
15
 
15
- class PdfMarkdownNode(MarkdownNode):
16
+ class PdfMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
16
17
  """
17
18
  Programmatic interface for creating Notion-style Markdown PDF embeds.
18
- Example: [pdf](https://example.com/document.pdf "My Caption")
19
19
  """
20
20
 
21
21
  def __init__(self, url: str, caption: Optional[str] = None):
@@ -27,9 +27,11 @@ class PdfMarkdownNode(MarkdownNode):
27
27
  return cls(url=params.url, caption=params.caption)
28
28
 
29
29
  def to_markdown(self) -> str:
30
+ """Return the Markdown representation.
31
+
32
+ Examples:
33
+ - [pdf](https://example.com/document.pdf)
34
+ - [pdf](https://example.com/document.pdf)(caption:Critical safety information)
30
35
  """
31
- Convert to markdown as [pdf](url "caption") or [pdf](url) if caption is empty.
32
- """
33
- if self.caption:
34
- return f'[pdf]({self.url} "{self.caption}")'
35
- return f"[pdf]({self.url})"
36
+ base_markdown = f"[pdf]({self.url})"
37
+ return self.append_caption_to_markdown(base_markdown, self.caption)
@@ -4,6 +4,7 @@ import re
4
4
  from typing import Optional
5
5
 
6
6
  from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
7
8
  from notionary.blocks.models import Block, BlockCreateResult, BlockType
8
9
  from notionary.blocks.quote.quote_models import CreateQuoteBlock, QuoteBlock
9
10
  from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
@@ -15,19 +16,19 @@ class QuoteElement(BaseBlockElement):
15
16
  Handles conversion between Markdown quotes and Notion quote blocks.
16
17
 
17
18
  Markdown quote syntax:
18
- - [quote](Simple quote text)
19
+ - > Simple quote text
19
20
 
20
21
  Only single-line quotes without author metadata.
21
22
  """
22
23
 
23
- PATTERN = re.compile(r"^\[quote\]\(([^\n\r]+)\)$")
24
+ PATTERN = re.compile(r"^>\s*(.+)$")
24
25
 
25
26
  @classmethod
26
27
  def match_notion(cls, block: Block) -> bool:
27
28
  return block.type == BlockType.QUOTE and block.quote
28
29
 
29
30
  @classmethod
30
- def markdown_to_notion(cls, text: str) -> BlockCreateResult:
31
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
31
32
  """Convert markdown quote to Notion QuoteBlock."""
32
33
  match = cls.PATTERN.match(text.strip())
33
34
  if not match:
@@ -37,22 +38,38 @@ class QuoteElement(BaseBlockElement):
37
38
  if not content:
38
39
  return None
39
40
 
40
- # Parse inline formatting into rich text objects
41
- rich_text = TextInlineFormatter.parse_inline_formatting(content)
41
+ # Reject multiline quotes
42
+ if "\n" in content or "\r" in content:
43
+ return None
44
+
45
+ rich_text = await TextInlineFormatter.parse_inline_formatting(content)
42
46
 
43
- # Return a typed QuoteBlock
44
47
  quote_content = QuoteBlock(rich_text=rich_text, color=BlockColor.DEFAULT)
45
48
  return CreateQuoteBlock(quote=quote_content)
46
49
 
47
50
  @classmethod
48
- def notion_to_markdown(cls, block: Block) -> Optional[str]:
51
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
49
52
  if block.type != BlockType.QUOTE or not block.quote:
50
53
  return None
51
54
 
52
55
  rich = block.quote.rich_text
53
- text = TextInlineFormatter.extract_text_with_formatting(rich)
56
+ text = await TextInlineFormatter.extract_text_with_formatting(rich)
54
57
 
55
58
  if not text.strip():
56
59
  return None
57
60
 
58
- return f"[quote]({text.strip()})"
61
+ return f"> {text.strip()}"
62
+
63
+ @classmethod
64
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
65
+ """Get system prompt information for quote blocks."""
66
+ return BlockElementMarkdownInformation(
67
+ block_type=cls.__name__,
68
+ description="Quote blocks display highlighted quotations or emphasized text",
69
+ syntax_examples=[
70
+ "> This is an important quote",
71
+ "> The only way to do great work is to love what you do",
72
+ "> Innovation distinguishes between a leader and a follower",
73
+ ],
74
+ usage_guidelines="Use for quotations, important statements, or text that should be visually emphasized. Content should be meaningful and stand out from regular paragraphs.",
75
+ )
@@ -12,7 +12,7 @@ class QuoteMarkdownBlockParams(BaseModel):
12
12
  class QuoteMarkdownNode(MarkdownNode):
13
13
  """
14
14
  Programmatic interface for creating Notion-style quote blocks.
15
- Example: [quote](This is a quote)
15
+ Example: > This is a quote
16
16
  """
17
17
 
18
18
  def __init__(self, text: str):
@@ -23,4 +23,4 @@ class QuoteMarkdownNode(MarkdownNode):
23
23
  return cls(text=params.text)
24
24
 
25
25
  def to_markdown(self) -> str:
26
- return f"[quote]({self.text})"
26
+ return f"> {self.text}"
@@ -3,11 +3,8 @@ from __future__ import annotations
3
3
  from typing import Optional, Type
4
4
 
5
5
  from notionary.blocks.base_block_element import BaseBlockElement
6
- from notionary.blocks.models import Block, BlockCreateResult
7
6
  from notionary.blocks.registry.block_registry_builder import BlockRegistryBuilder
8
7
  from notionary.telemetry import (
9
- MarkdownToNotionConversionEvent,
10
- NotionToMarkdownConversionEvent,
11
8
  ProductTelemetry,
12
9
  )
13
10
 
@@ -61,6 +58,7 @@ class BlockRegistry:
61
58
  .with_equation()
62
59
  .with_table_of_contents()
63
60
  .with_breadcrumbs()
61
+ .with_child_database()
64
62
  .with_paragraphs() # position here is important - its a fallback!
65
63
  )
66
64
 
@@ -92,49 +90,6 @@ class BlockRegistry:
92
90
  """
93
91
  return element_class.__name__ in self._builder._elements
94
92
 
95
- def find_markdown_handler(self, text: str) -> Optional[Type[BaseBlockElement]]:
96
- """Find an element that can handle the given markdown text."""
97
- for element in self._builder._elements.values():
98
- if element.match_markdown(text):
99
- return element
100
- return None
101
-
102
- def markdown_to_notion(self, text: str) -> "BlockCreateResult":
103
- """Convert markdown to Notion block using registered elements."""
104
- handler = self.find_markdown_handler(text)
105
-
106
- if handler:
107
- self.telemetry.capture(
108
- MarkdownToNotionConversionEvent(
109
- handler_element_name=handler.__name__,
110
- )
111
- )
112
-
113
- return handler.markdown_to_notion(text)
114
- return None
115
-
116
- def notion_to_markdown(self, block: "Block") -> Optional[str]:
117
- """Convert Notion block to markdown using registered elements."""
118
- handler = self._find_notion_handler(block)
119
-
120
- if not handler:
121
- return None
122
-
123
- self.telemetry.capture(
124
- NotionToMarkdownConversionEvent(
125
- handler_element_name=handler.__name__,
126
- )
127
- )
128
-
129
- return handler.notion_to_markdown(block)
130
-
131
93
  def get_elements(self) -> list[Type[BaseBlockElement]]:
132
94
  """Get all registered elements."""
133
95
  return list(self._builder._elements.values())
134
-
135
- def _find_notion_handler(self, block: Block) -> Optional[Type[BaseBlockElement]]:
136
- """Find an element that can handle the given Notion block."""
137
- for element in self._builder._elements.values():
138
- if element.match_notion(block):
139
- return element
140
- return None
@@ -9,6 +9,7 @@ from notionary.blocks.bookmark import BookmarkElement
9
9
  from notionary.blocks.breadcrumbs import BreadcrumbElement
10
10
  from notionary.blocks.bulleted_list import BulletedListElement
11
11
  from notionary.blocks.callout import CalloutElement
12
+ from notionary.blocks.child_database import ChildDatabaseElement
12
13
  from notionary.blocks.code import CodeElement
13
14
  from notionary.blocks.column import ColumnElement, ColumnListElement
14
15
  from notionary.blocks.divider import DividerElement
@@ -70,6 +71,7 @@ class BlockRegistryBuilder:
70
71
  .with_equation()
71
72
  .with_table_of_contents()
72
73
  .with_breadcrumbs()
74
+ .with_child_database()
73
75
  ).build()
74
76
 
75
77
  def remove_element(self, element_class: Type[BaseBlockElement]) -> Self:
@@ -151,6 +153,9 @@ class BlockRegistryBuilder:
151
153
  def with_breadcrumbs(self) -> Self:
152
154
  return self._add_element(BreadcrumbElement)
153
155
 
156
+ def with_child_database(self) -> Self:
157
+ return self._add_element(ChildDatabaseElement)
158
+
154
159
  def without_headings(self) -> Self:
155
160
  return self.remove_element(HeadingElement)
156
161
 
@@ -213,6 +218,9 @@ class BlockRegistryBuilder:
213
218
  def without_breadcrumbs(self) -> Self:
214
219
  return self.remove_element(BreadcrumbElement)
215
220
 
221
+ def without_child_database(self) -> Self:
222
+ return self.remove_element(ChildDatabaseElement)
223
+
216
224
  def build(self) -> BlockRegistry:
217
225
  """
218
226
  Build and return the configured BlockRegistry instance.
@@ -1,9 +1,35 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from enum import Enum
3
4
  from typing import Optional
4
5
 
5
6
  from pydantic import BaseModel
6
- from typing_extensions import Literal
7
+
8
+
9
+ class RichTextType(str, Enum):
10
+ """Types of rich text objects."""
11
+
12
+ TEXT = "text"
13
+ MENTION = "mention"
14
+ EQUATION = "equation"
15
+
16
+
17
+ class MentionType(str, Enum):
18
+ """Types of mention objects."""
19
+
20
+ USER = "user"
21
+ PAGE = "page"
22
+ DATABASE = "database"
23
+ DATE = "date"
24
+ LINK_PREVIEW = "link_preview"
25
+ TEMPLATE_MENTION = "template_mention"
26
+
27
+
28
+ class TemplateMentionType(str, Enum):
29
+ """Types of template mentions."""
30
+
31
+ USER = "template_mention_user"
32
+ DATE = "template_mention_date"
7
33
 
8
34
 
9
35
  class TextAnnotations(BaseModel):
@@ -53,13 +79,11 @@ class MentionDate(BaseModel):
53
79
 
54
80
  class MentionTemplateMention(BaseModel):
55
81
  # Notion hat zwei Template-Mention-Typen
56
- type: Literal["template_mention_user", "template_mention_date"]
82
+ type: TemplateMentionType
57
83
 
58
84
 
59
85
  class MentionObject(BaseModel):
60
- type: Literal[
61
- "user", "page", "database", "date", "link_preview", "template_mention"
62
- ]
86
+ type: MentionType
63
87
  user: Optional[MentionUserRef] = None
64
88
  page: Optional[MentionPageRef] = None
65
89
  database: Optional[MentionDatabaseRef] = None
@@ -69,7 +93,7 @@ class MentionObject(BaseModel):
69
93
 
70
94
 
71
95
  class RichTextObject(BaseModel):
72
- type: Literal["text", "mention", "equation"] = "text"
96
+ type: RichTextType = RichTextType.TEXT
73
97
 
74
98
  text: Optional[TextContent] = None
75
99
  annotations: Optional[TextAnnotations] = None
@@ -83,26 +107,30 @@ class RichTextObject(BaseModel):
83
107
  @classmethod
84
108
  def from_plain_text(cls, content: str, **ann) -> RichTextObject:
85
109
  return cls(
86
- type="text",
110
+ type=RichTextType.TEXT,
87
111
  text=TextContent(content=content),
88
112
  annotations=TextAnnotations(**ann) if ann else TextAnnotations(),
89
113
  plain_text=content,
90
114
  )
91
115
 
92
116
  @classmethod
93
- def for_code_block(cls, content: str) -> RichTextObject:
94
- # keine annotations setzen → Notion Code-Highlight bleibt an
117
+ def for_caption(cls, content: str) -> RichTextObject:
95
118
  return cls(
96
- type="text",
119
+ type=RichTextType.TEXT,
97
120
  text=TextContent(content=content),
98
121
  annotations=None,
99
122
  plain_text=content,
100
123
  )
101
124
 
125
+ @classmethod
126
+ def for_code_block(cls, content: str) -> RichTextObject:
127
+ # keine annotations setzen → Notion Code-Highlight bleibt an
128
+ return cls.for_caption(content)
129
+
102
130
  @classmethod
103
131
  def for_link(cls, content: str, url: str, **ann) -> RichTextObject:
104
132
  return cls(
105
- type="text",
133
+ type=RichTextType.TEXT,
106
134
  text=TextContent(content=content, link=LinkObject(url=url)),
107
135
  annotations=TextAnnotations(**ann) if ann else TextAnnotations(),
108
136
  plain_text=content,
@@ -111,25 +139,29 @@ class RichTextObject(BaseModel):
111
139
  @classmethod
112
140
  def mention_user(cls, user_id: str) -> RichTextObject:
113
141
  return cls(
114
- type="mention",
115
- mention=MentionObject(type="user", user=MentionUserRef(id=user_id)),
142
+ type=RichTextType.MENTION,
143
+ mention=MentionObject(
144
+ type=MentionType.USER, user=MentionUserRef(id=user_id)
145
+ ),
116
146
  annotations=TextAnnotations(),
117
147
  )
118
148
 
119
149
  @classmethod
120
150
  def mention_page(cls, page_id: str) -> RichTextObject:
121
151
  return cls(
122
- type="mention",
123
- mention=MentionObject(type="page", page=MentionPageRef(id=page_id)),
152
+ type=RichTextType.MENTION,
153
+ mention=MentionObject(
154
+ type=MentionType.PAGE, page=MentionPageRef(id=page_id)
155
+ ),
124
156
  annotations=TextAnnotations(),
125
157
  )
126
158
 
127
159
  @classmethod
128
160
  def mention_database(cls, database_id: str) -> RichTextObject:
129
161
  return cls(
130
- type="mention",
162
+ type=RichTextType.MENTION,
131
163
  mention=MentionObject(
132
- type="database", database=MentionDatabaseRef(id=database_id)
164
+ type=MentionType.DATABASE, database=MentionDatabaseRef(id=database_id)
133
165
  ),
134
166
  annotations=TextAnnotations(),
135
167
  )
@@ -137,9 +169,9 @@ class RichTextObject(BaseModel):
137
169
  @classmethod
138
170
  def mention_link_preview(cls, url: str) -> RichTextObject:
139
171
  return cls(
140
- type="mention",
172
+ type=RichTextType.MENTION,
141
173
  mention=MentionObject(
142
- type="link_preview", link_preview=MentionLinkPreview(url=url)
174
+ type=MentionType.LINK_PREVIEW, link_preview=MentionLinkPreview(url=url)
143
175
  ),
144
176
  annotations=TextAnnotations(),
145
177
  )
@@ -149,9 +181,10 @@ class RichTextObject(BaseModel):
149
181
  cls, start: str, end: str | None = None, time_zone: str | None = None
150
182
  ) -> RichTextObject:
151
183
  return cls(
152
- type="mention",
184
+ type=RichTextType.MENTION,
153
185
  mention=MentionObject(
154
- type="date", date=MentionDate(start=start, end=end, time_zone=time_zone)
186
+ type=MentionType.DATE,
187
+ date=MentionDate(start=start, end=end, time_zone=time_zone),
155
188
  ),
156
189
  annotations=TextAnnotations(),
157
190
  )
@@ -159,10 +192,10 @@ class RichTextObject(BaseModel):
159
192
  @classmethod
160
193
  def mention_template_user(cls) -> RichTextObject:
161
194
  return cls(
162
- type="mention",
195
+ type=RichTextType.MENTION,
163
196
  mention=MentionObject(
164
- type="template_mention",
165
- template_mention=MentionTemplateMention(type="template_mention_user"),
197
+ type=MentionType.TEMPLATE_MENTION,
198
+ template_mention=MentionTemplateMention(type=TemplateMentionType.USER),
166
199
  ),
167
200
  annotations=TextAnnotations(),
168
201
  )
@@ -170,10 +203,10 @@ class RichTextObject(BaseModel):
170
203
  @classmethod
171
204
  def mention_template_date(cls) -> RichTextObject:
172
205
  return cls(
173
- type="mention",
206
+ type=RichTextType.MENTION,
174
207
  mention=MentionObject(
175
- type="template_mention",
176
- template_mention=MentionTemplateMention(type="template_mention_date"),
208
+ type=MentionType.TEMPLATE_MENTION,
209
+ template_mention=MentionTemplateMention(type=TemplateMentionType.DATE),
177
210
  ),
178
211
  annotations=TextAnnotations(),
179
212
  )
@@ -181,8 +214,8 @@ class RichTextObject(BaseModel):
181
214
  @classmethod
182
215
  def equation_inline(cls, expression: str) -> RichTextObject:
183
216
  return cls(
184
- type="equation",
217
+ type=RichTextType.EQUATION,
185
218
  equation=EquationObject(expression=expression),
186
219
  annotations=TextAnnotations(),
187
- plain_text=expression, # Notion liefert plain_text serverseitig; für Roundtrip hilfreich
220
+ plain_text=expression,
188
221
  )