notionary 0.3.1__py3-none-any.whl → 0.4.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 (201) hide show
  1. notionary/__init__.py +49 -1
  2. notionary/blocks/client.py +37 -11
  3. notionary/blocks/enums.py +0 -6
  4. notionary/blocks/rich_text/markdown_rich_text_converter.py +49 -15
  5. notionary/blocks/rich_text/models.py +13 -4
  6. notionary/blocks/rich_text/name_id_resolver/data_source.py +9 -3
  7. notionary/blocks/rich_text/name_id_resolver/person.py +6 -2
  8. notionary/blocks/rich_text/rich_text_markdown_converter.py +10 -3
  9. notionary/blocks/schemas.py +33 -78
  10. notionary/comments/client.py +19 -6
  11. notionary/comments/factory.py +10 -3
  12. notionary/comments/schemas.py +10 -31
  13. notionary/comments/service.py +12 -4
  14. notionary/data_source/http/data_source_instance_client.py +59 -17
  15. notionary/data_source/properties/schemas.py +156 -115
  16. notionary/data_source/query/builder.py +67 -18
  17. notionary/data_source/query/resolver.py +16 -5
  18. notionary/data_source/query/schema.py +24 -6
  19. notionary/data_source/query/validator.py +18 -6
  20. notionary/data_source/schema/registry.py +31 -12
  21. notionary/data_source/schema/service.py +66 -20
  22. notionary/data_source/schemas.py +2 -2
  23. notionary/data_source/service.py +103 -43
  24. notionary/database/client.py +27 -9
  25. notionary/database/database_metadata_update_client.py +12 -4
  26. notionary/database/schemas.py +2 -2
  27. notionary/database/service.py +14 -9
  28. notionary/exceptions/__init__.py +20 -4
  29. notionary/exceptions/api.py +2 -2
  30. notionary/exceptions/base.py +1 -1
  31. notionary/exceptions/block_parsing.py +9 -5
  32. notionary/exceptions/data_source/builder.py +13 -7
  33. notionary/exceptions/data_source/properties.py +6 -4
  34. notionary/exceptions/file_upload.py +76 -0
  35. notionary/exceptions/properties.py +7 -5
  36. notionary/exceptions/search.py +10 -6
  37. notionary/file_upload/__init__.py +4 -0
  38. notionary/file_upload/client.py +128 -210
  39. notionary/file_upload/config/__init__.py +17 -0
  40. notionary/file_upload/config/config.py +39 -0
  41. notionary/file_upload/config/constants.py +16 -0
  42. notionary/file_upload/file/reader.py +28 -0
  43. notionary/file_upload/query/__init__.py +7 -0
  44. notionary/file_upload/query/builder.py +58 -0
  45. notionary/file_upload/query/models.py +37 -0
  46. notionary/file_upload/schemas.py +80 -0
  47. notionary/file_upload/service.py +182 -291
  48. notionary/file_upload/validation/factory.py +66 -0
  49. notionary/file_upload/validation/impl/file_name_length.py +25 -0
  50. notionary/file_upload/validation/models.py +134 -0
  51. notionary/file_upload/validation/port.py +7 -0
  52. notionary/file_upload/validation/service.py +17 -0
  53. notionary/file_upload/validation/validators/__init__.py +11 -0
  54. notionary/file_upload/validation/validators/file_exists.py +15 -0
  55. notionary/file_upload/validation/validators/file_extension.py +131 -0
  56. notionary/file_upload/validation/validators/file_name_length.py +21 -0
  57. notionary/file_upload/validation/validators/upload_limit.py +31 -0
  58. notionary/http/client.py +33 -30
  59. notionary/page/content/__init__.py +9 -0
  60. notionary/page/content/factory.py +21 -7
  61. notionary/page/content/markdown/builder.py +85 -23
  62. notionary/page/content/markdown/nodes/audio.py +8 -4
  63. notionary/page/content/markdown/nodes/base.py +3 -3
  64. notionary/page/content/markdown/nodes/bookmark.py +5 -3
  65. notionary/page/content/markdown/nodes/breadcrumb.py +2 -2
  66. notionary/page/content/markdown/nodes/bulleted_list.py +5 -3
  67. notionary/page/content/markdown/nodes/callout.py +2 -2
  68. notionary/page/content/markdown/nodes/code.py +5 -3
  69. notionary/page/content/markdown/nodes/columns.py +3 -3
  70. notionary/page/content/markdown/nodes/container.py +9 -5
  71. notionary/page/content/markdown/nodes/divider.py +2 -2
  72. notionary/page/content/markdown/nodes/embed.py +8 -4
  73. notionary/page/content/markdown/nodes/equation.py +4 -2
  74. notionary/page/content/markdown/nodes/file.py +8 -4
  75. notionary/page/content/markdown/nodes/heading.py +2 -2
  76. notionary/page/content/markdown/nodes/image.py +8 -4
  77. notionary/page/content/markdown/nodes/mixins/caption.py +5 -3
  78. notionary/page/content/markdown/nodes/numbered_list.py +5 -3
  79. notionary/page/content/markdown/nodes/paragraph.py +4 -2
  80. notionary/page/content/markdown/nodes/pdf.py +8 -4
  81. notionary/page/content/markdown/nodes/quote.py +2 -2
  82. notionary/page/content/markdown/nodes/space.py +2 -2
  83. notionary/page/content/markdown/nodes/table.py +8 -5
  84. notionary/page/content/markdown/nodes/table_of_contents.py +2 -2
  85. notionary/page/content/markdown/nodes/todo.py +15 -7
  86. notionary/page/content/markdown/nodes/toggle.py +2 -2
  87. notionary/page/content/markdown/nodes/video.py +8 -4
  88. notionary/page/content/markdown/structured_output/__init__.py +73 -0
  89. notionary/page/content/markdown/structured_output/models.py +391 -0
  90. notionary/page/content/markdown/structured_output/service.py +211 -0
  91. notionary/page/content/parser/context.py +1 -1
  92. notionary/page/content/parser/factory.py +26 -8
  93. notionary/page/content/parser/parsers/audio.py +12 -32
  94. notionary/page/content/parser/parsers/base.py +2 -2
  95. notionary/page/content/parser/parsers/bookmark.py +2 -2
  96. notionary/page/content/parser/parsers/breadcrumb.py +2 -2
  97. notionary/page/content/parser/parsers/bulleted_list.py +19 -6
  98. notionary/page/content/parser/parsers/callout.py +15 -5
  99. notionary/page/content/parser/parsers/caption.py +9 -3
  100. notionary/page/content/parser/parsers/code.py +21 -7
  101. notionary/page/content/parser/parsers/column.py +8 -4
  102. notionary/page/content/parser/parsers/column_list.py +19 -7
  103. notionary/page/content/parser/parsers/divider.py +2 -2
  104. notionary/page/content/parser/parsers/embed.py +2 -4
  105. notionary/page/content/parser/parsers/equation.py +8 -4
  106. notionary/page/content/parser/parsers/file.py +12 -34
  107. notionary/page/content/parser/parsers/file_like_block.py +109 -0
  108. notionary/page/content/parser/parsers/heading.py +31 -10
  109. notionary/page/content/parser/parsers/image.py +12 -34
  110. notionary/page/content/parser/parsers/numbered_list.py +18 -6
  111. notionary/page/content/parser/parsers/paragraph.py +3 -1
  112. notionary/page/content/parser/parsers/pdf.py +12 -34
  113. notionary/page/content/parser/parsers/quote.py +28 -9
  114. notionary/page/content/parser/parsers/space.py +2 -2
  115. notionary/page/content/parser/parsers/table.py +31 -10
  116. notionary/page/content/parser/parsers/table_of_contents.py +7 -3
  117. notionary/page/content/parser/parsers/todo.py +15 -5
  118. notionary/page/content/parser/parsers/toggle.py +15 -5
  119. notionary/page/content/parser/parsers/video.py +12 -34
  120. notionary/page/content/parser/post_processing/handlers/rich_text_length.py +8 -2
  121. notionary/page/content/parser/post_processing/handlers/rich_text_length_truncation.py +8 -2
  122. notionary/page/content/parser/post_processing/service.py +3 -1
  123. notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +21 -7
  124. notionary/page/content/parser/pre_processsing/handlers/indentation.py +11 -4
  125. notionary/page/content/parser/pre_processsing/handlers/video_syntax.py +13 -6
  126. notionary/page/content/parser/service.py +4 -1
  127. notionary/page/content/renderer/context.py +15 -5
  128. notionary/page/content/renderer/factory.py +12 -6
  129. notionary/page/content/renderer/post_processing/handlers/numbered_list.py +19 -9
  130. notionary/page/content/renderer/renderers/audio.py +20 -23
  131. notionary/page/content/renderer/renderers/base.py +3 -3
  132. notionary/page/content/renderer/renderers/bookmark.py +3 -1
  133. notionary/page/content/renderer/renderers/bulleted_list.py +11 -5
  134. notionary/page/content/renderer/renderers/callout.py +19 -7
  135. notionary/page/content/renderer/renderers/captioned_block.py +11 -5
  136. notionary/page/content/renderer/renderers/code.py +6 -2
  137. notionary/page/content/renderer/renderers/column.py +3 -1
  138. notionary/page/content/renderer/renderers/column_list.py +3 -1
  139. notionary/page/content/renderer/renderers/embed.py +3 -1
  140. notionary/page/content/renderer/renderers/equation.py +3 -1
  141. notionary/page/content/renderer/renderers/file.py +20 -23
  142. notionary/page/content/renderer/renderers/file_like_block.py +47 -0
  143. notionary/page/content/renderer/renderers/heading.py +22 -8
  144. notionary/page/content/renderer/renderers/image.py +20 -23
  145. notionary/page/content/renderer/renderers/numbered_list.py +8 -3
  146. notionary/page/content/renderer/renderers/paragraph.py +12 -4
  147. notionary/page/content/renderer/renderers/pdf.py +20 -23
  148. notionary/page/content/renderer/renderers/quote.py +14 -6
  149. notionary/page/content/renderer/renderers/table.py +15 -5
  150. notionary/page/content/renderer/renderers/todo.py +16 -6
  151. notionary/page/content/renderer/renderers/toggle.py +8 -4
  152. notionary/page/content/renderer/renderers/video.py +20 -23
  153. notionary/page/content/renderer/service.py +9 -3
  154. notionary/page/content/service.py +21 -7
  155. notionary/page/content/syntax/definition/__init__.py +11 -0
  156. notionary/page/content/syntax/definition/models.py +57 -0
  157. notionary/page/content/syntax/definition/registry.py +371 -0
  158. notionary/page/content/syntax/prompts/__init__.py +4 -0
  159. notionary/page/content/syntax/prompts/models.py +11 -0
  160. notionary/page/content/syntax/prompts/registry.py +703 -0
  161. notionary/page/page_metadata_update_client.py +12 -4
  162. notionary/page/properties/client.py +46 -16
  163. notionary/page/properties/factory.py +6 -2
  164. notionary/page/properties/{models.py → schemas.py} +93 -107
  165. notionary/page/properties/service.py +111 -37
  166. notionary/page/schemas.py +3 -3
  167. notionary/page/service.py +21 -7
  168. notionary/shared/entity/client.py +6 -2
  169. notionary/shared/entity/dto_parsers.py +4 -37
  170. notionary/shared/entity/entity_metadata_update_client.py +25 -5
  171. notionary/shared/entity/schemas.py +6 -6
  172. notionary/shared/entity/service.py +89 -35
  173. notionary/shared/models/file.py +36 -6
  174. notionary/shared/models/icon.py +5 -12
  175. notionary/user/base.py +6 -2
  176. notionary/user/bot.py +22 -14
  177. notionary/user/client.py +3 -1
  178. notionary/user/person.py +3 -1
  179. notionary/user/schemas.py +3 -1
  180. notionary/user/service.py +6 -2
  181. notionary/utils/decorators.py +13 -9
  182. notionary/utils/fuzzy.py +6 -2
  183. notionary/utils/mixins/logging.py +3 -1
  184. notionary/utils/pagination.py +14 -4
  185. notionary/workspace/__init__.py +6 -2
  186. notionary/workspace/query/__init__.py +2 -1
  187. notionary/workspace/query/service.py +42 -13
  188. notionary/workspace/service.py +74 -46
  189. {notionary-0.3.1.dist-info → notionary-0.4.1.dist-info}/METADATA +1 -1
  190. notionary-0.4.1.dist-info/RECORD +236 -0
  191. notionary/file_upload/models.py +0 -69
  192. notionary/page/blocks/client.py +0 -1
  193. notionary/page/content/syntax/__init__.py +0 -4
  194. notionary/page/content/syntax/models.py +0 -66
  195. notionary/page/content/syntax/registry.py +0 -393
  196. notionary/page/page_context.py +0 -50
  197. notionary/shared/models/cover.py +0 -20
  198. notionary-0.3.1.dist-info/RECORD +0 -211
  199. /notionary/page/content/syntax/{grammar.py → definition/grammar.py} +0 -0
  200. {notionary-0.3.1.dist-info → notionary-0.4.1.dist-info}/WHEEL +0 -0
  201. {notionary-0.3.1.dist-info → notionary-0.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,16 +1,20 @@
1
1
  from typing import override
2
2
 
3
3
  from notionary.blocks.enums import BlockType
4
- from notionary.blocks.schemas import BlockCreatePayload, CreateColumnListBlock, CreateColumnListData
4
+ from notionary.blocks.schemas import (
5
+ BlockCreatePayload,
6
+ CreateColumnListBlock,
7
+ CreateColumnListData,
8
+ )
5
9
  from notionary.page.content.parser.parsers.base import (
6
10
  BlockParsingContext,
7
11
  LineParser,
8
12
  )
9
- from notionary.page.content.syntax import SyntaxRegistry
13
+ from notionary.page.content.syntax.definition import SyntaxDefinitionRegistry
10
14
 
11
15
 
12
16
  class ColumnListParser(LineParser):
13
- def __init__(self, syntax_registry: SyntaxRegistry) -> None:
17
+ def __init__(self, syntax_registry: SyntaxDefinitionRegistry) -> None:
14
18
  super().__init__(syntax_registry)
15
19
  self._syntax = syntax_registry.get_column_list_syntax()
16
20
 
@@ -35,9 +39,13 @@ class ColumnListParser(LineParser):
35
39
  column_list_data = CreateColumnListData(children=[])
36
40
  return CreateColumnListBlock(column_list=column_list_data)
37
41
 
38
- async def _populate_columns(self, block: CreateColumnListBlock, context: BlockParsingContext) -> None:
42
+ async def _populate_columns(
43
+ self, block: CreateColumnListBlock, context: BlockParsingContext
44
+ ) -> None:
39
45
  parent_indent_level = context.get_line_indentation_level()
40
- child_lines = self._collect_children_allowing_empty_lines(context, parent_indent_level)
46
+ child_lines = self._collect_children_allowing_empty_lines(
47
+ context, parent_indent_level
48
+ )
41
49
 
42
50
  if not child_lines:
43
51
  return
@@ -46,7 +54,9 @@ class ColumnListParser(LineParser):
46
54
  block.column_list.children = column_blocks
47
55
  context.lines_consumed = len(child_lines)
48
56
 
49
- async def _parse_column_children(self, child_lines: list[str], context: BlockParsingContext) -> list:
57
+ async def _parse_column_children(
58
+ self, child_lines: list[str], context: BlockParsingContext
59
+ ) -> list:
50
60
  stripped_lines = context.strip_indentation_level(child_lines, levels=1)
51
61
  child_markdown = "\n".join(stripped_lines)
52
62
  parsed_blocks = await context.parse_nested_markdown(child_markdown)
@@ -67,7 +77,9 @@ class ColumnListParser(LineParser):
67
77
 
68
78
  return child_lines
69
79
 
70
- def _should_include_as_child(self, line: str, expected_indent: int, context: BlockParsingContext) -> bool:
80
+ def _should_include_as_child(
81
+ self, line: str, expected_indent: int, context: BlockParsingContext
82
+ ) -> bool:
71
83
  if not line.strip():
72
84
  return True
73
85
 
@@ -5,11 +5,11 @@ from notionary.page.content.parser.parsers.base import (
5
5
  BlockParsingContext,
6
6
  LineParser,
7
7
  )
8
- from notionary.page.content.syntax import SyntaxRegistry
8
+ from notionary.page.content.syntax.definition import SyntaxDefinitionRegistry
9
9
 
10
10
 
11
11
  class DividerParser(LineParser):
12
- def __init__(self, syntax_registry: SyntaxRegistry) -> None:
12
+ def __init__(self, syntax_registry: SyntaxDefinitionRegistry) -> None:
13
13
  super().__init__(syntax_registry)
14
14
  self._syntax = syntax_registry.get_divider_syntax()
15
15
 
@@ -1,14 +1,12 @@
1
- """Parser for embed blocks."""
2
-
3
1
  from typing import override
4
2
 
5
3
  from notionary.blocks.schemas import CreateEmbedBlock, EmbedData
6
4
  from notionary.page.content.parser.parsers.base import BlockParsingContext, LineParser
7
- from notionary.page.content.syntax import SyntaxRegistry
5
+ from notionary.page.content.syntax.definition import SyntaxDefinitionRegistry
8
6
 
9
7
 
10
8
  class EmbedParser(LineParser):
11
- def __init__(self, syntax_registry: SyntaxRegistry) -> None:
9
+ def __init__(self, syntax_registry: SyntaxDefinitionRegistry) -> None:
12
10
  super().__init__(syntax_registry)
13
11
  self._syntax = syntax_registry.get_embed_syntax()
14
12
 
@@ -5,11 +5,11 @@ from notionary.page.content.parser.parsers.base import (
5
5
  BlockParsingContext,
6
6
  LineParser,
7
7
  )
8
- from notionary.page.content.syntax import SyntaxRegistry
8
+ from notionary.page.content.syntax.definition import SyntaxDefinitionRegistry
9
9
 
10
10
 
11
11
  class EquationParser(LineParser):
12
- def __init__(self, syntax_registry: SyntaxRegistry) -> None:
12
+ def __init__(self, syntax_registry: SyntaxDefinitionRegistry) -> None:
13
13
  super().__init__(syntax_registry)
14
14
  self._syntax = syntax_registry.get_equation_syntax()
15
15
 
@@ -24,7 +24,9 @@ class EquationParser(LineParser):
24
24
  equation_content = self._collect_equation_content(context)
25
25
  lines_consumed = self._count_lines_consumed(context)
26
26
 
27
- block = self._create_equation_block(opening_line=context.line, equation_lines=equation_content)
27
+ block = self._create_equation_block(
28
+ opening_line=context.line, equation_lines=equation_content
29
+ )
28
30
 
29
31
  if block:
30
32
  context.lines_consumed = lines_consumed
@@ -50,7 +52,9 @@ class EquationParser(LineParser):
50
52
 
51
53
  return len(context.get_remaining_lines())
52
54
 
53
- def _create_equation_block(self, opening_line: str, equation_lines: list[str]) -> CreateEquationBlock | None:
55
+ def _create_equation_block(
56
+ self, opening_line: str, equation_lines: list[str]
57
+ ) -> CreateEquationBlock | None:
54
58
  if opening_line.strip() != self._syntax.start_delimiter:
55
59
  return None
56
60
 
@@ -1,42 +1,20 @@
1
- """Parser for file blocks."""
2
-
3
1
  from typing import override
4
2
 
5
- from notionary.blocks.schemas import (
6
- CreateFileBlock,
7
- ExternalFile,
8
- FileData,
9
- FileType,
3
+ from notionary.blocks.schemas import CreateFileBlock, ExternalFileWithCaption
4
+ from notionary.page.content.parser.parsers.file_like_block import FileLikeBlockParser
5
+ from notionary.page.content.syntax.definition import (
6
+ SyntaxDefinition,
7
+ SyntaxDefinitionRegistry,
10
8
  )
11
- from notionary.page.content.parser.parsers.base import BlockParsingContext, LineParser
12
- from notionary.page.content.syntax import SyntaxRegistry
13
-
14
9
 
15
- class FileParser(LineParser):
16
- def __init__(self, syntax_registry: SyntaxRegistry) -> None:
17
- super().__init__(syntax_registry)
18
- self._syntax = syntax_registry.get_file_syntax()
19
10
 
11
+ class FileParser(FileLikeBlockParser[CreateFileBlock]):
20
12
  @override
21
- def _can_handle(self, context: BlockParsingContext) -> bool:
22
- if context.is_inside_parent_context():
23
- return False
24
- return self._syntax.regex_pattern.search(context.line) is not None
13
+ def _get_syntax(
14
+ self, syntax_registry: SyntaxDefinitionRegistry
15
+ ) -> SyntaxDefinition:
16
+ return syntax_registry.get_file_syntax()
25
17
 
26
18
  @override
27
- async def _process(self, context: BlockParsingContext) -> None:
28
- url = self._extract_url(context.line)
29
- if not url:
30
- return
31
-
32
- file_data = FileData(
33
- type=FileType.EXTERNAL,
34
- external=ExternalFile(url=url),
35
- caption=[],
36
- )
37
- block = CreateFileBlock(file=file_data)
38
- context.result_blocks.append(block)
39
-
40
- def _extract_url(self, line: str) -> str | None:
41
- match = self._syntax.regex_pattern.search(line)
42
- return match.group(1).strip() if match else None
19
+ def _create_block(self, file_data: ExternalFileWithCaption) -> CreateFileBlock:
20
+ return CreateFileBlock(file=file_data)
@@ -0,0 +1,109 @@
1
+ from abc import abstractmethod
2
+ from pathlib import Path
3
+ from typing import Generic, TypeVar, override
4
+
5
+ from notionary.blocks.schemas import (
6
+ ExternalFileWithCaption,
7
+ FileUploadFileWithCaption,
8
+ FileWithCaption,
9
+ )
10
+ from notionary.exceptions.file_upload import UploadFailedError, UploadTimeoutError
11
+ from notionary.file_upload.service import NotionFileUpload
12
+ from notionary.page.content.parser.parsers.base import BlockParsingContext, LineParser
13
+ from notionary.page.content.syntax.definition import SyntaxDefinitionRegistry
14
+ from notionary.page.content.syntax.definition.models import SyntaxDefinition
15
+ from notionary.shared.models.file import ExternalFileData, FileUploadedFileData
16
+ from notionary.utils.mixins.logging import LoggingMixin
17
+
18
+ _TBlock = TypeVar("_TBlock")
19
+
20
+
21
+ class FileLikeBlockParser(LineParser, LoggingMixin, Generic[_TBlock]):
22
+ def __init__(
23
+ self,
24
+ syntax_registry: SyntaxDefinitionRegistry,
25
+ file_upload_service: NotionFileUpload | None = None,
26
+ ) -> None:
27
+ super().__init__(syntax_registry)
28
+ self._syntax = self._get_syntax(syntax_registry)
29
+ self._file_upload_service = file_upload_service or NotionFileUpload()
30
+
31
+ @abstractmethod
32
+ def _get_syntax(
33
+ self, syntax_registry: SyntaxDefinitionRegistry
34
+ ) -> SyntaxDefinition:
35
+ pass
36
+
37
+ @abstractmethod
38
+ def _create_block(self, file_data: FileWithCaption) -> _TBlock:
39
+ pass
40
+
41
+ @override
42
+ def _can_handle(self, context: BlockParsingContext) -> bool:
43
+ if context.is_inside_parent_context():
44
+ return False
45
+ return self._syntax.regex_pattern.search(context.line) is not None
46
+
47
+ @override
48
+ async def _process(self, context: BlockParsingContext) -> None:
49
+ path_or_url = self._extract_path_or_url(context.line)
50
+ if not path_or_url:
51
+ return
52
+
53
+ try:
54
+ if self._is_external_url(path_or_url):
55
+ file_data = ExternalFileWithCaption(
56
+ external=ExternalFileData(url=path_or_url)
57
+ )
58
+ else:
59
+ file_data = await self._upload_local_file(path_or_url)
60
+
61
+ block = self._create_block(file_data)
62
+ context.result_blocks.append(block)
63
+
64
+ except FileNotFoundError:
65
+ self.logger.warning("File not found: '%s' - skipping block", path_or_url)
66
+ except PermissionError:
67
+ self.logger.warning(
68
+ "No permission to read file: '%s' - skipping block", path_or_url
69
+ )
70
+ except IsADirectoryError:
71
+ self.logger.warning(
72
+ "Path is a directory, not a file: '%s' - skipping block", path_or_url
73
+ )
74
+ except (UploadFailedError, UploadTimeoutError) as e:
75
+ self.logger.warning(
76
+ "Upload failed for '%s': %s - skipping block", path_or_url, e
77
+ )
78
+ except OSError as e:
79
+ self.logger.warning(
80
+ "IO error reading file '%s': %s - skipping block", path_or_url, e
81
+ )
82
+ except Exception as e:
83
+ self.logger.warning(
84
+ "Unexpected error processing file '%s': %s - skipping block",
85
+ path_or_url,
86
+ e,
87
+ )
88
+
89
+ def _extract_path_or_url(self, line: str) -> str | None:
90
+ match = self._syntax.regex_pattern.search(line)
91
+ return match.group(1).strip() if match else None
92
+
93
+ def _is_external_url(self, path_or_url: str) -> bool:
94
+ if path_or_url.startswith("http://") or path_or_url.startswith("https://"):
95
+ return True
96
+
97
+ if path_or_url.startswith("data:"):
98
+ return True
99
+
100
+ return path_or_url.startswith("/")
101
+
102
+ async def _upload_local_file(self, file_path: str) -> FileUploadFileWithCaption:
103
+ path = Path(file_path)
104
+ self.logger.debug("Uploading local file: '%s'", path)
105
+ upload_response = await self._file_upload_service.upload_file(path)
106
+
107
+ return FileUploadFileWithCaption(
108
+ file_upload=FileUploadedFileData(id=upload_response.id),
109
+ )
@@ -1,6 +1,8 @@
1
1
  from typing import override
2
2
 
3
- from notionary.blocks.rich_text.markdown_rich_text_converter import MarkdownRichTextConverter
3
+ from notionary.blocks.rich_text.markdown_rich_text_converter import (
4
+ MarkdownRichTextConverter,
5
+ )
4
6
  from notionary.blocks.schemas import (
5
7
  BlockColor,
6
8
  BlockCreatePayload,
@@ -15,14 +17,18 @@ from notionary.page.content.parser.parsers.base import (
15
17
  BlockParsingContext,
16
18
  LineParser,
17
19
  )
18
- from notionary.page.content.syntax import SyntaxRegistry
20
+ from notionary.page.content.syntax.definition import SyntaxDefinitionRegistry
19
21
 
20
22
 
21
23
  class HeadingParser(LineParser):
22
24
  MIN_HEADING_LEVEL = 1
23
25
  MAX_HEADING_LEVEL = 3
24
26
 
25
- def __init__(self, syntax_registry: SyntaxRegistry, rich_text_converter: MarkdownRichTextConverter) -> None:
27
+ def __init__(
28
+ self,
29
+ syntax_registry: SyntaxDefinitionRegistry,
30
+ rich_text_converter: MarkdownRichTextConverter,
31
+ ) -> None:
26
32
  super().__init__(syntax_registry)
27
33
  self._syntax = syntax_registry.get_heading_syntax()
28
34
  self._rich_text_converter = rich_text_converter
@@ -42,7 +48,9 @@ class HeadingParser(LineParser):
42
48
  await self._process_nested_children(block, context)
43
49
  context.result_blocks.append(block)
44
50
 
45
- async def _process_nested_children(self, block: CreateHeadingBlock, context: BlockParsingContext) -> None:
51
+ async def _process_nested_children(
52
+ self, block: CreateHeadingBlock, context: BlockParsingContext
53
+ ) -> None:
46
54
  parent_indent_level = context.get_line_indentation_level()
47
55
  child_lines = context.collect_indented_child_lines(parent_indent_level)
48
56
 
@@ -64,7 +72,9 @@ class HeadingParser(LineParser):
64
72
 
65
73
  context.lines_consumed = len(child_lines)
66
74
 
67
- def _set_heading_toggleable(self, block: CreateHeadingBlock, is_toggleable: bool) -> None:
75
+ def _set_heading_toggleable(
76
+ self, block: CreateHeadingBlock, is_toggleable: bool
77
+ ) -> None:
68
78
  if block.type == BlockType.HEADING_1:
69
79
  block.heading_1.is_toggleable = is_toggleable
70
80
  elif block.type == BlockType.HEADING_2:
@@ -72,7 +82,9 @@ class HeadingParser(LineParser):
72
82
  elif block.type == BlockType.HEADING_3:
73
83
  block.heading_3.is_toggleable = is_toggleable
74
84
 
75
- def _set_heading_children(self, block: CreateHeadingBlock, children: list[BlockCreatePayload]) -> None:
85
+ def _set_heading_children(
86
+ self, block: CreateHeadingBlock, children: list[BlockCreatePayload]
87
+ ) -> None:
76
88
  if block.type == BlockType.HEADING_1:
77
89
  block.heading_1.children = children
78
90
  elif block.type == BlockType.HEADING_2:
@@ -100,13 +112,22 @@ class HeadingParser(LineParser):
100
112
  return self._create_heading_block_by_level(level, heading_data)
101
113
 
102
114
  def _is_valid_heading(self, level: int, content: str) -> bool:
103
- return self.MIN_HEADING_LEVEL <= level <= self.MAX_HEADING_LEVEL and bool(content)
115
+ return self.MIN_HEADING_LEVEL <= level <= self.MAX_HEADING_LEVEL and bool(
116
+ content
117
+ )
104
118
 
105
119
  async def _build_heading_data(self, content: str) -> CreateHeadingData:
106
120
  rich_text = await self._rich_text_converter.to_rich_text(content)
107
- return CreateHeadingData(rich_text=rich_text, color=BlockColor.DEFAULT, is_toggleable=False, children=[])
108
-
109
- def _create_heading_block_by_level(self, level: int, heading_data: CreateHeadingData) -> CreateHeadingBlock:
121
+ return CreateHeadingData(
122
+ rich_text=rich_text,
123
+ color=BlockColor.DEFAULT,
124
+ is_toggleable=False,
125
+ children=[],
126
+ )
127
+
128
+ def _create_heading_block_by_level(
129
+ self, level: int, heading_data: CreateHeadingData
130
+ ) -> CreateHeadingBlock:
110
131
  if level == 1:
111
132
  return CreateHeading1Block(heading_1=heading_data)
112
133
  elif level == 2:
@@ -1,42 +1,20 @@
1
- """Parser for image blocks."""
2
-
3
1
  from typing import override
4
2
 
5
- from notionary.blocks.schemas import (
6
- CreateImageBlock,
7
- ExternalFile,
8
- FileData,
9
- FileType,
3
+ from notionary.blocks.schemas import CreateImageBlock, ExternalFileWithCaption
4
+ from notionary.page.content.parser.parsers.file_like_block import FileLikeBlockParser
5
+ from notionary.page.content.syntax.definition import (
6
+ SyntaxDefinition,
7
+ SyntaxDefinitionRegistry,
10
8
  )
11
- from notionary.page.content.parser.parsers.base import BlockParsingContext, LineParser
12
- from notionary.page.content.syntax import SyntaxRegistry
13
-
14
9
 
15
- class ImageParser(LineParser):
16
- def __init__(self, syntax_registry: SyntaxRegistry) -> None:
17
- super().__init__(syntax_registry)
18
- self._syntax = syntax_registry.get_image_syntax()
19
10
 
11
+ class ImageParser(FileLikeBlockParser[CreateImageBlock]):
20
12
  @override
21
- def _can_handle(self, context: BlockParsingContext) -> bool:
22
- if context.is_inside_parent_context():
23
- return False
24
- return self._syntax.regex_pattern.search(context.line) is not None
13
+ def _get_syntax(
14
+ self, syntax_registry: SyntaxDefinitionRegistry
15
+ ) -> SyntaxDefinition:
16
+ return syntax_registry.get_image_syntax()
25
17
 
26
18
  @override
27
- async def _process(self, context: BlockParsingContext) -> None:
28
- url = self._extract_url(context.line)
29
- if not url:
30
- return
31
-
32
- image_data = FileData(
33
- type=FileType.EXTERNAL,
34
- external=ExternalFile(url=url),
35
- caption=[],
36
- )
37
- block = CreateImageBlock(image=image_data)
38
- context.result_blocks.append(block)
39
-
40
- def _extract_url(self, line: str) -> str | None:
41
- match = self._syntax.regex_pattern.search(line)
42
- return match.group(1).strip() if match else None
19
+ def _create_block(self, file_data: ExternalFileWithCaption) -> CreateImageBlock:
20
+ return CreateImageBlock(image=file_data)
@@ -12,11 +12,15 @@ from notionary.page.content.parser.parsers.base import (
12
12
  BlockParsingContext,
13
13
  LineParser,
14
14
  )
15
- from notionary.page.content.syntax import SyntaxRegistry
15
+ from notionary.page.content.syntax.definition import SyntaxDefinitionRegistry
16
16
 
17
17
 
18
18
  class NumberedListParser(LineParser):
19
- def __init__(self, syntax_registry: SyntaxRegistry, rich_text_converter: MarkdownRichTextConverter) -> None:
19
+ def __init__(
20
+ self,
21
+ syntax_registry: SyntaxDefinitionRegistry,
22
+ rich_text_converter: MarkdownRichTextConverter,
23
+ ) -> None:
20
24
  super().__init__(syntax_registry)
21
25
  self._syntax = syntax_registry.get_numbered_list_syntax()
22
26
  self._rich_text_converter = rich_text_converter
@@ -39,7 +43,9 @@ class NumberedListParser(LineParser):
39
43
  await self._process_nested_children(block, context)
40
44
  context.result_blocks.append(block)
41
45
 
42
- async def _process_nested_children(self, block: CreateNumberedListItemBlock, context: BlockParsingContext) -> None:
46
+ async def _process_nested_children(
47
+ self, block: CreateNumberedListItemBlock, context: BlockParsingContext
48
+ ) -> None:
43
49
  child_lines = self._collect_child_lines(context)
44
50
  if not child_lines:
45
51
  return
@@ -61,13 +67,17 @@ class NumberedListParser(LineParser):
61
67
  children_text = self._convert_lines_to_text(stripped_lines)
62
68
  return await context.parse_nested_markdown(children_text)
63
69
 
64
- def _remove_parent_indentation(self, lines: list[str], context: BlockParsingContext) -> list[str]:
70
+ def _remove_parent_indentation(
71
+ self, lines: list[str], context: BlockParsingContext
72
+ ) -> list[str]:
65
73
  return context.strip_indentation_level(lines, levels=1)
66
74
 
67
75
  def _convert_lines_to_text(self, lines: list[str]) -> str:
68
76
  return "\n".join(lines)
69
77
 
70
- async def _create_numbered_list_block(self, text: str) -> CreateNumberedListItemBlock | None:
78
+ async def _create_numbered_list_block(
79
+ self, text: str
80
+ ) -> CreateNumberedListItemBlock | None:
71
81
  content = self._extract_list_content(text)
72
82
  if content is None:
73
83
  return None
@@ -85,5 +95,7 @@ class NumberedListParser(LineParser):
85
95
  return await self._rich_text_converter.to_rich_text(content)
86
96
 
87
97
  def _build_block(self, rich_text) -> CreateNumberedListItemBlock:
88
- numbered_list_content = CreateNumberedListItemData(rich_text=rich_text, color=BlockColor.DEFAULT)
98
+ numbered_list_content = CreateNumberedListItemData(
99
+ rich_text=rich_text, color=BlockColor.DEFAULT
100
+ )
89
101
  return CreateNumberedListItemBlock(numbered_list_item=numbered_list_content)
@@ -33,5 +33,7 @@ class ParagraphParser(LineParser):
33
33
  return None
34
34
 
35
35
  rich_text = await self._rich_text_converter.to_rich_text(text)
36
- paragraph_content = CreateParagraphData(rich_text=rich_text, color=BlockColor.DEFAULT)
36
+ paragraph_content = CreateParagraphData(
37
+ rich_text=rich_text, color=BlockColor.DEFAULT
38
+ )
37
39
  return CreateParagraphBlock(paragraph=paragraph_content)
@@ -1,42 +1,20 @@
1
- """Parser for PDF blocks."""
2
-
3
1
  from typing import override
4
2
 
5
- from notionary.blocks.schemas import (
6
- CreatePdfBlock,
7
- ExternalFile,
8
- FileData,
9
- FileType,
3
+ from notionary.blocks.schemas import CreatePdfBlock, ExternalFileWithCaption
4
+ from notionary.page.content.parser.parsers.file_like_block import FileLikeBlockParser
5
+ from notionary.page.content.syntax.definition import (
6
+ SyntaxDefinition,
7
+ SyntaxDefinitionRegistry,
10
8
  )
11
- from notionary.page.content.parser.parsers.base import BlockParsingContext, LineParser
12
- from notionary.page.content.syntax import SyntaxRegistry
13
-
14
9
 
15
- class PdfParser(LineParser):
16
- def __init__(self, syntax_registry: SyntaxRegistry) -> None:
17
- super().__init__(syntax_registry)
18
- self._syntax = syntax_registry.get_pdf_syntax()
19
10
 
11
+ class PdfParser(FileLikeBlockParser[CreatePdfBlock]):
20
12
  @override
21
- def _can_handle(self, context: BlockParsingContext) -> bool:
22
- if context.is_inside_parent_context():
23
- return False
24
- return self._syntax.regex_pattern.search(context.line) is not None
13
+ def _get_syntax(
14
+ self, syntax_registry: SyntaxDefinitionRegistry
15
+ ) -> SyntaxDefinition:
16
+ return syntax_registry.get_pdf_syntax()
25
17
 
26
18
  @override
27
- async def _process(self, context: BlockParsingContext) -> None:
28
- url = self._extract_url(context.line)
29
- if not url:
30
- return
31
-
32
- pdf_data = FileData(
33
- type=FileType.EXTERNAL,
34
- external=ExternalFile(url=url),
35
- caption=[],
36
- )
37
- block = CreatePdfBlock(pdf=pdf_data)
38
- context.result_blocks.append(block)
39
-
40
- def _extract_url(self, line: str) -> str | None:
41
- match = self._syntax.regex_pattern.search(line)
42
- return match.group(1).strip() if match else None
19
+ def _create_block(self, file_data: ExternalFileWithCaption) -> CreatePdfBlock:
20
+ return CreatePdfBlock(pdf=file_data)
@@ -1,16 +1,22 @@
1
1
  from typing import override
2
2
 
3
- from notionary.blocks.rich_text.markdown_rich_text_converter import MarkdownRichTextConverter
3
+ from notionary.blocks.rich_text.markdown_rich_text_converter import (
4
+ MarkdownRichTextConverter,
5
+ )
4
6
  from notionary.blocks.schemas import BlockColor, CreateQuoteBlock, CreateQuoteData
5
7
  from notionary.page.content.parser.parsers.base import (
6
8
  BlockParsingContext,
7
9
  LineParser,
8
10
  )
9
- from notionary.page.content.syntax import SyntaxRegistry
11
+ from notionary.page.content.syntax.definition import SyntaxDefinitionRegistry
10
12
 
11
13
 
12
14
  class QuoteParser(LineParser):
13
- def __init__(self, syntax_registry: SyntaxRegistry, rich_text_converter: MarkdownRichTextConverter) -> None:
15
+ def __init__(
16
+ self,
17
+ syntax_registry: SyntaxDefinitionRegistry,
18
+ rich_text_converter: MarkdownRichTextConverter,
19
+ ) -> None:
14
20
  super().__init__(syntax_registry)
15
21
  self._syntax = syntax_registry.get_quote_syntax()
16
22
  self._rich_text_converter = rich_text_converter
@@ -47,11 +53,16 @@ class QuoteParser(LineParser):
47
53
  return quote_lines
48
54
 
49
55
  async def _process_nested_children(
50
- self, block: CreateQuoteBlock, context: BlockParsingContext, quote_lines: list[str]
56
+ self,
57
+ block: CreateQuoteBlock,
58
+ context: BlockParsingContext,
59
+ quote_lines: list[str],
51
60
  ) -> None:
52
61
  # Calculate indent level after all quote lines
53
62
  last_quote_line_index = len(quote_lines) - 1
54
- child_lines = self._collect_child_lines_after_quote(context, last_quote_line_index)
63
+ child_lines = self._collect_child_lines_after_quote(
64
+ context, last_quote_line_index
65
+ )
55
66
 
56
67
  if not child_lines:
57
68
  return
@@ -62,7 +73,9 @@ class QuoteParser(LineParser):
62
73
 
63
74
  context.lines_consumed += len(child_lines)
64
75
 
65
- def _collect_child_lines_after_quote(self, context: BlockParsingContext, last_quote_index: int) -> list[str]:
76
+ def _collect_child_lines_after_quote(
77
+ self, context: BlockParsingContext, last_quote_index: int
78
+ ) -> list[str]:
66
79
  """Collect indented children after the quote block."""
67
80
  parent_indent_level = context.get_line_indentation_level()
68
81
  remaining_lines = context.get_remaining_lines()
@@ -86,18 +99,24 @@ class QuoteParser(LineParser):
86
99
 
87
100
  return child_lines
88
101
 
89
- async def _parse_child_blocks(self, child_lines: list[str], context: BlockParsingContext) -> list[CreateQuoteBlock]:
102
+ async def _parse_child_blocks(
103
+ self, child_lines: list[str], context: BlockParsingContext
104
+ ) -> list[CreateQuoteBlock]:
90
105
  stripped_lines = self._remove_parent_indentation(child_lines, context)
91
106
  children_text = self._convert_lines_to_text(stripped_lines)
92
107
  return await context.parse_nested_markdown(children_text)
93
108
 
94
- def _remove_parent_indentation(self, lines: list[str], context: BlockParsingContext) -> list[str]:
109
+ def _remove_parent_indentation(
110
+ self, lines: list[str], context: BlockParsingContext
111
+ ) -> list[str]:
95
112
  return context.strip_indentation_level(lines, levels=1)
96
113
 
97
114
  def _convert_lines_to_text(self, lines: list[str]) -> str:
98
115
  return "\n".join(lines)
99
116
 
100
- async def _create_quote_block(self, quote_lines: list[str]) -> CreateQuoteBlock | None:
117
+ async def _create_quote_block(
118
+ self, quote_lines: list[str]
119
+ ) -> CreateQuoteBlock | None:
101
120
  contents = self._extract_quote_contents(quote_lines)
102
121
  if not contents:
103
122
  return None
@@ -6,11 +6,11 @@ from notionary.page.content.parser.parsers.base import (
6
6
  BlockParsingContext,
7
7
  LineParser,
8
8
  )
9
- from notionary.page.content.syntax import SyntaxRegistry
9
+ from notionary.page.content.syntax.definition import SyntaxDefinitionRegistry
10
10
 
11
11
 
12
12
  class SpaceParser(LineParser):
13
- def __init__(self, syntax_registry: SyntaxRegistry) -> None:
13
+ def __init__(self, syntax_registry: SyntaxDefinitionRegistry) -> None:
14
14
  super().__init__(syntax_registry)
15
15
  self._syntax = syntax_registry.get_space_syntax()
16
16