notionary 0.3.0__py3-none-any.whl → 0.4.0__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.
- notionary/__init__.py +14 -2
- notionary/blocks/enums.py +27 -6
- notionary/blocks/schemas.py +32 -78
- notionary/comments/client.py +6 -9
- notionary/comments/schemas.py +2 -29
- notionary/data_source/http/data_source_instance_client.py +4 -4
- notionary/data_source/properties/schemas.py +128 -107
- notionary/data_source/query/__init__.py +9 -0
- notionary/data_source/query/builder.py +12 -3
- notionary/data_source/query/schema.py +5 -0
- notionary/data_source/schemas.py +2 -2
- notionary/data_source/service.py +43 -132
- notionary/database/schemas.py +2 -2
- notionary/database/service.py +19 -63
- notionary/exceptions/__init__.py +10 -2
- notionary/exceptions/api.py +2 -2
- notionary/exceptions/base.py +1 -1
- notionary/exceptions/block_parsing.py +24 -3
- notionary/exceptions/data_source/builder.py +2 -2
- notionary/exceptions/data_source/properties.py +3 -3
- notionary/exceptions/file_upload.py +67 -0
- notionary/exceptions/properties.py +4 -4
- notionary/exceptions/search.py +4 -4
- notionary/file_upload/__init__.py +4 -0
- notionary/file_upload/client.py +124 -210
- notionary/file_upload/config/__init__.py +17 -0
- notionary/file_upload/config/config.py +32 -0
- notionary/file_upload/config/constants.py +16 -0
- notionary/file_upload/file/reader.py +28 -0
- notionary/file_upload/query/__init__.py +7 -0
- notionary/file_upload/query/builder.py +54 -0
- notionary/file_upload/query/models.py +37 -0
- notionary/file_upload/schemas.py +78 -0
- notionary/file_upload/service.py +152 -289
- notionary/file_upload/validation/factory.py +64 -0
- notionary/file_upload/validation/impl/file_name_length.py +23 -0
- notionary/file_upload/validation/models.py +124 -0
- notionary/file_upload/validation/port.py +7 -0
- notionary/file_upload/validation/service.py +17 -0
- notionary/file_upload/validation/validators/__init__.py +11 -0
- notionary/file_upload/validation/validators/file_exists.py +15 -0
- notionary/file_upload/validation/validators/file_extension.py +122 -0
- notionary/file_upload/validation/validators/file_name_length.py +21 -0
- notionary/file_upload/validation/validators/upload_limit.py +31 -0
- notionary/http/client.py +7 -23
- notionary/page/content/factory.py +2 -0
- notionary/page/content/parser/factory.py +8 -5
- notionary/page/content/parser/parsers/audio.py +8 -33
- notionary/page/content/parser/parsers/embed.py +0 -2
- notionary/page/content/parser/parsers/file.py +8 -35
- notionary/page/content/parser/parsers/file_like_block.py +89 -0
- notionary/page/content/parser/parsers/image.py +8 -35
- notionary/page/content/parser/parsers/pdf.py +8 -35
- notionary/page/content/parser/parsers/video.py +8 -35
- notionary/page/content/parser/pre_processsing/handlers/__init__.py +2 -0
- notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +12 -8
- notionary/page/content/parser/pre_processsing/handlers/indentation.py +2 -0
- notionary/page/content/parser/pre_processsing/handlers/video_syntax.py +66 -0
- notionary/page/content/parser/pre_processsing/handlers/whitespace.py +2 -0
- notionary/page/content/renderer/renderers/audio.py +9 -21
- notionary/page/content/renderer/renderers/file.py +9 -21
- notionary/page/content/renderer/renderers/file_like_block.py +43 -0
- notionary/page/content/renderer/renderers/image.py +9 -21
- notionary/page/content/renderer/renderers/pdf.py +9 -21
- notionary/page/content/renderer/renderers/video.py +9 -21
- notionary/page/content/syntax/__init__.py +2 -1
- notionary/page/content/syntax/registry.py +38 -60
- notionary/page/properties/client.py +3 -3
- notionary/page/properties/{models.py → schemas.py} +93 -107
- notionary/page/properties/service.py +15 -4
- notionary/page/schemas.py +3 -3
- notionary/page/service.py +18 -79
- notionary/shared/entity/dto_parsers.py +1 -36
- notionary/shared/entity/entity_metadata_update_client.py +18 -4
- notionary/shared/entity/schemas.py +6 -6
- notionary/shared/entity/service.py +121 -40
- notionary/shared/models/file.py +34 -6
- notionary/shared/models/icon.py +5 -12
- notionary/user/bot.py +12 -12
- notionary/utils/decorators.py +8 -8
- notionary/utils/pagination.py +36 -32
- notionary/workspace/__init__.py +2 -2
- notionary/workspace/client.py +2 -0
- notionary/workspace/query/__init__.py +3 -2
- notionary/workspace/query/builder.py +25 -1
- notionary/workspace/query/models.py +9 -1
- notionary/workspace/query/service.py +15 -11
- notionary/workspace/service.py +46 -36
- {notionary-0.3.0.dist-info → notionary-0.4.0.dist-info}/METADATA +9 -5
- {notionary-0.3.0.dist-info → notionary-0.4.0.dist-info}/RECORD +92 -71
- notionary/file_upload/models.py +0 -69
- notionary/page/page_context.py +0 -50
- notionary/shared/models/cover.py +0 -20
- {notionary-0.3.0.dist-info → notionary-0.4.0.dist-info}/WHEEL +0 -0
- {notionary-0.3.0.dist-info → notionary-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
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 = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
57
|
+
WORD_TEMPLATE = "application/vnd.openxmlformats-officedocument.wordprocessingml.template"
|
|
58
|
+
EXCEL = "application/vnd.ms-excel"
|
|
59
|
+
EXCEL_SHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
60
|
+
EXCEL_TEMPLATE = "application/vnd.openxmlformats-officedocument.spreadsheetml.template"
|
|
61
|
+
POWERPOINT = "application/vnd.ms-powerpoint"
|
|
62
|
+
POWERPOINT_PRESENTATION = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
63
|
+
POWERPOINT_TEMPLATE = "application/vnd.openxmlformats-officedocument.presentationml.template"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ImageExtension(StrEnum):
|
|
67
|
+
GIF = ".gif"
|
|
68
|
+
HEIC = ".heic"
|
|
69
|
+
JPEG = ".jpeg"
|
|
70
|
+
JPG = ".jpg"
|
|
71
|
+
PNG = ".png"
|
|
72
|
+
SVG = ".svg"
|
|
73
|
+
TIF = ".tif"
|
|
74
|
+
TIFF = ".tiff"
|
|
75
|
+
WEBP = ".webp"
|
|
76
|
+
ICO = ".ico"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ImageMimeType(StrEnum):
|
|
80
|
+
GIF = "image/gif"
|
|
81
|
+
HEIC = "image/heic"
|
|
82
|
+
JPEG = "image/jpeg"
|
|
83
|
+
PNG = "image/png"
|
|
84
|
+
SVG = "image/svg+xml"
|
|
85
|
+
TIFF = "image/tiff"
|
|
86
|
+
WEBP = "image/webp"
|
|
87
|
+
ICON = "image/vnd.microsoft.icon"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class VideoExtension(StrEnum):
|
|
91
|
+
AMV = ".amv"
|
|
92
|
+
ASF = ".asf"
|
|
93
|
+
WMV = ".wmv"
|
|
94
|
+
AVI = ".avi"
|
|
95
|
+
F4V = ".f4v"
|
|
96
|
+
FLV = ".flv"
|
|
97
|
+
GIFV = ".gifv"
|
|
98
|
+
M4V = ".m4v"
|
|
99
|
+
MP4 = ".mp4"
|
|
100
|
+
MKV = ".mkv"
|
|
101
|
+
WEBM = ".webm"
|
|
102
|
+
MOV = ".mov"
|
|
103
|
+
QT = ".qt"
|
|
104
|
+
MPEG = ".mpeg"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class VideoMimeType(StrEnum):
|
|
108
|
+
AMV = "video/x-amv"
|
|
109
|
+
ASF = "video/x-ms-asf"
|
|
110
|
+
AVI = "video/x-msvideo"
|
|
111
|
+
F4V = "video/x-f4v"
|
|
112
|
+
FLV = "video/x-flv"
|
|
113
|
+
MP4 = "video/mp4"
|
|
114
|
+
MKV = "video/x-matroska"
|
|
115
|
+
WEBM = "video/webm"
|
|
116
|
+
QUICKTIME = "video/quicktime"
|
|
117
|
+
MPEG = "video/mpeg"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class FileCategory(StrEnum):
|
|
121
|
+
AUDIO = "audio"
|
|
122
|
+
DOCUMENT = "document"
|
|
123
|
+
IMAGE = "image"
|
|
124
|
+
VIDEO = "video"
|
|
@@ -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,122 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import ClassVar, override
|
|
3
|
+
|
|
4
|
+
from notionary.exceptions.file_upload import NoFileExtensionException, UnsupportedFileTypeException
|
|
5
|
+
from notionary.file_upload.validation.models import (
|
|
6
|
+
AudioExtension,
|
|
7
|
+
AudioMimeType,
|
|
8
|
+
DocumentExtension,
|
|
9
|
+
DocumentMimeType,
|
|
10
|
+
FileCategory,
|
|
11
|
+
ImageExtension,
|
|
12
|
+
ImageMimeType,
|
|
13
|
+
VideoExtension,
|
|
14
|
+
VideoMimeType,
|
|
15
|
+
)
|
|
16
|
+
from notionary.file_upload.validation.port import FileUploadValidator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FileExtensionValidator(FileUploadValidator):
|
|
20
|
+
EXTENSION_TO_MIME: ClassVar[dict[str, str]] = {
|
|
21
|
+
AudioExtension.AAC: AudioMimeType.AAC,
|
|
22
|
+
AudioExtension.ADTS: AudioMimeType.AAC,
|
|
23
|
+
AudioExtension.MID: AudioMimeType.MIDI,
|
|
24
|
+
AudioExtension.MIDI: AudioMimeType.MIDI,
|
|
25
|
+
AudioExtension.MP3: AudioMimeType.MPEG,
|
|
26
|
+
AudioExtension.MPGA: AudioMimeType.MPEG,
|
|
27
|
+
AudioExtension.M4A: AudioMimeType.MP4,
|
|
28
|
+
AudioExtension.M4B: AudioMimeType.MP4,
|
|
29
|
+
AudioExtension.MP4: AudioMimeType.MP4,
|
|
30
|
+
AudioExtension.OGA: AudioMimeType.OGG,
|
|
31
|
+
AudioExtension.OGG: AudioMimeType.OGG,
|
|
32
|
+
AudioExtension.WAV: AudioMimeType.WAV,
|
|
33
|
+
AudioExtension.WMA: AudioMimeType.WMA,
|
|
34
|
+
DocumentExtension.PDF: DocumentMimeType.PDF,
|
|
35
|
+
DocumentExtension.TXT: DocumentMimeType.PLAIN_TEXT,
|
|
36
|
+
DocumentExtension.JSON: DocumentMimeType.JSON,
|
|
37
|
+
DocumentExtension.DOC: DocumentMimeType.MSWORD,
|
|
38
|
+
DocumentExtension.DOT: DocumentMimeType.MSWORD,
|
|
39
|
+
DocumentExtension.DOCX: DocumentMimeType.WORD_DOCUMENT,
|
|
40
|
+
DocumentExtension.DOTX: DocumentMimeType.WORD_TEMPLATE,
|
|
41
|
+
DocumentExtension.XLS: DocumentMimeType.EXCEL,
|
|
42
|
+
DocumentExtension.XLT: DocumentMimeType.EXCEL,
|
|
43
|
+
DocumentExtension.XLA: DocumentMimeType.EXCEL,
|
|
44
|
+
DocumentExtension.XLSX: DocumentMimeType.EXCEL_SHEET,
|
|
45
|
+
DocumentExtension.XLTX: DocumentMimeType.EXCEL_TEMPLATE,
|
|
46
|
+
DocumentExtension.PPT: DocumentMimeType.POWERPOINT,
|
|
47
|
+
DocumentExtension.POT: DocumentMimeType.POWERPOINT,
|
|
48
|
+
DocumentExtension.PPS: DocumentMimeType.POWERPOINT,
|
|
49
|
+
DocumentExtension.PPA: DocumentMimeType.POWERPOINT,
|
|
50
|
+
DocumentExtension.PPTX: DocumentMimeType.POWERPOINT_PRESENTATION,
|
|
51
|
+
DocumentExtension.POTX: DocumentMimeType.POWERPOINT_TEMPLATE,
|
|
52
|
+
ImageExtension.GIF: ImageMimeType.GIF,
|
|
53
|
+
ImageExtension.HEIC: ImageMimeType.HEIC,
|
|
54
|
+
ImageExtension.JPEG: ImageMimeType.JPEG,
|
|
55
|
+
ImageExtension.JPG: ImageMimeType.JPEG,
|
|
56
|
+
ImageExtension.PNG: ImageMimeType.PNG,
|
|
57
|
+
ImageExtension.SVG: ImageMimeType.SVG,
|
|
58
|
+
ImageExtension.TIF: ImageMimeType.TIFF,
|
|
59
|
+
ImageExtension.TIFF: ImageMimeType.TIFF,
|
|
60
|
+
ImageExtension.WEBP: ImageMimeType.WEBP,
|
|
61
|
+
ImageExtension.ICO: ImageMimeType.ICON,
|
|
62
|
+
VideoExtension.AMV: VideoMimeType.AMV,
|
|
63
|
+
VideoExtension.ASF: VideoMimeType.ASF,
|
|
64
|
+
VideoExtension.WMV: VideoMimeType.ASF,
|
|
65
|
+
VideoExtension.AVI: VideoMimeType.AVI,
|
|
66
|
+
VideoExtension.F4V: VideoMimeType.F4V,
|
|
67
|
+
VideoExtension.FLV: VideoMimeType.FLV,
|
|
68
|
+
VideoExtension.GIFV: VideoMimeType.WEBM,
|
|
69
|
+
VideoExtension.M4V: VideoMimeType.MP4,
|
|
70
|
+
VideoExtension.MP4: VideoMimeType.MP4,
|
|
71
|
+
VideoExtension.MKV: VideoMimeType.MKV,
|
|
72
|
+
VideoExtension.WEBM: VideoMimeType.WEBM,
|
|
73
|
+
VideoExtension.MOV: VideoMimeType.QUICKTIME,
|
|
74
|
+
VideoExtension.QT: VideoMimeType.QUICKTIME,
|
|
75
|
+
VideoExtension.MPEG: VideoMimeType.MPEG,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
EXTENSION_TO_CATEGORY: ClassVar[dict[str, FileCategory]] = {
|
|
79
|
+
**{ext.value: FileCategory.AUDIO for ext in AudioExtension},
|
|
80
|
+
**{ext.value: FileCategory.DOCUMENT for ext in DocumentExtension},
|
|
81
|
+
**{ext.value: FileCategory.IMAGE for ext in ImageExtension},
|
|
82
|
+
**{ext.value: FileCategory.VIDEO for ext in VideoExtension},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def __init__(self, filename: str | Path) -> None:
|
|
86
|
+
self.filename = Path(filename).name if isinstance(filename, Path) else filename
|
|
87
|
+
|
|
88
|
+
@override
|
|
89
|
+
async def validate(self) -> None:
|
|
90
|
+
extension = self._extract_extension(self.filename)
|
|
91
|
+
|
|
92
|
+
if not extension:
|
|
93
|
+
raise NoFileExtensionException(self.filename)
|
|
94
|
+
|
|
95
|
+
if not self._is_supported(extension):
|
|
96
|
+
supported_by_category = self._get_supported_extensions_by_category()
|
|
97
|
+
raise UnsupportedFileTypeException(extension, self.filename, supported_by_category)
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def _extract_extension(filename: str) -> str:
|
|
101
|
+
import os
|
|
102
|
+
|
|
103
|
+
_, ext = os.path.splitext(filename)
|
|
104
|
+
return ext.lower()
|
|
105
|
+
|
|
106
|
+
def _is_supported(self, extension: str) -> bool:
|
|
107
|
+
normalized = self._normalize_extension(extension)
|
|
108
|
+
return normalized in self.EXTENSION_TO_MIME
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _normalize_extension(extension: str) -> str:
|
|
112
|
+
extension = extension.lower()
|
|
113
|
+
if not extension.startswith("."):
|
|
114
|
+
extension = f".{extension}"
|
|
115
|
+
return extension
|
|
116
|
+
|
|
117
|
+
def _get_supported_extensions_by_category(self) -> dict[str, list[str]]:
|
|
118
|
+
result = {}
|
|
119
|
+
for category in FileCategory:
|
|
120
|
+
extensions = [ext for ext, cat in self.EXTENSION_TO_CATEGORY.items() if cat == category]
|
|
121
|
+
result[category.value] = extensions
|
|
122
|
+
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,7 +44,6 @@ 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
|
|
|
@@ -60,18 +59,13 @@ class NotionHttpClient(LoggingMixin):
|
|
|
60
59
|
self.logger.warning("No event loop available for auto-closing NotionHttpClient")
|
|
61
60
|
|
|
62
61
|
async def __aenter__(self):
|
|
63
|
-
"""Async context manager entry."""
|
|
64
62
|
await self._ensure_initialized()
|
|
65
63
|
return self
|
|
66
64
|
|
|
67
65
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
68
|
-
"""Async context manager exit."""
|
|
69
66
|
await self.close()
|
|
70
67
|
|
|
71
68
|
async def close(self) -> None:
|
|
72
|
-
"""
|
|
73
|
-
Closes the HTTP client and releases resources.
|
|
74
|
-
"""
|
|
75
69
|
if not hasattr(self, "client") or not self.client:
|
|
76
70
|
return
|
|
77
71
|
|
|
@@ -81,33 +75,24 @@ class NotionHttpClient(LoggingMixin):
|
|
|
81
75
|
self.logger.debug("NotionHttpClient closed")
|
|
82
76
|
|
|
83
77
|
async def get(self, endpoint: str, params: JsonDict | None = None) -> JsonDict | None:
|
|
84
|
-
return await self.
|
|
78
|
+
return await self._make_request(HttpMethod.GET, endpoint, params=params)
|
|
85
79
|
|
|
86
80
|
async def post(self, endpoint: str, data: JsonDict | None = None) -> JsonDict | None:
|
|
87
|
-
return await self.
|
|
81
|
+
return await self._make_request(HttpMethod.POST, endpoint, data)
|
|
88
82
|
|
|
89
83
|
async def patch(self, endpoint: str, data: JsonDict | None = None) -> JsonDict | None:
|
|
90
|
-
return await self.
|
|
84
|
+
return await self._make_request(HttpMethod.PATCH, endpoint, data)
|
|
91
85
|
|
|
92
86
|
async def delete(self, endpoint: str) -> JsonDict | None:
|
|
93
|
-
return await self.
|
|
87
|
+
return await self._make_request(HttpMethod.DELETE, endpoint)
|
|
94
88
|
|
|
95
|
-
async def
|
|
89
|
+
async def _make_request(
|
|
96
90
|
self,
|
|
97
91
|
method: HttpMethod,
|
|
98
92
|
endpoint: str,
|
|
99
93
|
data: JsonDict | None = None,
|
|
100
94
|
params: JsonDict | None = None,
|
|
101
95
|
) -> 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
96
|
await self._ensure_initialized()
|
|
112
97
|
|
|
113
98
|
url = f"{self.BASE_URL}/{endpoint.lstrip('/')}"
|
|
@@ -141,7 +126,6 @@ class NotionHttpClient(LoggingMixin):
|
|
|
141
126
|
status_code = e.response.status_code
|
|
142
127
|
response_text = e.response.text
|
|
143
128
|
|
|
144
|
-
# Map HTTP status codes to specific business exceptions
|
|
145
129
|
if status_code == 401:
|
|
146
130
|
raise NotionAuthenticationError(
|
|
147
131
|
"Invalid or missing API key. Please check your Notion integration token.",
|
|
@@ -158,7 +142,7 @@ class NotionHttpClient(LoggingMixin):
|
|
|
158
142
|
)
|
|
159
143
|
if status_code == 404:
|
|
160
144
|
raise NotionResourceNotFoundError(
|
|
161
|
-
"The requested resource was not found. Please verify the page/database ID.",
|
|
145
|
+
"The requested resource was not found. Please verify the page/database/datasource ID.",
|
|
162
146
|
status_code=status_code,
|
|
163
147
|
response_text=response_text,
|
|
164
148
|
)
|
|
@@ -5,6 +5,7 @@ from notionary.page.content.parser.post_processing.service import BlockPostProce
|
|
|
5
5
|
from notionary.page.content.parser.pre_processsing.handlers import (
|
|
6
6
|
ColumnSyntaxPreProcessor,
|
|
7
7
|
IndentationNormalizer,
|
|
8
|
+
VideoFormatPreProcessor,
|
|
8
9
|
WhitespacePreProcessor,
|
|
9
10
|
)
|
|
10
11
|
from notionary.page.content.parser.pre_processsing.service import MarkdownPreProcessor
|
|
@@ -60,6 +61,7 @@ class PageContentServiceFactory:
|
|
|
60
61
|
pre_processor.register(ColumnSyntaxPreProcessor())
|
|
61
62
|
pre_processor.register(WhitespacePreProcessor())
|
|
62
63
|
pre_processor.register(IndentationNormalizer())
|
|
64
|
+
pre_processor.register(VideoFormatPreProcessor())
|
|
63
65
|
return pre_processor
|
|
64
66
|
|
|
65
67
|
def _create_post_processor(self) -> BlockPostProcessor:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from notionary.blocks.rich_text.markdown_rich_text_converter import (
|
|
2
2
|
MarkdownRichTextConverter,
|
|
3
3
|
)
|
|
4
|
+
from notionary.file_upload.service import NotionFileUpload
|
|
4
5
|
from notionary.page.content.parser.parsers import (
|
|
5
6
|
AudioParser,
|
|
6
7
|
BookmarkParser,
|
|
@@ -37,9 +38,11 @@ class ConverterChainFactory:
|
|
|
37
38
|
self,
|
|
38
39
|
rich_text_converter: MarkdownRichTextConverter | None = None,
|
|
39
40
|
syntax_registry: SyntaxRegistry | None = None,
|
|
41
|
+
file_upload_service: NotionFileUpload | None = None,
|
|
40
42
|
) -> None:
|
|
41
43
|
self._rich_text_converter = rich_text_converter or MarkdownRichTextConverter()
|
|
42
44
|
self._syntax_registry = syntax_registry or SyntaxRegistry()
|
|
45
|
+
self._file_upload_service = file_upload_service
|
|
43
46
|
|
|
44
47
|
def create(self) -> LineParser:
|
|
45
48
|
# multi-line (structural) blocks
|
|
@@ -186,19 +189,19 @@ class ConverterChainFactory:
|
|
|
186
189
|
return EmbedParser(syntax_registry=self._syntax_registry)
|
|
187
190
|
|
|
188
191
|
def _create_image_parser(self) -> ImageParser:
|
|
189
|
-
return ImageParser(syntax_registry=self._syntax_registry)
|
|
192
|
+
return ImageParser(syntax_registry=self._syntax_registry, file_upload_service=self._file_upload_service)
|
|
190
193
|
|
|
191
194
|
def _create_video_parser(self) -> VideoParser:
|
|
192
|
-
return VideoParser(syntax_registry=self._syntax_registry)
|
|
195
|
+
return VideoParser(syntax_registry=self._syntax_registry, file_upload_service=self._file_upload_service)
|
|
193
196
|
|
|
194
197
|
def _create_audio_parser(self) -> AudioParser:
|
|
195
|
-
return AudioParser(syntax_registry=self._syntax_registry)
|
|
198
|
+
return AudioParser(syntax_registry=self._syntax_registry, file_upload_service=self._file_upload_service)
|
|
196
199
|
|
|
197
200
|
def _create_file_parser(self) -> FileParser:
|
|
198
|
-
return FileParser(syntax_registry=self._syntax_registry)
|
|
201
|
+
return FileParser(syntax_registry=self._syntax_registry, file_upload_service=self._file_upload_service)
|
|
199
202
|
|
|
200
203
|
def _create_pdf_parser(self) -> PdfParser:
|
|
201
|
-
return PdfParser(syntax_registry=self._syntax_registry)
|
|
204
|
+
return PdfParser(syntax_registry=self._syntax_registry, file_upload_service=self._file_upload_service)
|
|
202
205
|
|
|
203
206
|
def _create_caption_parser(self) -> CaptionParser:
|
|
204
207
|
return CaptionParser(
|
|
@@ -1,40 +1,15 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
|
-
from notionary.blocks.schemas import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
FileData,
|
|
7
|
-
FileType,
|
|
8
|
-
)
|
|
9
|
-
from notionary.page.content.parser.parsers.base import BlockParsingContext, LineParser
|
|
10
|
-
from notionary.page.content.syntax import SyntaxRegistry
|
|
3
|
+
from notionary.blocks.schemas import CreateAudioBlock, ExternalFileWithCaption
|
|
4
|
+
from notionary.page.content.parser.parsers.file_like_block import FileLikeBlockParser
|
|
5
|
+
from notionary.page.content.syntax import SyntaxDefinition, SyntaxRegistry
|
|
11
6
|
|
|
12
7
|
|
|
13
|
-
class AudioParser(
|
|
14
|
-
def __init__(self, syntax_registry: SyntaxRegistry) -> None:
|
|
15
|
-
super().__init__(syntax_registry)
|
|
16
|
-
self._syntax = syntax_registry.get_audio_syntax()
|
|
17
|
-
|
|
8
|
+
class AudioParser(FileLikeBlockParser[CreateAudioBlock]):
|
|
18
9
|
@override
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
return False
|
|
22
|
-
return self._syntax.regex_pattern.search(context.line) is not None
|
|
10
|
+
def _get_syntax(self, syntax_registry: SyntaxRegistry) -> SyntaxDefinition:
|
|
11
|
+
return syntax_registry.get_audio_syntax()
|
|
23
12
|
|
|
24
13
|
@override
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if url is None:
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
-
audio_data = FileData(
|
|
31
|
-
type=FileType.EXTERNAL,
|
|
32
|
-
external=ExternalFile(url=url),
|
|
33
|
-
caption=[],
|
|
34
|
-
)
|
|
35
|
-
block = CreateAudioBlock(audio=audio_data)
|
|
36
|
-
context.result_blocks.append(block)
|
|
37
|
-
|
|
38
|
-
def _extract_url(self, line: str) -> str | None:
|
|
39
|
-
match = self._syntax.regex_pattern.search(line)
|
|
40
|
-
return match.group(1).strip() if match else None
|
|
14
|
+
def _create_block(self, file_data: ExternalFileWithCaption) -> CreateAudioBlock:
|
|
15
|
+
return CreateAudioBlock(audio=file_data)
|
|
@@ -1,42 +1,15 @@
|
|
|
1
|
-
"""Parser for file blocks."""
|
|
2
|
-
|
|
3
1
|
from typing import override
|
|
4
2
|
|
|
5
|
-
from notionary.blocks.schemas import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
FileData,
|
|
9
|
-
FileType,
|
|
10
|
-
)
|
|
11
|
-
from notionary.page.content.parser.parsers.base import BlockParsingContext, LineParser
|
|
12
|
-
from notionary.page.content.syntax import SyntaxRegistry
|
|
13
|
-
|
|
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 import SyntaxDefinition, SyntaxRegistry
|
|
14
6
|
|
|
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
7
|
|
|
8
|
+
class FileParser(FileLikeBlockParser[CreateFileBlock]):
|
|
20
9
|
@override
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
return False
|
|
24
|
-
return self._syntax.regex_pattern.search(context.line) is not None
|
|
10
|
+
def _get_syntax(self, syntax_registry: SyntaxRegistry) -> SyntaxDefinition:
|
|
11
|
+
return syntax_registry.get_file_syntax()
|
|
25
12
|
|
|
26
13
|
@override
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
14
|
+
def _create_block(self, file_data: ExternalFileWithCaption) -> CreateFileBlock:
|
|
15
|
+
return CreateFileBlock(file=file_data)
|