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
@@ -0,0 +1,134 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class AudioExtension(StrEnum):
5
+ AAC = ".aac"
6
+ ADTS = ".adts"
7
+ MID = ".mid"
8
+ MIDI = ".midi"
9
+ MP3 = ".mp3"
10
+ MPGA = ".mpga"
11
+ M4A = ".m4a"
12
+ M4B = ".m4b"
13
+ MP4 = ".mp4"
14
+ OGA = ".oga"
15
+ OGG = ".ogg"
16
+ WAV = ".wav"
17
+ WMA = ".wma"
18
+
19
+
20
+ class AudioMimeType(StrEnum):
21
+ AAC = "audio/aac"
22
+ MIDI = "audio/midi"
23
+ MPEG = "audio/mpeg"
24
+ MP4 = "audio/mp4"
25
+ OGG = "audio/ogg"
26
+ WAV = "audio/wav"
27
+ WMA = "audio/x-ms-wma"
28
+
29
+
30
+ class DocumentExtension(StrEnum):
31
+ PDF = ".pdf"
32
+ TXT = ".txt"
33
+ JSON = ".json"
34
+ DOC = ".doc"
35
+ DOT = ".dot"
36
+ DOCX = ".docx"
37
+ DOTX = ".dotx"
38
+ XLS = ".xls"
39
+ XLT = ".xlt"
40
+ XLA = ".xla"
41
+ XLSX = ".xlsx"
42
+ XLTX = ".xltx"
43
+ PPT = ".ppt"
44
+ POT = ".pot"
45
+ PPS = ".pps"
46
+ PPA = ".ppa"
47
+ PPTX = ".pptx"
48
+ POTX = ".potx"
49
+
50
+
51
+ class DocumentMimeType(StrEnum):
52
+ PDF = "application/pdf"
53
+ PLAIN_TEXT = "text/plain"
54
+ JSON = "application/json"
55
+ MSWORD = "application/msword"
56
+ WORD_DOCUMENT = (
57
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
58
+ )
59
+ WORD_TEMPLATE = (
60
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.template"
61
+ )
62
+ EXCEL = "application/vnd.ms-excel"
63
+ EXCEL_SHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
64
+ EXCEL_TEMPLATE = (
65
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.template"
66
+ )
67
+ POWERPOINT = "application/vnd.ms-powerpoint"
68
+ POWERPOINT_PRESENTATION = (
69
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation"
70
+ )
71
+ POWERPOINT_TEMPLATE = (
72
+ "application/vnd.openxmlformats-officedocument.presentationml.template"
73
+ )
74
+
75
+
76
+ class ImageExtension(StrEnum):
77
+ GIF = ".gif"
78
+ HEIC = ".heic"
79
+ JPEG = ".jpeg"
80
+ JPG = ".jpg"
81
+ PNG = ".png"
82
+ SVG = ".svg"
83
+ TIF = ".tif"
84
+ TIFF = ".tiff"
85
+ WEBP = ".webp"
86
+ ICO = ".ico"
87
+
88
+
89
+ class ImageMimeType(StrEnum):
90
+ GIF = "image/gif"
91
+ HEIC = "image/heic"
92
+ JPEG = "image/jpeg"
93
+ PNG = "image/png"
94
+ SVG = "image/svg+xml"
95
+ TIFF = "image/tiff"
96
+ WEBP = "image/webp"
97
+ ICON = "image/vnd.microsoft.icon"
98
+
99
+
100
+ class VideoExtension(StrEnum):
101
+ AMV = ".amv"
102
+ ASF = ".asf"
103
+ WMV = ".wmv"
104
+ AVI = ".avi"
105
+ F4V = ".f4v"
106
+ FLV = ".flv"
107
+ GIFV = ".gifv"
108
+ M4V = ".m4v"
109
+ MP4 = ".mp4"
110
+ MKV = ".mkv"
111
+ WEBM = ".webm"
112
+ MOV = ".mov"
113
+ QT = ".qt"
114
+ MPEG = ".mpeg"
115
+
116
+
117
+ class VideoMimeType(StrEnum):
118
+ AMV = "video/x-amv"
119
+ ASF = "video/x-ms-asf"
120
+ AVI = "video/x-msvideo"
121
+ F4V = "video/x-f4v"
122
+ FLV = "video/x-flv"
123
+ MP4 = "video/mp4"
124
+ MKV = "video/x-matroska"
125
+ WEBM = "video/webm"
126
+ QUICKTIME = "video/quicktime"
127
+ MPEG = "video/mpeg"
128
+
129
+
130
+ class FileCategory(StrEnum):
131
+ AUDIO = "audio"
132
+ DOCUMENT = "document"
133
+ IMAGE = "image"
134
+ VIDEO = "video"
@@ -0,0 +1,7 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class FileUploadValidator(ABC):
5
+ @abstractmethod
6
+ async def validate(self) -> None:
7
+ pass
@@ -0,0 +1,17 @@
1
+ from notionary.file_upload.validation.port import FileUploadValidator
2
+ from notionary.utils.decorators import time_execution_async
3
+ from notionary.utils.mixins.logging import LoggingMixin
4
+
5
+
6
+ class FileUploadValidationService(LoggingMixin):
7
+ def __init__(self) -> None:
8
+ self._validators: list[FileUploadValidator] = []
9
+
10
+ def register(self, validator: FileUploadValidator) -> None:
11
+ self._validators.append(validator)
12
+
13
+ @time_execution_async()
14
+ async def validate(self) -> None:
15
+ for validator in self._validators:
16
+ self.logger.info("Validating with %s", validator.__class__.__name__)
17
+ await validator.validate()
@@ -0,0 +1,11 @@
1
+ from .file_exists import FileExistsValidator
2
+ from .file_extension import FileExtensionValidator
3
+ from .file_name_length import FileNameLengthValidator
4
+ from .upload_limit import FileUploadLimitValidator
5
+
6
+ __all__ = [
7
+ "FileExistsValidator",
8
+ "FileExtensionValidator",
9
+ "FileNameLengthValidator",
10
+ "FileUploadLimitValidator",
11
+ ]
@@ -0,0 +1,15 @@
1
+ from pathlib import Path
2
+ from typing import override
3
+
4
+ from notionary.exceptions.file_upload import FileNotFoundError
5
+ from notionary.file_upload.validation.port import FileUploadValidator
6
+
7
+
8
+ class FileExistsValidator(FileUploadValidator):
9
+ def __init__(self, file_path: Path) -> None:
10
+ self.file_path = file_path
11
+
12
+ @override
13
+ async def validate(self) -> None:
14
+ if not self.file_path.exists():
15
+ raise FileNotFoundError(str(self.file_path))
@@ -0,0 +1,131 @@
1
+ from pathlib import Path
2
+ from typing import ClassVar, override
3
+
4
+ from notionary.exceptions.file_upload import (
5
+ NoFileExtensionException,
6
+ UnsupportedFileTypeException,
7
+ )
8
+ from notionary.file_upload.validation.models import (
9
+ AudioExtension,
10
+ AudioMimeType,
11
+ DocumentExtension,
12
+ DocumentMimeType,
13
+ FileCategory,
14
+ ImageExtension,
15
+ ImageMimeType,
16
+ VideoExtension,
17
+ VideoMimeType,
18
+ )
19
+ from notionary.file_upload.validation.port import FileUploadValidator
20
+
21
+
22
+ class FileExtensionValidator(FileUploadValidator):
23
+ EXTENSION_TO_MIME: ClassVar[dict[str, str]] = {
24
+ AudioExtension.AAC: AudioMimeType.AAC,
25
+ AudioExtension.ADTS: AudioMimeType.AAC,
26
+ AudioExtension.MID: AudioMimeType.MIDI,
27
+ AudioExtension.MIDI: AudioMimeType.MIDI,
28
+ AudioExtension.MP3: AudioMimeType.MPEG,
29
+ AudioExtension.MPGA: AudioMimeType.MPEG,
30
+ AudioExtension.M4A: AudioMimeType.MP4,
31
+ AudioExtension.M4B: AudioMimeType.MP4,
32
+ AudioExtension.MP4: AudioMimeType.MP4,
33
+ AudioExtension.OGA: AudioMimeType.OGG,
34
+ AudioExtension.OGG: AudioMimeType.OGG,
35
+ AudioExtension.WAV: AudioMimeType.WAV,
36
+ AudioExtension.WMA: AudioMimeType.WMA,
37
+ DocumentExtension.PDF: DocumentMimeType.PDF,
38
+ DocumentExtension.TXT: DocumentMimeType.PLAIN_TEXT,
39
+ DocumentExtension.JSON: DocumentMimeType.JSON,
40
+ DocumentExtension.DOC: DocumentMimeType.MSWORD,
41
+ DocumentExtension.DOT: DocumentMimeType.MSWORD,
42
+ DocumentExtension.DOCX: DocumentMimeType.WORD_DOCUMENT,
43
+ DocumentExtension.DOTX: DocumentMimeType.WORD_TEMPLATE,
44
+ DocumentExtension.XLS: DocumentMimeType.EXCEL,
45
+ DocumentExtension.XLT: DocumentMimeType.EXCEL,
46
+ DocumentExtension.XLA: DocumentMimeType.EXCEL,
47
+ DocumentExtension.XLSX: DocumentMimeType.EXCEL_SHEET,
48
+ DocumentExtension.XLTX: DocumentMimeType.EXCEL_TEMPLATE,
49
+ DocumentExtension.PPT: DocumentMimeType.POWERPOINT,
50
+ DocumentExtension.POT: DocumentMimeType.POWERPOINT,
51
+ DocumentExtension.PPS: DocumentMimeType.POWERPOINT,
52
+ DocumentExtension.PPA: DocumentMimeType.POWERPOINT,
53
+ DocumentExtension.PPTX: DocumentMimeType.POWERPOINT_PRESENTATION,
54
+ DocumentExtension.POTX: DocumentMimeType.POWERPOINT_TEMPLATE,
55
+ ImageExtension.GIF: ImageMimeType.GIF,
56
+ ImageExtension.HEIC: ImageMimeType.HEIC,
57
+ ImageExtension.JPEG: ImageMimeType.JPEG,
58
+ ImageExtension.JPG: ImageMimeType.JPEG,
59
+ ImageExtension.PNG: ImageMimeType.PNG,
60
+ ImageExtension.SVG: ImageMimeType.SVG,
61
+ ImageExtension.TIF: ImageMimeType.TIFF,
62
+ ImageExtension.TIFF: ImageMimeType.TIFF,
63
+ ImageExtension.WEBP: ImageMimeType.WEBP,
64
+ ImageExtension.ICO: ImageMimeType.ICON,
65
+ VideoExtension.AMV: VideoMimeType.AMV,
66
+ VideoExtension.ASF: VideoMimeType.ASF,
67
+ VideoExtension.WMV: VideoMimeType.ASF,
68
+ VideoExtension.AVI: VideoMimeType.AVI,
69
+ VideoExtension.F4V: VideoMimeType.F4V,
70
+ VideoExtension.FLV: VideoMimeType.FLV,
71
+ VideoExtension.GIFV: VideoMimeType.WEBM,
72
+ VideoExtension.M4V: VideoMimeType.MP4,
73
+ VideoExtension.MP4: VideoMimeType.MP4,
74
+ VideoExtension.MKV: VideoMimeType.MKV,
75
+ VideoExtension.WEBM: VideoMimeType.WEBM,
76
+ VideoExtension.MOV: VideoMimeType.QUICKTIME,
77
+ VideoExtension.QT: VideoMimeType.QUICKTIME,
78
+ VideoExtension.MPEG: VideoMimeType.MPEG,
79
+ }
80
+
81
+ EXTENSION_TO_CATEGORY: ClassVar[dict[str, FileCategory]] = {
82
+ **{ext.value: FileCategory.AUDIO for ext in AudioExtension},
83
+ **{ext.value: FileCategory.DOCUMENT for ext in DocumentExtension},
84
+ **{ext.value: FileCategory.IMAGE for ext in ImageExtension},
85
+ **{ext.value: FileCategory.VIDEO for ext in VideoExtension},
86
+ }
87
+
88
+ def __init__(self, filename: str | Path) -> None:
89
+ self.filename = Path(filename).name if isinstance(filename, Path) else filename
90
+
91
+ @override
92
+ async def validate(self) -> None:
93
+ extension = self._extract_extension(self.filename)
94
+
95
+ if not extension:
96
+ raise NoFileExtensionException(self.filename)
97
+
98
+ if not self._is_supported(extension):
99
+ supported_by_category = self._get_supported_extensions_by_category()
100
+ raise UnsupportedFileTypeException(
101
+ extension, self.filename, supported_by_category
102
+ )
103
+
104
+ @staticmethod
105
+ def _extract_extension(filename: str) -> str:
106
+ import os
107
+
108
+ _, ext = os.path.splitext(filename)
109
+ return ext.lower()
110
+
111
+ def _is_supported(self, extension: str) -> bool:
112
+ normalized = self._normalize_extension(extension)
113
+ return normalized in self.EXTENSION_TO_MIME
114
+
115
+ @staticmethod
116
+ def _normalize_extension(extension: str) -> str:
117
+ extension = extension.lower()
118
+ if not extension.startswith("."):
119
+ extension = f".{extension}"
120
+ return extension
121
+
122
+ def _get_supported_extensions_by_category(self) -> dict[str, list[str]]:
123
+ result = {}
124
+ for category in FileCategory:
125
+ extensions = [
126
+ ext
127
+ for ext, cat in self.EXTENSION_TO_CATEGORY.items()
128
+ if cat == category
129
+ ]
130
+ result[category.value] = extensions
131
+ return result
@@ -0,0 +1,21 @@
1
+ from typing import override
2
+
3
+ from notionary.exceptions.file_upload import FilenameTooLongError
4
+ from notionary.file_upload.config.constants import NOTION_MAX_FILENAME_BYTES
5
+ from notionary.file_upload.validation.port import FileUploadValidator
6
+
7
+
8
+ class FileNameLengthValidator(FileUploadValidator):
9
+ def __init__(self, filename: str) -> None:
10
+ self._filename = filename
11
+ self._max_filename_bytes = NOTION_MAX_FILENAME_BYTES
12
+
13
+ @override
14
+ async def validate(self) -> None:
15
+ filename_bytes = len(self._filename.encode("utf-8"))
16
+ if filename_bytes > self._max_filename_bytes:
17
+ raise FilenameTooLongError(
18
+ filename=self._filename,
19
+ filename_bytes=filename_bytes,
20
+ max_filename_bytes=self._max_filename_bytes,
21
+ )
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING, override
5
+
6
+ from notionary.exceptions.file_upload import FileSizeException
7
+ from notionary.file_upload.validation.port import FileUploadValidator
8
+
9
+ if TYPE_CHECKING:
10
+ from notionary.user import BotUser
11
+
12
+
13
+ class FileUploadLimitValidator(FileUploadValidator):
14
+ def __init__(self, filename: str | Path, file_size_bytes: int) -> None:
15
+ self.filename = Path(filename).name if isinstance(filename, Path) else filename
16
+ self.file_size_bytes = file_size_bytes
17
+
18
+ @override
19
+ async def validate(self, integration: BotUser | None = None) -> None:
20
+ from notionary.user import BotUser
21
+
22
+ integration = integration or await BotUser.from_current_integration()
23
+
24
+ max_file_size_in_bytes = integration.workspace_file_upload_limit_in_bytes
25
+
26
+ if self.file_size_bytes > max_file_size_in_bytes:
27
+ raise FileSizeException(
28
+ filename=self.filename,
29
+ file_size_bytes=self.file_size_bytes,
30
+ max_size_bytes=max_file_size_in_bytes,
31
+ )
notionary/http/client.py CHANGED
@@ -18,7 +18,7 @@ from notionary.http.models import HttpMethod
18
18
  from notionary.shared.typings import JsonDict
19
19
  from notionary.utils.mixins.logging import LoggingMixin
20
20
 
21
- load_dotenv()
21
+ load_dotenv(override=True)
22
22
 
23
23
 
24
24
  class NotionHttpClient(LoggingMixin):
@@ -44,34 +44,32 @@ class NotionHttpClient(LoggingMixin):
44
44
  self._is_initialized = False
45
45
 
46
46
  def __del__(self):
47
- """Auto-cleanup when client is destroyed."""
48
47
  if not hasattr(self, "client") or not self.client:
49
48
  return
50
49
 
51
50
  try:
52
51
  loop = asyncio.get_event_loop()
53
52
  if not loop.is_running():
54
- self.logger.warning("Event loop not running, could not auto-close NotionHttpClient")
53
+ self.logger.warning(
54
+ "Event loop not running, could not auto-close NotionHttpClient"
55
+ )
55
56
  return
56
57
 
57
58
  loop.create_task(self.close())
58
59
  self.logger.debug("Created cleanup task for NotionHttpClient")
59
60
  except RuntimeError:
60
- self.logger.warning("No event loop available for auto-closing NotionHttpClient")
61
+ self.logger.warning(
62
+ "No event loop available for auto-closing NotionHttpClient"
63
+ )
61
64
 
62
65
  async def __aenter__(self):
63
- """Async context manager entry."""
64
66
  await self._ensure_initialized()
65
67
  return self
66
68
 
67
69
  async def __aexit__(self, exc_type, exc_val, exc_tb):
68
- """Async context manager exit."""
69
70
  await self.close()
70
71
 
71
72
  async def close(self) -> None:
72
- """
73
- Closes the HTTP client and releases resources.
74
- """
75
73
  if not hasattr(self, "client") or not self.client:
76
74
  return
77
75
 
@@ -80,34 +78,31 @@ class NotionHttpClient(LoggingMixin):
80
78
  self._is_initialized = False
81
79
  self.logger.debug("NotionHttpClient closed")
82
80
 
83
- async def get(self, endpoint: str, params: JsonDict | None = None) -> JsonDict | None:
84
- return await self.make_request(HttpMethod.GET, endpoint, params=params)
81
+ async def get(
82
+ self, endpoint: str, params: JsonDict | None = None
83
+ ) -> JsonDict | None:
84
+ return await self._make_request(HttpMethod.GET, endpoint, params=params)
85
85
 
86
- async def post(self, endpoint: str, data: JsonDict | None = None) -> JsonDict | None:
87
- return await self.make_request(HttpMethod.POST, endpoint, data)
86
+ async def post(
87
+ self, endpoint: str, data: JsonDict | None = None
88
+ ) -> JsonDict | None:
89
+ return await self._make_request(HttpMethod.POST, endpoint, data)
88
90
 
89
- async def patch(self, endpoint: str, data: JsonDict | None = None) -> JsonDict | None:
90
- return await self.make_request(HttpMethod.PATCH, endpoint, data)
91
+ async def patch(
92
+ self, endpoint: str, data: JsonDict | None = None
93
+ ) -> JsonDict | None:
94
+ return await self._make_request(HttpMethod.PATCH, endpoint, data)
91
95
 
92
96
  async def delete(self, endpoint: str) -> JsonDict | None:
93
- return await self.make_request(HttpMethod.DELETE, endpoint)
97
+ return await self._make_request(HttpMethod.DELETE, endpoint)
94
98
 
95
- async def make_request(
99
+ async def _make_request(
96
100
  self,
97
101
  method: HttpMethod,
98
102
  endpoint: str,
99
103
  data: JsonDict | None = None,
100
104
  params: JsonDict | None = None,
101
105
  ) -> JsonDict | None:
102
- """
103
- Executes an HTTP request and returns the data or None on error.
104
-
105
- Args:
106
- method: HTTP method to use
107
- endpoint: API endpoint
108
- data: Request body data (for POST/PATCH)
109
- params: Query parameters (for GET requests)
110
- """
111
106
  await self._ensure_initialized()
112
107
 
113
108
  url = f"{self.BASE_URL}/{endpoint.lstrip('/')}"
@@ -120,10 +115,15 @@ class NotionHttpClient(LoggingMixin):
120
115
  if params:
121
116
  request_kwargs["params"] = params
122
117
 
123
- if method.value in [HttpMethod.POST.value, HttpMethod.PATCH.value] and data is not None:
118
+ if (
119
+ method.value in [HttpMethod.POST.value, HttpMethod.PATCH.value]
120
+ and data is not None
121
+ ):
124
122
  request_kwargs["json"] = data
125
123
 
126
- response: httpx.Response = await getattr(self.client, method.value)(url, **request_kwargs)
124
+ response: httpx.Response = await getattr(self.client, method.value)(
125
+ url, **request_kwargs
126
+ )
127
127
 
128
128
  response.raise_for_status()
129
129
  result_data = response.json()
@@ -141,7 +141,6 @@ class NotionHttpClient(LoggingMixin):
141
141
  status_code = e.response.status_code
142
142
  response_text = e.response.text
143
143
 
144
- # Map HTTP status codes to specific business exceptions
145
144
  if status_code == 401:
146
145
  raise NotionAuthenticationError(
147
146
  "Invalid or missing API key. Please check your Notion integration token.",
@@ -189,7 +188,11 @@ class NotionHttpClient(LoggingMixin):
189
188
 
190
189
  def _find_token(self) -> str | None:
191
190
  token = next(
192
- (os.getenv(var) for var in ("NOTION_SECRET", "NOTION_INTEGRATION_KEY", "NOTION_TOKEN") if os.getenv(var)),
191
+ (
192
+ os.getenv(var)
193
+ for var in ("NOTION_SECRET", "NOTION_INTEGRATION_KEY", "NOTION_TOKEN")
194
+ if os.getenv(var)
195
+ ),
193
196
  None,
194
197
  )
195
198
  if token:
@@ -0,0 +1,9 @@
1
+ from .factory import PageContentServiceFactory
2
+ from .service import PageContentService
3
+ from .syntax.prompts import SyntaxPromptRegistry
4
+
5
+ __all__ = [
6
+ "PageContentService",
7
+ "PageContentServiceFactory",
8
+ "SyntaxPromptRegistry",
9
+ ]
@@ -1,6 +1,8 @@
1
1
  from notionary.blocks.client import NotionBlockHttpClient
2
2
  from notionary.page.content.parser.factory import ConverterChainFactory
3
- from notionary.page.content.parser.post_processing.handlers import RichTextLengthTruncationPostProcessor
3
+ from notionary.page.content.parser.post_processing.handlers import (
4
+ RichTextLengthTruncationPostProcessor,
5
+ )
4
6
  from notionary.page.content.parser.post_processing.service import BlockPostProcessor
5
7
  from notionary.page.content.parser.pre_processsing.handlers import (
6
8
  ColumnSyntaxPreProcessor,
@@ -11,8 +13,12 @@ from notionary.page.content.parser.pre_processsing.handlers import (
11
13
  from notionary.page.content.parser.pre_processsing.service import MarkdownPreProcessor
12
14
  from notionary.page.content.parser.service import MarkdownToNotionConverter
13
15
  from notionary.page.content.renderer.factory import RendererChainFactory
14
- from notionary.page.content.renderer.post_processing.handlers import NumberedListPlaceholderReplacerPostProcessor
15
- from notionary.page.content.renderer.post_processing.service import MarkdownRenderingPostProcessor
16
+ from notionary.page.content.renderer.post_processing.handlers import (
17
+ NumberedListPlaceholderReplacerPostProcessor,
18
+ )
19
+ from notionary.page.content.renderer.post_processing.service import (
20
+ MarkdownRenderingPostProcessor,
21
+ )
16
22
  from notionary.page.content.renderer.service import NotionToMarkdownConverter
17
23
  from notionary.page.content.service import PageContentService
18
24
 
@@ -23,10 +29,14 @@ class PageContentServiceFactory:
23
29
  converter_chain_factory: ConverterChainFactory | None = None,
24
30
  renderer_chain_factory: RendererChainFactory | None = None,
25
31
  ) -> None:
26
- self._converter_chain_factory = converter_chain_factory or ConverterChainFactory()
32
+ self._converter_chain_factory = (
33
+ converter_chain_factory or ConverterChainFactory()
34
+ )
27
35
  self._renderer_chain_factory = renderer_chain_factory or RendererChainFactory()
28
36
 
29
- def create(self, page_id: str, block_client: NotionBlockHttpClient) -> PageContentService:
37
+ def create(
38
+ self, page_id: str, block_client: NotionBlockHttpClient
39
+ ) -> PageContentService:
30
40
  markdown_converter = self._create_markdown_to_notion_converter()
31
41
  notion_to_markdown_converter = self._create_notion_to_markdown_converter()
32
42
 
@@ -50,7 +60,9 @@ class PageContentServiceFactory:
50
60
 
51
61
  def _create_notion_to_markdown_converter(self) -> NotionToMarkdownConverter:
52
62
  renderer_chain = self._renderer_chain_factory.create()
53
- markdown_rendering_post_processor = self._create_markdown_rendering_post_processor()
63
+ markdown_rendering_post_processor = (
64
+ self._create_markdown_rendering_post_processor()
65
+ )
54
66
  return NotionToMarkdownConverter(
55
67
  renderer_chain=renderer_chain,
56
68
  post_processor=markdown_rendering_post_processor,
@@ -69,7 +81,9 @@ class PageContentServiceFactory:
69
81
  post_processor.register(RichTextLengthTruncationPostProcessor())
70
82
  return post_processor
71
83
 
72
- def _create_markdown_rendering_post_processor(self) -> MarkdownRenderingPostProcessor:
84
+ def _create_markdown_rendering_post_processor(
85
+ self,
86
+ ) -> MarkdownRenderingPostProcessor:
73
87
  post_processor = MarkdownRenderingPostProcessor()
74
88
  post_processor.register(NumberedListPlaceholderReplacerPostProcessor())
75
89
  return post_processor