notionary 0.2.28__py3-none-any.whl → 0.3.1__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 (149) hide show
  1. notionary/__init__.py +9 -2
  2. notionary/blocks/__init__.py +5 -0
  3. notionary/blocks/client.py +6 -4
  4. notionary/blocks/enums.py +28 -1
  5. notionary/blocks/rich_text/markdown_rich_text_converter.py +14 -0
  6. notionary/blocks/rich_text/models.py +14 -0
  7. notionary/blocks/rich_text/name_id_resolver/__init__.py +2 -0
  8. notionary/blocks/rich_text/name_id_resolver/data_source.py +32 -0
  9. notionary/blocks/rich_text/rich_text_markdown_converter.py +12 -0
  10. notionary/blocks/rich_text/rich_text_patterns.py +3 -0
  11. notionary/blocks/schemas.py +42 -10
  12. notionary/comments/__init__.py +5 -0
  13. notionary/comments/client.py +7 -10
  14. notionary/comments/factory.py +4 -6
  15. notionary/data_source/http/data_source_instance_client.py +14 -4
  16. notionary/data_source/properties/{models.py → schemas.py} +4 -8
  17. notionary/data_source/query/__init__.py +9 -0
  18. notionary/data_source/query/builder.py +38 -10
  19. notionary/data_source/query/schema.py +13 -10
  20. notionary/data_source/query/validator.py +11 -11
  21. notionary/data_source/schema/registry.py +104 -0
  22. notionary/data_source/schema/service.py +136 -0
  23. notionary/data_source/schemas.py +1 -1
  24. notionary/data_source/service.py +29 -103
  25. notionary/database/service.py +17 -60
  26. notionary/exceptions/__init__.py +5 -1
  27. notionary/exceptions/block_parsing.py +21 -0
  28. notionary/exceptions/search.py +24 -0
  29. notionary/http/client.py +9 -10
  30. notionary/http/models.py +5 -4
  31. notionary/page/content/factory.py +10 -3
  32. notionary/page/content/markdown/builder.py +76 -154
  33. notionary/page/content/markdown/nodes/__init__.py +0 -2
  34. notionary/page/content/markdown/nodes/audio.py +1 -1
  35. notionary/page/content/markdown/nodes/base.py +1 -1
  36. notionary/page/content/markdown/nodes/bookmark.py +1 -1
  37. notionary/page/content/markdown/nodes/breadcrumb.py +1 -1
  38. notionary/page/content/markdown/nodes/bulleted_list.py +31 -8
  39. notionary/page/content/markdown/nodes/callout.py +12 -10
  40. notionary/page/content/markdown/nodes/code.py +3 -5
  41. notionary/page/content/markdown/nodes/columns.py +39 -21
  42. notionary/page/content/markdown/nodes/container.py +64 -0
  43. notionary/page/content/markdown/nodes/divider.py +1 -1
  44. notionary/page/content/markdown/nodes/embed.py +1 -1
  45. notionary/page/content/markdown/nodes/equation.py +1 -1
  46. notionary/page/content/markdown/nodes/file.py +1 -1
  47. notionary/page/content/markdown/nodes/heading.py +26 -6
  48. notionary/page/content/markdown/nodes/image.py +1 -1
  49. notionary/page/content/markdown/nodes/mixins/__init__.py +5 -0
  50. notionary/page/content/markdown/nodes/mixins/caption.py +1 -1
  51. notionary/page/content/markdown/nodes/numbered_list.py +28 -5
  52. notionary/page/content/markdown/nodes/paragraph.py +1 -1
  53. notionary/page/content/markdown/nodes/pdf.py +1 -1
  54. notionary/page/content/markdown/nodes/quote.py +17 -5
  55. notionary/page/content/markdown/nodes/space.py +1 -1
  56. notionary/page/content/markdown/nodes/table.py +1 -1
  57. notionary/page/content/markdown/nodes/table_of_contents.py +1 -1
  58. notionary/page/content/markdown/nodes/todo.py +23 -7
  59. notionary/page/content/markdown/nodes/toggle.py +13 -14
  60. notionary/page/content/markdown/nodes/video.py +1 -1
  61. notionary/page/content/parser/context.py +98 -21
  62. notionary/page/content/parser/factory.py +1 -10
  63. notionary/page/content/parser/parsers/__init__.py +0 -2
  64. notionary/page/content/parser/parsers/audio.py +1 -1
  65. notionary/page/content/parser/parsers/base.py +1 -1
  66. notionary/page/content/parser/parsers/bookmark.py +1 -1
  67. notionary/page/content/parser/parsers/breadcrumb.py +1 -1
  68. notionary/page/content/parser/parsers/bulleted_list.py +52 -8
  69. notionary/page/content/parser/parsers/callout.py +55 -84
  70. notionary/page/content/parser/parsers/caption.py +1 -1
  71. notionary/page/content/parser/parsers/code.py +5 -5
  72. notionary/page/content/parser/parsers/column.py +23 -64
  73. notionary/page/content/parser/parsers/column_list.py +45 -45
  74. notionary/page/content/parser/parsers/divider.py +1 -1
  75. notionary/page/content/parser/parsers/embed.py +1 -1
  76. notionary/page/content/parser/parsers/equation.py +1 -1
  77. notionary/page/content/parser/parsers/file.py +1 -1
  78. notionary/page/content/parser/parsers/heading.py +65 -8
  79. notionary/page/content/parser/parsers/image.py +1 -1
  80. notionary/page/content/parser/parsers/numbered_list.py +52 -8
  81. notionary/page/content/parser/parsers/paragraph.py +3 -2
  82. notionary/page/content/parser/parsers/pdf.py +1 -1
  83. notionary/page/content/parser/parsers/quote.py +75 -15
  84. notionary/page/content/parser/parsers/space.py +14 -8
  85. notionary/page/content/parser/parsers/table.py +1 -1
  86. notionary/page/content/parser/parsers/table_of_contents.py +1 -1
  87. notionary/page/content/parser/parsers/todo.py +57 -19
  88. notionary/page/content/parser/parsers/toggle.py +17 -74
  89. notionary/page/content/parser/parsers/video.py +1 -1
  90. notionary/page/content/parser/post_processing/handlers/rich_text_length.py +6 -4
  91. notionary/page/content/parser/post_processing/handlers/rich_text_length_truncation.py +43 -22
  92. notionary/page/content/parser/pre_processsing/handlers/__init__.py +4 -0
  93. notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +108 -54
  94. notionary/page/content/parser/pre_processsing/handlers/indentation.py +86 -0
  95. notionary/page/content/parser/pre_processsing/handlers/video_syntax.py +66 -0
  96. notionary/page/content/parser/pre_processsing/handlers/whitespace.py +14 -7
  97. notionary/page/content/parser/service.py +9 -0
  98. notionary/page/content/renderer/context.py +5 -2
  99. notionary/page/content/renderer/factory.py +2 -11
  100. notionary/page/content/renderer/post_processing/handlers/__init__.py +2 -2
  101. notionary/page/content/renderer/post_processing/handlers/numbered_list.py +156 -0
  102. notionary/page/content/renderer/renderers/__init__.py +0 -2
  103. notionary/page/content/renderer/renderers/base.py +1 -1
  104. notionary/page/content/renderer/renderers/bulleted_list.py +1 -1
  105. notionary/page/content/renderer/renderers/callout.py +6 -21
  106. notionary/page/content/renderer/renderers/captioned_block.py +1 -1
  107. notionary/page/content/renderer/renderers/column.py +28 -19
  108. notionary/page/content/renderer/renderers/column_list.py +24 -11
  109. notionary/page/content/renderer/renderers/heading.py +53 -27
  110. notionary/page/content/renderer/renderers/numbered_list.py +6 -5
  111. notionary/page/content/renderer/renderers/quote.py +1 -1
  112. notionary/page/content/renderer/renderers/todo.py +1 -1
  113. notionary/page/content/renderer/renderers/toggle.py +6 -7
  114. notionary/page/content/service.py +4 -1
  115. notionary/page/content/syntax/__init__.py +4 -0
  116. notionary/page/content/syntax/grammar.py +10 -0
  117. notionary/page/content/syntax/models.py +0 -2
  118. notionary/page/content/syntax/{service.py → registry.py} +31 -91
  119. notionary/page/properties/client.py +3 -3
  120. notionary/page/properties/models.py +3 -2
  121. notionary/page/properties/service.py +18 -3
  122. notionary/page/service.py +22 -80
  123. notionary/shared/entity/service.py +94 -36
  124. notionary/shared/models/cover.py +1 -1
  125. notionary/shared/typings.py +3 -0
  126. notionary/user/base.py +60 -11
  127. notionary/user/factory.py +0 -0
  128. notionary/utils/decorators.py +122 -0
  129. notionary/utils/fuzzy.py +18 -6
  130. notionary/utils/mixins/logging.py +38 -27
  131. notionary/utils/pagination.py +70 -16
  132. notionary/workspace/__init__.py +2 -1
  133. notionary/workspace/client.py +4 -2
  134. notionary/workspace/query/__init__.py +3 -0
  135. notionary/workspace/query/builder.py +25 -1
  136. notionary/workspace/query/models.py +12 -3
  137. notionary/workspace/query/service.py +57 -32
  138. notionary/workspace/service.py +31 -21
  139. {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/METADATA +35 -105
  140. notionary-0.3.1.dist-info/RECORD +211 -0
  141. notionary/page/content/markdown/nodes/toggleable_heading.py +0 -35
  142. notionary/page/content/parser/parsers/toggleable_heading.py +0 -150
  143. notionary/page/content/renderer/post_processing/handlers/numbered_list_placeholdere.py +0 -62
  144. notionary/page/content/renderer/renderers/toggleable_heading.py +0 -78
  145. notionary/utils/async_retry.py +0 -39
  146. notionary/utils/singleton.py +0 -13
  147. notionary-0.2.28.dist-info/RECORD +0 -200
  148. {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/WHEEL +0 -0
  149. {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/licenses/LICENSE +0 -0
notionary/__init__.py CHANGED
@@ -2,6 +2,13 @@ from .data_source.service import NotionDataSource
2
2
  from .database.service import NotionDatabase
3
3
  from .page.content.markdown.builder import MarkdownBuilder
4
4
  from .page.service import NotionPage
5
- from .workspace import NotionWorkspace
5
+ from .workspace import NotionWorkspace, NotionWorkspaceQueryConfigBuilder
6
6
 
7
- __all__ = ["MarkdownBuilder", "NotionDataSource", "NotionDatabase", "NotionPage", "NotionWorkspace", "NotionWorkspace"]
7
+ __all__ = [
8
+ "MarkdownBuilder",
9
+ "NotionDataSource",
10
+ "NotionDatabase",
11
+ "NotionPage",
12
+ "NotionWorkspace",
13
+ "NotionWorkspaceQueryConfigBuilder",
14
+ ]
@@ -0,0 +1,5 @@
1
+ from .enums import CodingLanguage
2
+
3
+ # Import from schemas aswell
4
+
5
+ __all__ = ["CodingLanguage"]
@@ -1,7 +1,7 @@
1
- from typing import Any
2
-
3
1
  from notionary.blocks.schemas import Block, BlockChildrenResponse, BlockCreatePayload
4
2
  from notionary.http.client import NotionHttpClient
3
+ from notionary.shared.typings import JsonDict
4
+ from notionary.utils.decorators import time_execution_async
5
5
  from notionary.utils.pagination import paginate_notion_api
6
6
 
7
7
 
@@ -16,6 +16,7 @@ class NotionBlockHttpClient(NotionHttpClient):
16
16
  self.logger.debug("Deleting block: %s", block_id)
17
17
  await self.delete(f"blocks/{block_id}")
18
18
 
19
+ @time_execution_async()
19
20
  async def get_block_tree(self, parent_block_id: str) -> list[Block]:
20
21
  blocks_at_this_level = await self.get_all_block_children(parent_block_id)
21
22
 
@@ -26,6 +27,7 @@ class NotionBlockHttpClient(NotionHttpClient):
26
27
 
27
28
  return blocks_at_this_level
28
29
 
30
+ @time_execution_async()
29
31
  async def get_all_block_children(self, parent_block_id: str) -> list[Block]:
30
32
  self.logger.debug("Retrieving all children for block: %s", parent_block_id)
31
33
 
@@ -73,11 +75,11 @@ class NotionBlockHttpClient(NotionHttpClient):
73
75
  batches.append(batch)
74
76
  return batches
75
77
 
76
- def _serialize_blocks(self, blocks: list[BlockCreatePayload]) -> list[dict[str, Any]]:
78
+ def _serialize_blocks(self, blocks: list[BlockCreatePayload]) -> list[JsonDict]:
77
79
  return [block.model_dump(exclude_none=True) for block in blocks]
78
80
 
79
81
  async def _send_append_request(
80
- self, block_id: str, children: list[dict[str, Any]], after_block_id: str | None = None
82
+ self, block_id: str, children: list[JsonDict], after_block_id: str | None = None
81
83
  ) -> BlockChildrenResponse:
82
84
  payload = {"children": children}
83
85
  if after_block_id:
notionary/blocks/enums.py CHANGED
@@ -68,7 +68,7 @@ class FileType(StrEnum):
68
68
  FILE_UPLOAD = "file_upload"
69
69
 
70
70
 
71
- class CodeLanguage(StrEnum):
71
+ class CodingLanguage(StrEnum):
72
72
  ABAP = "abap"
73
73
  ARDUINO = "arduino"
74
74
  BASH = "bash"
@@ -165,3 +165,30 @@ class CodeLanguage(StrEnum):
165
165
  return member
166
166
 
167
167
  return default if default is not None else cls.PLAIN_TEXT
168
+
169
+
170
+ class VideoFileType(StrEnum):
171
+ AMV = ".amv"
172
+ ASF = ".asf"
173
+ AVI = ".avi"
174
+ F4V = ".f4v"
175
+ FLV = ".flv"
176
+ GIFV = ".gifv"
177
+ MKV = ".mkv"
178
+ MOV = ".mov"
179
+ MPG = ".mpg"
180
+ MPEG = ".mpeg"
181
+ MPV = ".mpv"
182
+ MP4 = ".mp4"
183
+ M4V = ".m4v"
184
+ QT = ".qt"
185
+ WMV = ".wmv"
186
+
187
+ @classmethod
188
+ def get_all_extensions(cls) -> set[str]:
189
+ return {ext.value for ext in cls}
190
+
191
+ @classmethod
192
+ def is_valid_extension(cls, filename: str) -> bool:
193
+ lower_filename = filename.lower()
194
+ return any(lower_filename.endswith(ext.value) for ext in cls)
@@ -7,6 +7,7 @@ from typing import ClassVar
7
7
  from notionary.blocks.rich_text.models import MentionType, RichText, RichTextType, TextAnnotations
8
8
  from notionary.blocks.rich_text.name_id_resolver import (
9
9
  DatabaseNameIdResolver,
10
+ DataSourceNameIdResolver,
10
11
  NameIdResolver,
11
12
  PageNameIdResolver,
12
13
  PersonNameIdResolver,
@@ -44,10 +45,12 @@ class MarkdownRichTextConverter:
44
45
  *,
45
46
  page_resolver: NameIdResolver | None = None,
46
47
  database_resolver: NameIdResolver | None = None,
48
+ data_source_resolver: NameIdResolver | None = None,
47
49
  person_resolver: NameIdResolver | None = None,
48
50
  ):
49
51
  self.page_resolver = page_resolver or PageNameIdResolver()
50
52
  self.database_resolver = database_resolver or DatabaseNameIdResolver()
53
+ self.data_source_resolver = data_source_resolver or DataSourceNameIdResolver()
51
54
  self.person_resolver = person_resolver or PersonNameIdResolver()
52
55
  self.format_handlers = self._setup_format_handlers()
53
56
 
@@ -64,6 +67,7 @@ class MarkdownRichTextConverter:
64
67
  PatternHandler(RichTextPatterns.COLOR, self._handle_color_pattern),
65
68
  PatternHandler(RichTextPatterns.PAGE_MENTION, self._handle_page_mention_pattern),
66
69
  PatternHandler(RichTextPatterns.DATABASE_MENTION, self._handle_database_mention_pattern),
70
+ PatternHandler(RichTextPatterns.DATASOURCE_MENTION, self._handle_data_source_mention_pattern),
67
71
  PatternHandler(RichTextPatterns.USER_MENTION, self._handle_user_mention_pattern),
68
72
  ]
69
73
 
@@ -119,6 +123,7 @@ class MarkdownRichTextConverter:
119
123
  async_handlers = {
120
124
  self._handle_page_mention_pattern,
121
125
  self._handle_database_mention_pattern,
126
+ self._handle_data_source_mention_pattern,
122
127
  self._handle_color_pattern, # Color pattern needs async for recursive parsing
123
128
  self._handle_user_mention_pattern,
124
129
  }
@@ -210,6 +215,15 @@ class MarkdownRichTextConverter:
210
215
  mention_type=MentionType.DATABASE,
211
216
  )
212
217
 
218
+ async def _handle_data_source_mention_pattern(self, match: Match) -> RichText:
219
+ identifier = match.group(1)
220
+ return await self._create_mention_or_fallback(
221
+ identifier=identifier,
222
+ resolve_func=self.data_source_resolver.resolve_name_to_id,
223
+ create_mention_func=RichText.mention_data_source,
224
+ mention_type=MentionType.DATASOURCE,
225
+ )
226
+
213
227
  async def _handle_user_mention_pattern(self, match: Match) -> RichText:
214
228
  identifier = match.group(1)
215
229
  return await self._create_mention_or_fallback(
@@ -16,6 +16,7 @@ class MentionType(StrEnum):
16
16
  USER = "user"
17
17
  PAGE = "page"
18
18
  DATABASE = "database"
19
+ DATASOURCE = "data_source"
19
20
  DATE = "date"
20
21
  LINK_PREVIEW = "link_preview"
21
22
  TEMPLATE_MENTION = "template_mention"
@@ -60,6 +61,10 @@ class MentionDatabaseRef(BaseModel):
60
61
  id: str
61
62
 
62
63
 
64
+ class MentionDataSourceRef(BaseModel):
65
+ id: str
66
+
67
+
63
68
  class MentionLinkPreview(BaseModel):
64
69
  url: str
65
70
 
@@ -81,6 +86,7 @@ class MentionObject(BaseModel):
81
86
  user: MentionUserRef | None = None
82
87
  page: MentionPageRef | None = None
83
88
  database: MentionDatabaseRef | None = None
89
+ data_source: MentionDataSourceRef | None = None
84
90
  date: MentionDate | None = None
85
91
  link_preview: MentionLinkPreview | None = None
86
92
  template_mention: MentionTemplateMention | None = None
@@ -154,6 +160,14 @@ class RichText(BaseModel):
154
160
  annotations=TextAnnotations(),
155
161
  )
156
162
 
163
+ @classmethod
164
+ def mention_data_source(cls, data_source_id: str) -> Self:
165
+ return cls(
166
+ type=RichTextType.MENTION,
167
+ mention=MentionObject(type=MentionType.DATASOURCE, data_source=MentionDataSourceRef(id=data_source_id)),
168
+ annotations=TextAnnotations(),
169
+ )
170
+
157
171
  @classmethod
158
172
  def equation_inline(cls, expression: str) -> Self:
159
173
  return cls(
@@ -1,9 +1,11 @@
1
+ from .data_source import DataSourceNameIdResolver
1
2
  from .database import DatabaseNameIdResolver
2
3
  from .page import PageNameIdResolver
3
4
  from .person import PersonNameIdResolver
4
5
  from .port import NameIdResolver
5
6
 
6
7
  __all__ = [
8
+ "DataSourceNameIdResolver",
7
9
  "DatabaseNameIdResolver",
8
10
  "NameIdResolver",
9
11
  "PageNameIdResolver",
@@ -0,0 +1,32 @@
1
+ from typing import override
2
+
3
+ from notionary.blocks.rich_text.name_id_resolver.port import NameIdResolver
4
+ from notionary.workspace.query.service import WorkspaceQueryService
5
+
6
+
7
+ # !!! in the notion api mentions that reference datasources are not provided yet (it's a limiation of the API as of now)
8
+ class DataSourceNameIdResolver(NameIdResolver):
9
+ def __init__(self, workspace_query_service: WorkspaceQueryService | None = None) -> None:
10
+ self._workspace_query_service = workspace_query_service or WorkspaceQueryService()
11
+
12
+ @override
13
+ async def resolve_name_to_id(self, name: str) -> str | None:
14
+ if not name:
15
+ return None
16
+
17
+ cleaned_name = name.strip()
18
+ data_source = await self._workspace_query_service.find_data_source(query=cleaned_name)
19
+ return data_source.id if data_source else None
20
+
21
+ @override
22
+ async def resolve_id_to_name(self, data_source_id: str) -> str | None:
23
+ if not data_source_id:
24
+ return None
25
+
26
+ try:
27
+ from notionary import NotionDataSource
28
+
29
+ data_source = await NotionDataSource.from_id(data_source_id)
30
+ return data_source.title if data_source else None
31
+ except Exception:
32
+ return None
@@ -8,6 +8,7 @@ from notionary.blocks.rich_text.models import (
8
8
  )
9
9
  from notionary.blocks.rich_text.name_id_resolver import (
10
10
  DatabaseNameIdResolver,
11
+ DataSourceNameIdResolver,
11
12
  NameIdResolver,
12
13
  PageNameIdResolver,
13
14
  PersonNameIdResolver,
@@ -23,10 +24,12 @@ class RichTextToMarkdownConverter:
23
24
  *,
24
25
  page_resolver: NameIdResolver | None = None,
25
26
  database_resolver: NameIdResolver | None = None,
27
+ data_source_resolver: NameIdResolver | None = None,
26
28
  person_resolver: NameIdResolver | None = None,
27
29
  ) -> None:
28
30
  self.page_resolver = page_resolver or PageNameIdResolver()
29
31
  self.database_resolver = database_resolver or DatabaseNameIdResolver()
32
+ self.data_source_resolver = data_source_resolver or DataSourceNameIdResolver()
30
33
  self.person_resolver = person_resolver or PersonNameIdResolver()
31
34
 
32
35
  async def to_markdown(self, rich_text: list[RichText]) -> str:
@@ -65,6 +68,9 @@ class RichTextToMarkdownConverter:
65
68
  elif mention.type == MentionType.DATABASE and mention.database:
66
69
  return await self._extract_database_mention_markdown(mention.database.id)
67
70
 
71
+ elif mention.type == MentionType.DATASOURCE and mention.data_source:
72
+ return await self._extract_data_source_mention_markdown(mention.data_source.id)
73
+
68
74
  elif mention.type == MentionType.USER and mention.user:
69
75
  return await self._extract_user_mention_markdown(mention.user.id)
70
76
 
@@ -81,6 +87,10 @@ class RichTextToMarkdownConverter:
81
87
  database_name = await self.database_resolver.resolve_id_to_name(database_id)
82
88
  return f"@database[{database_name or database_id}]"
83
89
 
90
+ async def _extract_data_source_mention_markdown(self, data_source_id: str) -> str:
91
+ data_source_name = await self.data_source_resolver.resolve_id_to_name(data_source_id)
92
+ return f"@datasource[{data_source_name or data_source_id}]"
93
+
84
94
  async def _extract_user_mention_markdown(self, user_id: str) -> str:
85
95
  user_name = await self.person_resolver.resolve_id_to_name(user_id)
86
96
  return f"@user[{user_name or user_id}]"
@@ -122,11 +132,13 @@ async def convert_rich_text_to_markdown(
122
132
  *,
123
133
  page_resolver: NameIdResolver | None = None,
124
134
  database_resolver: NameIdResolver | None = None,
135
+ data_source_resolver: NameIdResolver | None = None,
125
136
  person_resolver: NameIdResolver | None = None,
126
137
  ) -> str:
127
138
  converter = RichTextToMarkdownConverter(
128
139
  page_resolver=page_resolver,
129
140
  database_resolver=database_resolver,
141
+ data_source_resolver=data_source_resolver,
130
142
  person_resolver=person_resolver,
131
143
  )
132
144
  return await converter.to_markdown(rich_text)
@@ -35,5 +35,8 @@ class RichTextPatterns(StrEnum):
35
35
  DATABASE_MENTION = r"@database\[([^\]]+)\]"
36
36
  """Matches a Notion database mention by name or ID. Example: `@database[Tasks]`."""
37
37
 
38
+ DATASOURCE_MENTION = r"@datasource\[([^\]]+)\]"
39
+ """Matches a Notion data source mention by name or ID. Example: `@datasource[My Data]`."""
40
+
38
41
  USER_MENTION = r"@user\[([^\]]+)\]"
39
42
  """Matches a Notion user mention by name or ID. Example: `@user[Some Person]`."""
@@ -4,7 +4,7 @@ from typing import Annotated, Literal
4
4
 
5
5
  from pydantic import BaseModel, ConfigDict, Field
6
6
 
7
- from notionary.blocks.enums import BlockColor, BlockType, CodeLanguage, FileType
7
+ from notionary.blocks.enums import BlockColor, BlockType, CodingLanguage, FileType
8
8
  from notionary.blocks.rich_text.models import RichText
9
9
  from notionary.shared.models.icon import Icon
10
10
  from notionary.shared.models.parent import Parent
@@ -129,19 +129,27 @@ class CreateBreadcrumbBlock(BaseModel):
129
129
  # ============================================================================
130
130
 
131
131
 
132
- class BulletedListItemData(BaseModel):
132
+ class BaseBulletedListItemData(BaseModel):
133
133
  rich_text: list[RichText]
134
134
  color: BlockColor = BlockColor.DEFAULT
135
135
 
136
136
 
137
+ class BulletedListItemData(BaseBulletedListItemData):
138
+ children: list[Block] | None = None
139
+
140
+
137
141
  class BulletedListItemBlock(BaseBlock):
138
142
  type: Literal[BlockType.BULLETED_LIST_ITEM] = BlockType.BULLETED_LIST_ITEM
139
143
  bulleted_list_item: BulletedListItemData
140
144
 
141
145
 
146
+ class CreateBulletedListItemData(BaseBulletedListItemData):
147
+ children: list[BlockCreatePayload] | None = None
148
+
149
+
142
150
  class CreateBulletedListItemBlock(BaseModel):
143
151
  type: Literal[BlockType.BULLETED_LIST_ITEM] = BlockType.BULLETED_LIST_ITEM
144
- bulleted_list_item: BulletedListItemData
152
+ bulleted_list_item: CreateBulletedListItemData
145
153
 
146
154
 
147
155
  # ============================================================================
@@ -219,7 +227,7 @@ class CreateChildDatabaseBlock(BaseModel):
219
227
  class CodeData(BaseModel):
220
228
  caption: list[RichText] = Field(default_factory=list)
221
229
  rich_text: list[RichText]
222
- language: CodeLanguage = CodeLanguage.PLAIN_TEXT
230
+ language: CodingLanguage = CodingLanguage.PLAIN_TEXT
223
231
 
224
232
  model_config = ConfigDict(arbitrary_types_allowed=True)
225
233
 
@@ -433,19 +441,27 @@ class CreateImageBlock(BaseModel):
433
441
  # ============================================================================
434
442
 
435
443
 
436
- class NumberedListItemData(BaseModel):
444
+ class BaseNumberedListItemData(BaseModel):
437
445
  rich_text: list[RichText]
438
446
  color: BlockColor = BlockColor.DEFAULT
439
447
 
440
448
 
449
+ class NumberedListItemData(BaseNumberedListItemData):
450
+ children: list[Block] | None = None
451
+
452
+
441
453
  class NumberedListItemBlock(BaseBlock):
442
454
  type: Literal[BlockType.NUMBERED_LIST_ITEM] = BlockType.NUMBERED_LIST_ITEM
443
455
  numbered_list_item: NumberedListItemData
444
456
 
445
457
 
458
+ class CreateNumberedListItemData(BaseNumberedListItemData):
459
+ children: list[BlockCreatePayload] | None = None
460
+
461
+
446
462
  class CreateNumberedListItemBlock(BaseModel):
447
463
  type: Literal[BlockType.NUMBERED_LIST_ITEM] = BlockType.NUMBERED_LIST_ITEM
448
- numbered_list_item: NumberedListItemData
464
+ numbered_list_item: CreateNumberedListItemData
449
465
 
450
466
 
451
467
  # ============================================================================
@@ -453,19 +469,27 @@ class CreateNumberedListItemBlock(BaseModel):
453
469
  # ============================================================================
454
470
 
455
471
 
456
- class ParagraphData(BaseModel):
472
+ class BaseParagraphData(BaseModel):
457
473
  rich_text: list[RichText]
458
474
  color: BlockColor = BlockColor.DEFAULT
459
475
 
460
476
 
477
+ class ParagraphData(BaseParagraphData):
478
+ children: list[Block] | None = None
479
+
480
+
461
481
  class ParagraphBlock(BaseBlock):
462
482
  type: Literal[BlockType.PARAGRAPH] = BlockType.PARAGRAPH
463
483
  paragraph: ParagraphData
464
484
 
465
485
 
486
+ class CreateParagraphData(BaseParagraphData):
487
+ children: list[BlockCreatePayload] | None = None
488
+
489
+
466
490
  class CreateParagraphBlock(BaseModel):
467
491
  type: Literal[BlockType.PARAGRAPH] = BlockType.PARAGRAPH
468
- paragraph: ParagraphData
492
+ paragraph: CreateParagraphData
469
493
 
470
494
 
471
495
  # ============================================================================
@@ -588,20 +612,28 @@ class CreateTableOfContentsBlock(BaseModel):
588
612
  # ============================================================================
589
613
 
590
614
 
591
- class ToDoData(BaseModel):
615
+ class BaseToDoData(BaseModel):
592
616
  rich_text: list[RichText]
593
617
  checked: bool = False
594
618
  color: BlockColor = BlockColor.DEFAULT
595
619
 
596
620
 
621
+ class ToDoData(BaseToDoData):
622
+ children: list[Block] | None = None
623
+
624
+
597
625
  class ToDoBlock(BaseBlock):
598
626
  type: Literal[BlockType.TO_DO] = BlockType.TO_DO
599
627
  to_do: ToDoData
600
628
 
601
629
 
630
+ class CreateToDoData(BaseToDoData):
631
+ children: list[BlockCreatePayload] | None = None
632
+
633
+
602
634
  class CreateToDoBlock(BaseModel):
603
635
  type: Literal[BlockType.TO_DO] = BlockType.TO_DO
604
- to_do: ToDoData
636
+ to_do: CreateToDoData
605
637
 
606
638
 
607
639
  # ============================================================================
@@ -0,0 +1,5 @@
1
+ from .models import Comment
2
+
3
+ __all__ = [
4
+ "Comment",
5
+ ]
@@ -18,20 +18,17 @@ class CommentClient(NotionHttpClient):
18
18
  async def iter_comments(
19
19
  self,
20
20
  block_id: str,
21
- *,
22
- page_size: int = 100,
21
+ total_results_limit: int | None = None,
23
22
  ) -> AsyncGenerator[CommentDto]:
24
- """
25
- Iterates through all comments for a block, yielding each comment individually.
26
- Uses pagination to handle large result sets efficiently without loading everything into memory.
27
- """
28
23
  async for comment in paginate_notion_api_generator(
29
- self._list_comments_page, block_id=block_id, page_size=page_size
24
+ self._list_comments_page, block_id=block_id, total_results_limit=total_results_limit
30
25
  ):
31
26
  yield comment
32
27
 
33
- async def get_all_comments(self, block_id: str, *, page_size: int = 100) -> list[CommentDto]:
34
- all_comments = await paginate_notion_api(self._list_comments_page, block_id=block_id, page_size=page_size)
28
+ async def get_all_comments(self, block_id: str, *, total_results_limit: int | None = None) -> list[CommentDto]:
29
+ all_comments = await paginate_notion_api(
30
+ self._list_comments_page, block_id=block_id, total_results_limit=total_results_limit
31
+ )
35
32
 
36
33
  self.logger.debug("Retrieved %d total comments for block %s", len(all_comments), block_id)
37
34
  return all_comments
@@ -48,7 +45,7 @@ class CommentClient(NotionHttpClient):
48
45
  start_cursor=start_cursor,
49
46
  page_size=page_size,
50
47
  )
51
- resp = await self.get("comments", params=request.model_dump())
48
+ resp = await self.get("comments", params=request.model_dump(exclude_none=True))
52
49
  return CommentListResponse.model_validate(resp)
53
50
 
54
51
  async def create_comment_for_page(
@@ -3,8 +3,8 @@ import asyncio
3
3
  from notionary.blocks.rich_text.rich_text_markdown_converter import RichTextToMarkdownConverter
4
4
  from notionary.comments.models import Comment
5
5
  from notionary.comments.schemas import CommentDto
6
+ from notionary.user.base import BaseUser
6
7
  from notionary.user.client import UserHttpClient
7
- from notionary.user.person import PersonUser
8
8
  from notionary.utils.mixins.logging import LoggingMixin
9
9
 
10
10
 
@@ -25,14 +25,12 @@ class CommentFactory(LoggingMixin):
25
25
  return Comment(author_name=author_name, content=content)
26
26
 
27
27
  async def _resolve_user_name(self, dto: CommentDto) -> str:
28
- user_id = dto.created_by.id
28
+ created_by_id = dto.created_by.id
29
29
 
30
30
  try:
31
- person = await PersonUser.from_id(user_id, self.http_client)
32
- if person and person.name:
33
- return person.name
31
+ return await BaseUser.from_id_auto(created_by_id, self.http_client)
34
32
  except Exception:
35
- self.logger.warning(f"Failed to resolve user name for user_id: {user_id}", exc_info=True)
33
+ self.logger.warning(f"Failed to resolve user name for user_id: {created_by_id}", exc_info=True)
36
34
 
37
35
  return self.UNKNOWN_AUTHOR
38
36
 
@@ -9,6 +9,7 @@ from notionary.data_source.schemas import DataSourceDto, QueryDataSourceResponse
9
9
  from notionary.http.client import NotionHttpClient
10
10
  from notionary.page.schemas import NotionPageDto
11
11
  from notionary.shared.entity.entity_metadata_update_client import EntityMetadataUpdateClient
12
+ from notionary.shared.typings import JsonDict
12
13
  from notionary.utils.pagination import paginate_notion_api, paginate_notion_api_generator
13
14
 
14
15
  if TYPE_CHECKING:
@@ -56,8 +57,12 @@ class DataSourceInstanceClient(NotionHttpClient, EntityMetadataUpdateClient):
56
57
  return updated_markdown_description
57
58
 
58
59
  async def query(self, query_params: DataSourceQueryParams | None = None) -> QueryDataSourceResponse:
59
- query_params_dict = query_params.model_dump() if query_params else {}
60
- all_results = await paginate_notion_api(self._make_query_request, query_data=query_params_dict or {})
60
+ query_params_dict = query_params.to_api_params() if query_params else {}
61
+ total_result_limit = query_params.total_results_limit if query_params else None
62
+
63
+ all_results = await paginate_notion_api(
64
+ self._make_query_request, query_data=query_params_dict or {}, total_result_limit=total_result_limit
65
+ )
61
66
 
62
67
  return QueryDataSourceResponse(
63
68
  results=all_results,
@@ -67,16 +72,21 @@ class DataSourceInstanceClient(NotionHttpClient, EntityMetadataUpdateClient):
67
72
 
68
73
  async def query_stream(self, query_params: DataSourceQueryParams | None = None) -> AsyncIterator[Any]:
69
74
  query_params_dict = query_params.model_dump() if query_params else {}
75
+ total_result_limit = query_params.total_results_limit if query_params else None
70
76
 
71
- async for result in paginate_notion_api_generator(self._make_query_request, query_data=query_params_dict or {}):
77
+ async for result in paginate_notion_api_generator(
78
+ self._make_query_request, query_data=query_params_dict or {}, total_results_limit=total_result_limit
79
+ ):
72
80
  yield result
73
81
 
74
82
  async def _make_query_request(
75
- self, query_data: dict[str, Any], start_cursor: str | None = None
83
+ self, query_data: JsonDict, start_cursor: str | None = None, page_size: int | None = None
76
84
  ) -> QueryDataSourceResponse:
77
85
  current_query_data = query_data.copy()
78
86
  if start_cursor:
79
87
  current_query_data["start_cursor"] = start_cursor
88
+ if page_size:
89
+ current_query_data["page_size"] = page_size
80
90
 
81
91
  response = await self.post(f"data_sources/{self._data_source_id}/query", data=current_query_data)
82
92
  return QueryDataSourceResponse.model_validate(response)
@@ -1,9 +1,10 @@
1
1
  from enum import StrEnum
2
- from typing import Annotated, Any, Literal, TypeVar
2
+ from typing import Annotated, Literal, TypeVar
3
3
 
4
4
  from pydantic import BaseModel, Field
5
5
 
6
6
  from notionary.shared.properties.type import PropertyType
7
+ from notionary.shared.typings import JsonDict
7
8
 
8
9
  # ============================================================================
9
10
  # Base Model
@@ -144,10 +145,9 @@ class DataSourceMultiSelectConfig(BaseModel):
144
145
 
145
146
 
146
147
  class DataSourceRelationConfig(BaseModel):
147
- database_id: str | None = None
148
- data_source_id: str | None = None
148
+ data_source_id: str
149
149
  type: RelationType = RelationType.SINGLE_PROPERTY
150
- single_property: dict[str, Any] = Field(default_factory=dict)
150
+ single_property: JsonDict = Field(default_factory=dict)
151
151
 
152
152
 
153
153
  class DataSourceNumberConfig(BaseModel):
@@ -264,10 +264,6 @@ class DataSourceRelationProperty(DataSourceProperty):
264
264
  type: Literal[PropertyType.RELATION] = PropertyType.RELATION
265
265
  relation: DataSourceRelationConfig = Field(default_factory=DataSourceRelationConfig)
266
266
 
267
- @property
268
- def related_database_id(self) -> str | None:
269
- return self.relation.database_id
270
-
271
267
  @property
272
268
  def related_data_source_id(self) -> str | None:
273
269
  return self.relation.data_source_id
@@ -0,0 +1,9 @@
1
+ from .builder import DataSourceQueryBuilder
2
+ from .resolver import QueryResolver
3
+ from .schema import DataSourceQueryParams
4
+
5
+ __all__ = [
6
+ "DataSourceQueryBuilder",
7
+ "DataSourceQueryParams",
8
+ "QueryResolver",
9
+ ]