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.
- notionary/__init__.py +49 -1
- notionary/blocks/client.py +37 -11
- notionary/blocks/enums.py +0 -6
- notionary/blocks/rich_text/markdown_rich_text_converter.py +49 -15
- notionary/blocks/rich_text/models.py +13 -4
- notionary/blocks/rich_text/name_id_resolver/data_source.py +9 -3
- notionary/blocks/rich_text/name_id_resolver/person.py +6 -2
- notionary/blocks/rich_text/rich_text_markdown_converter.py +10 -3
- notionary/blocks/schemas.py +33 -78
- notionary/comments/client.py +19 -6
- notionary/comments/factory.py +10 -3
- notionary/comments/schemas.py +10 -31
- notionary/comments/service.py +12 -4
- notionary/data_source/http/data_source_instance_client.py +59 -17
- notionary/data_source/properties/schemas.py +156 -115
- notionary/data_source/query/builder.py +67 -18
- notionary/data_source/query/resolver.py +16 -5
- notionary/data_source/query/schema.py +24 -6
- notionary/data_source/query/validator.py +18 -6
- notionary/data_source/schema/registry.py +31 -12
- notionary/data_source/schema/service.py +66 -20
- notionary/data_source/schemas.py +2 -2
- notionary/data_source/service.py +103 -43
- notionary/database/client.py +27 -9
- notionary/database/database_metadata_update_client.py +12 -4
- notionary/database/schemas.py +2 -2
- notionary/database/service.py +14 -9
- notionary/exceptions/__init__.py +20 -4
- notionary/exceptions/api.py +2 -2
- notionary/exceptions/base.py +1 -1
- notionary/exceptions/block_parsing.py +9 -5
- notionary/exceptions/data_source/builder.py +13 -7
- notionary/exceptions/data_source/properties.py +6 -4
- notionary/exceptions/file_upload.py +76 -0
- notionary/exceptions/properties.py +7 -5
- notionary/exceptions/search.py +10 -6
- notionary/file_upload/__init__.py +4 -0
- notionary/file_upload/client.py +128 -210
- notionary/file_upload/config/__init__.py +17 -0
- notionary/file_upload/config/config.py +39 -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 +58 -0
- notionary/file_upload/query/models.py +37 -0
- notionary/file_upload/schemas.py +80 -0
- notionary/file_upload/service.py +182 -291
- notionary/file_upload/validation/factory.py +66 -0
- notionary/file_upload/validation/impl/file_name_length.py +25 -0
- notionary/file_upload/validation/models.py +134 -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 +131 -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 +33 -30
- notionary/page/content/__init__.py +9 -0
- notionary/page/content/factory.py +21 -7
- notionary/page/content/markdown/builder.py +85 -23
- notionary/page/content/markdown/nodes/audio.py +8 -4
- notionary/page/content/markdown/nodes/base.py +3 -3
- notionary/page/content/markdown/nodes/bookmark.py +5 -3
- notionary/page/content/markdown/nodes/breadcrumb.py +2 -2
- notionary/page/content/markdown/nodes/bulleted_list.py +5 -3
- notionary/page/content/markdown/nodes/callout.py +2 -2
- notionary/page/content/markdown/nodes/code.py +5 -3
- notionary/page/content/markdown/nodes/columns.py +3 -3
- notionary/page/content/markdown/nodes/container.py +9 -5
- notionary/page/content/markdown/nodes/divider.py +2 -2
- notionary/page/content/markdown/nodes/embed.py +8 -4
- notionary/page/content/markdown/nodes/equation.py +4 -2
- notionary/page/content/markdown/nodes/file.py +8 -4
- notionary/page/content/markdown/nodes/heading.py +2 -2
- notionary/page/content/markdown/nodes/image.py +8 -4
- notionary/page/content/markdown/nodes/mixins/caption.py +5 -3
- notionary/page/content/markdown/nodes/numbered_list.py +5 -3
- notionary/page/content/markdown/nodes/paragraph.py +4 -2
- notionary/page/content/markdown/nodes/pdf.py +8 -4
- notionary/page/content/markdown/nodes/quote.py +2 -2
- notionary/page/content/markdown/nodes/space.py +2 -2
- notionary/page/content/markdown/nodes/table.py +8 -5
- notionary/page/content/markdown/nodes/table_of_contents.py +2 -2
- notionary/page/content/markdown/nodes/todo.py +15 -7
- notionary/page/content/markdown/nodes/toggle.py +2 -2
- notionary/page/content/markdown/nodes/video.py +8 -4
- notionary/page/content/markdown/structured_output/__init__.py +73 -0
- notionary/page/content/markdown/structured_output/models.py +391 -0
- notionary/page/content/markdown/structured_output/service.py +211 -0
- notionary/page/content/parser/context.py +1 -1
- notionary/page/content/parser/factory.py +26 -8
- notionary/page/content/parser/parsers/audio.py +12 -32
- notionary/page/content/parser/parsers/base.py +2 -2
- notionary/page/content/parser/parsers/bookmark.py +2 -2
- notionary/page/content/parser/parsers/breadcrumb.py +2 -2
- notionary/page/content/parser/parsers/bulleted_list.py +19 -6
- notionary/page/content/parser/parsers/callout.py +15 -5
- notionary/page/content/parser/parsers/caption.py +9 -3
- notionary/page/content/parser/parsers/code.py +21 -7
- notionary/page/content/parser/parsers/column.py +8 -4
- notionary/page/content/parser/parsers/column_list.py +19 -7
- notionary/page/content/parser/parsers/divider.py +2 -2
- notionary/page/content/parser/parsers/embed.py +2 -4
- notionary/page/content/parser/parsers/equation.py +8 -4
- notionary/page/content/parser/parsers/file.py +12 -34
- notionary/page/content/parser/parsers/file_like_block.py +109 -0
- notionary/page/content/parser/parsers/heading.py +31 -10
- notionary/page/content/parser/parsers/image.py +12 -34
- notionary/page/content/parser/parsers/numbered_list.py +18 -6
- notionary/page/content/parser/parsers/paragraph.py +3 -1
- notionary/page/content/parser/parsers/pdf.py +12 -34
- notionary/page/content/parser/parsers/quote.py +28 -9
- notionary/page/content/parser/parsers/space.py +2 -2
- notionary/page/content/parser/parsers/table.py +31 -10
- notionary/page/content/parser/parsers/table_of_contents.py +7 -3
- notionary/page/content/parser/parsers/todo.py +15 -5
- notionary/page/content/parser/parsers/toggle.py +15 -5
- notionary/page/content/parser/parsers/video.py +12 -34
- notionary/page/content/parser/post_processing/handlers/rich_text_length.py +8 -2
- notionary/page/content/parser/post_processing/handlers/rich_text_length_truncation.py +8 -2
- notionary/page/content/parser/post_processing/service.py +3 -1
- notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +21 -7
- notionary/page/content/parser/pre_processsing/handlers/indentation.py +11 -4
- notionary/page/content/parser/pre_processsing/handlers/video_syntax.py +13 -6
- notionary/page/content/parser/service.py +4 -1
- notionary/page/content/renderer/context.py +15 -5
- notionary/page/content/renderer/factory.py +12 -6
- notionary/page/content/renderer/post_processing/handlers/numbered_list.py +19 -9
- notionary/page/content/renderer/renderers/audio.py +20 -23
- notionary/page/content/renderer/renderers/base.py +3 -3
- notionary/page/content/renderer/renderers/bookmark.py +3 -1
- notionary/page/content/renderer/renderers/bulleted_list.py +11 -5
- notionary/page/content/renderer/renderers/callout.py +19 -7
- notionary/page/content/renderer/renderers/captioned_block.py +11 -5
- notionary/page/content/renderer/renderers/code.py +6 -2
- notionary/page/content/renderer/renderers/column.py +3 -1
- notionary/page/content/renderer/renderers/column_list.py +3 -1
- notionary/page/content/renderer/renderers/embed.py +3 -1
- notionary/page/content/renderer/renderers/equation.py +3 -1
- notionary/page/content/renderer/renderers/file.py +20 -23
- notionary/page/content/renderer/renderers/file_like_block.py +47 -0
- notionary/page/content/renderer/renderers/heading.py +22 -8
- notionary/page/content/renderer/renderers/image.py +20 -23
- notionary/page/content/renderer/renderers/numbered_list.py +8 -3
- notionary/page/content/renderer/renderers/paragraph.py +12 -4
- notionary/page/content/renderer/renderers/pdf.py +20 -23
- notionary/page/content/renderer/renderers/quote.py +14 -6
- notionary/page/content/renderer/renderers/table.py +15 -5
- notionary/page/content/renderer/renderers/todo.py +16 -6
- notionary/page/content/renderer/renderers/toggle.py +8 -4
- notionary/page/content/renderer/renderers/video.py +20 -23
- notionary/page/content/renderer/service.py +9 -3
- notionary/page/content/service.py +21 -7
- notionary/page/content/syntax/definition/__init__.py +11 -0
- notionary/page/content/syntax/definition/models.py +57 -0
- notionary/page/content/syntax/definition/registry.py +371 -0
- notionary/page/content/syntax/prompts/__init__.py +4 -0
- notionary/page/content/syntax/prompts/models.py +11 -0
- notionary/page/content/syntax/prompts/registry.py +703 -0
- notionary/page/page_metadata_update_client.py +12 -4
- notionary/page/properties/client.py +46 -16
- notionary/page/properties/factory.py +6 -2
- notionary/page/properties/{models.py → schemas.py} +93 -107
- notionary/page/properties/service.py +111 -37
- notionary/page/schemas.py +3 -3
- notionary/page/service.py +21 -7
- notionary/shared/entity/client.py +6 -2
- notionary/shared/entity/dto_parsers.py +4 -37
- notionary/shared/entity/entity_metadata_update_client.py +25 -5
- notionary/shared/entity/schemas.py +6 -6
- notionary/shared/entity/service.py +89 -35
- notionary/shared/models/file.py +36 -6
- notionary/shared/models/icon.py +5 -12
- notionary/user/base.py +6 -2
- notionary/user/bot.py +22 -14
- notionary/user/client.py +3 -1
- notionary/user/person.py +3 -1
- notionary/user/schemas.py +3 -1
- notionary/user/service.py +6 -2
- notionary/utils/decorators.py +13 -9
- notionary/utils/fuzzy.py +6 -2
- notionary/utils/mixins/logging.py +3 -1
- notionary/utils/pagination.py +14 -4
- notionary/workspace/__init__.py +6 -2
- notionary/workspace/query/__init__.py +2 -1
- notionary/workspace/query/service.py +42 -13
- notionary/workspace/service.py +74 -46
- {notionary-0.3.1.dist-info → notionary-0.4.1.dist-info}/METADATA +1 -1
- notionary-0.4.1.dist-info/RECORD +236 -0
- notionary/file_upload/models.py +0 -69
- notionary/page/blocks/client.py +0 -1
- notionary/page/content/syntax/__init__.py +0 -4
- notionary/page/content/syntax/models.py +0 -66
- notionary/page/content/syntax/registry.py +0 -393
- notionary/page/page_context.py +0 -50
- notionary/shared/models/cover.py +0 -20
- notionary-0.3.1.dist-info/RECORD +0 -211
- /notionary/page/content/syntax/{grammar.py → definition/grammar.py} +0 -0
- {notionary-0.3.1.dist-info → notionary-0.4.1.dist-info}/WHEEL +0 -0
- {notionary-0.3.1.dist-info → notionary-0.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import random
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
3
|
from collections.abc import Sequence
|
|
4
|
-
from
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Self, cast
|
|
5
6
|
|
|
6
|
-
from notionary.
|
|
7
|
+
from notionary.file_upload.service import NotionFileUpload
|
|
8
|
+
from notionary.shared.entity.entity_metadata_update_client import (
|
|
9
|
+
EntityMetadataUpdateClient,
|
|
10
|
+
)
|
|
7
11
|
from notionary.shared.entity.schemas import EntityResponseDto
|
|
8
|
-
from notionary.shared.models.
|
|
9
|
-
from notionary.shared.models.icon import IconType
|
|
12
|
+
from notionary.shared.models.file import ExternalFile, FileType, NotionHostedFile
|
|
13
|
+
from notionary.shared.models.icon import EmojiIcon, IconType
|
|
10
14
|
from notionary.shared.models.parent import ParentType
|
|
11
15
|
from notionary.user.base import BaseUser
|
|
12
16
|
from notionary.user.service import UserService
|
|
@@ -19,6 +23,7 @@ class Entity(LoggingMixin, ABC):
|
|
|
19
23
|
self,
|
|
20
24
|
dto: EntityResponseDto,
|
|
21
25
|
user_service: UserService | None = None,
|
|
26
|
+
file_upload_service: NotionFileUpload | None = None,
|
|
22
27
|
) -> None:
|
|
23
28
|
self._id = dto.id
|
|
24
29
|
self._created_time = dto.created_time
|
|
@@ -35,6 +40,7 @@ class Entity(LoggingMixin, ABC):
|
|
|
35
40
|
self._cover_image_url = self._extract_cover_image_url(dto)
|
|
36
41
|
|
|
37
42
|
self._user_service = user_service or UserService()
|
|
43
|
+
self._file_upload_service = file_upload_service or NotionFileUpload()
|
|
38
44
|
|
|
39
45
|
@staticmethod
|
|
40
46
|
def _extract_emoji_icon(dto: EntityResponseDto) -> str | None:
|
|
@@ -43,25 +49,36 @@ class Entity(LoggingMixin, ABC):
|
|
|
43
49
|
if dto.icon.type is not IconType.EMOJI:
|
|
44
50
|
return None
|
|
45
51
|
|
|
46
|
-
|
|
52
|
+
emoji_icon = cast(EmojiIcon, dto.icon)
|
|
53
|
+
return emoji_icon.emoji
|
|
47
54
|
|
|
48
55
|
@staticmethod
|
|
49
56
|
def _extract_external_icon_url(dto: EntityResponseDto) -> str | None:
|
|
50
57
|
if dto.icon is None:
|
|
51
58
|
return None
|
|
52
|
-
if dto.icon.type is not IconType.EXTERNAL:
|
|
53
|
-
return None
|
|
54
59
|
|
|
55
|
-
|
|
60
|
+
if dto.icon.type == IconType.EXTERNAL:
|
|
61
|
+
external_icon = cast(ExternalFile, dto.icon)
|
|
62
|
+
return external_icon.external.url
|
|
63
|
+
elif dto.icon.type == IconType.FILE:
|
|
64
|
+
notion_file_icon = cast(NotionHostedFile, dto.icon)
|
|
65
|
+
return notion_file_icon.file.url
|
|
66
|
+
|
|
67
|
+
return None
|
|
56
68
|
|
|
57
69
|
@staticmethod
|
|
58
70
|
def _extract_cover_image_url(dto: EntityResponseDto) -> str | None:
|
|
59
71
|
if dto.cover is None:
|
|
60
72
|
return None
|
|
61
|
-
if dto.cover.type is not CoverType.EXTERNAL:
|
|
62
|
-
return None
|
|
63
73
|
|
|
64
|
-
|
|
74
|
+
if dto.cover.type == FileType.EXTERNAL:
|
|
75
|
+
external_cover = cast(ExternalFile, dto.cover)
|
|
76
|
+
return external_cover.external.url
|
|
77
|
+
elif dto.cover.type == FileType.FILE:
|
|
78
|
+
notion_file_cover = cast(NotionHostedFile, dto.cover)
|
|
79
|
+
return notion_file_cover.file.url
|
|
80
|
+
|
|
81
|
+
return None
|
|
65
82
|
|
|
66
83
|
@classmethod
|
|
67
84
|
@abstractmethod
|
|
@@ -82,10 +99,7 @@ class Entity(LoggingMixin, ABC):
|
|
|
82
99
|
|
|
83
100
|
@property
|
|
84
101
|
@abstractmethod
|
|
85
|
-
def _entity_metadata_update_client(self) -> EntityMetadataUpdateClient:
|
|
86
|
-
# functionality for updating properties like title, icon, cover, archive status depends on interface for template like implementation
|
|
87
|
-
# has to be implementated by inheritants to correctly use the methods below
|
|
88
|
-
...
|
|
102
|
+
def _entity_metadata_update_client(self) -> EntityMetadataUpdateClient: ...
|
|
89
103
|
|
|
90
104
|
@property
|
|
91
105
|
def id(self) -> str:
|
|
@@ -123,10 +137,6 @@ class Entity(LoggingMixin, ABC):
|
|
|
123
137
|
def public_url(self) -> str | None:
|
|
124
138
|
return self._public_url
|
|
125
139
|
|
|
126
|
-
# =========================================================================
|
|
127
|
-
# Parent ID Getters
|
|
128
|
-
# =========================================================================
|
|
129
|
-
|
|
130
140
|
def get_parent_database_id_if_present(self) -> str | None:
|
|
131
141
|
if self._parent.type == ParentType.DATABASE_ID:
|
|
132
142
|
return self._parent.database_id
|
|
@@ -150,27 +160,48 @@ class Entity(LoggingMixin, ABC):
|
|
|
150
160
|
def is_workspace_parent(self) -> bool:
|
|
151
161
|
return self._parent.type == ParentType.WORKSPACE
|
|
152
162
|
|
|
153
|
-
# =========================================================================
|
|
154
|
-
# User Methods
|
|
155
|
-
# =========================================================================
|
|
156
|
-
|
|
157
163
|
async def get_created_by_user(self) -> BaseUser | None:
|
|
158
164
|
return await self._user_service.get_user_by_id(self._created_by.id)
|
|
159
165
|
|
|
160
166
|
async def get_last_edited_by_user(self) -> BaseUser | None:
|
|
161
167
|
return await self._user_service.get_user_by_id(self._last_edited_by.id)
|
|
162
168
|
|
|
163
|
-
# =========================================================================
|
|
164
|
-
# Icon & Cover Methods
|
|
165
|
-
# =========================================================================
|
|
166
|
-
|
|
167
169
|
async def set_emoji_icon(self, emoji: str) -> None:
|
|
168
|
-
entity_response = await self._entity_metadata_update_client.patch_emoji_icon(
|
|
170
|
+
entity_response = await self._entity_metadata_update_client.patch_emoji_icon(
|
|
171
|
+
emoji
|
|
172
|
+
)
|
|
169
173
|
self._emoji_icon = self._extract_emoji_icon(entity_response)
|
|
170
174
|
self._external_icon_url = None
|
|
171
175
|
|
|
172
176
|
async def set_external_icon(self, icon_url: str) -> None:
|
|
173
|
-
entity_response = await self._entity_metadata_update_client.patch_external_icon(
|
|
177
|
+
entity_response = await self._entity_metadata_update_client.patch_external_icon(
|
|
178
|
+
icon_url
|
|
179
|
+
)
|
|
180
|
+
self._emoji_icon = None
|
|
181
|
+
self._external_icon_url = self._extract_external_icon_url(entity_response)
|
|
182
|
+
|
|
183
|
+
async def set_icon_from_file(
|
|
184
|
+
self, file_path: Path, filename: str | None = None
|
|
185
|
+
) -> None:
|
|
186
|
+
upload_response = await self._file_upload_service.upload_file(
|
|
187
|
+
file_path, filename
|
|
188
|
+
)
|
|
189
|
+
await self.set_icon_from_file_upload(upload_response.id)
|
|
190
|
+
|
|
191
|
+
async def set_icon_from_bytes(
|
|
192
|
+
self, file_content: bytes, filename: str, content_type: str | None = None
|
|
193
|
+
) -> None:
|
|
194
|
+
upload_response = await self._file_upload_service.upload_from_bytes(
|
|
195
|
+
file_content, filename, content_type
|
|
196
|
+
)
|
|
197
|
+
await self.set_icon_from_file_upload(upload_response.id)
|
|
198
|
+
|
|
199
|
+
async def set_icon_from_file_upload(self, file_upload_id: str) -> None:
|
|
200
|
+
entity_response = (
|
|
201
|
+
await self._entity_metadata_update_client.patch_icon_from_file_upload(
|
|
202
|
+
file_upload_id
|
|
203
|
+
)
|
|
204
|
+
)
|
|
174
205
|
self._emoji_icon = None
|
|
175
206
|
self._external_icon_url = self._extract_external_icon_url(entity_response)
|
|
176
207
|
|
|
@@ -180,7 +211,33 @@ class Entity(LoggingMixin, ABC):
|
|
|
180
211
|
self._external_icon_url = None
|
|
181
212
|
|
|
182
213
|
async def set_cover_image_by_url(self, image_url: str) -> None:
|
|
183
|
-
entity_response =
|
|
214
|
+
entity_response = (
|
|
215
|
+
await self._entity_metadata_update_client.patch_external_cover(image_url)
|
|
216
|
+
)
|
|
217
|
+
self._cover_image_url = self._extract_cover_image_url(entity_response)
|
|
218
|
+
|
|
219
|
+
async def set_cover_image_from_file(
|
|
220
|
+
self, file_path: Path, filename: str | None = None
|
|
221
|
+
) -> None:
|
|
222
|
+
upload_response = await self._file_upload_service.upload_file(
|
|
223
|
+
file_path, filename
|
|
224
|
+
)
|
|
225
|
+
await self.set_cover_image_from_file_upload(upload_response.id)
|
|
226
|
+
|
|
227
|
+
async def set_cover_image_from_bytes(
|
|
228
|
+
self, file_content: bytes, filename: str, content_type: str | None = None
|
|
229
|
+
) -> None:
|
|
230
|
+
upload_response = await self._file_upload_service.upload_from_bytes(
|
|
231
|
+
file_content, filename, content_type
|
|
232
|
+
)
|
|
233
|
+
await self.set_cover_image_from_file_upload(upload_response.id)
|
|
234
|
+
|
|
235
|
+
async def set_cover_image_from_file_upload(self, file_upload_id: str) -> None:
|
|
236
|
+
entity_response = (
|
|
237
|
+
await self._entity_metadata_update_client.patch_cover_from_file_upload(
|
|
238
|
+
file_upload_id
|
|
239
|
+
)
|
|
240
|
+
)
|
|
184
241
|
self._cover_image_url = self._extract_cover_image_url(entity_response)
|
|
185
242
|
|
|
186
243
|
async def set_random_gradient_cover(self) -> None:
|
|
@@ -191,10 +248,6 @@ class Entity(LoggingMixin, ABC):
|
|
|
191
248
|
await self._entity_metadata_update_client.remove_cover()
|
|
192
249
|
self._cover_image_url = None
|
|
193
250
|
|
|
194
|
-
# =========================================================================
|
|
195
|
-
# Trash Methods
|
|
196
|
-
# =========================================================================
|
|
197
|
-
|
|
198
251
|
async def move_to_trash(self) -> None:
|
|
199
252
|
if self._in_trash:
|
|
200
253
|
self.logger.warning("Entity is already in trash.")
|
|
@@ -223,7 +276,8 @@ class Entity(LoggingMixin, ABC):
|
|
|
223
276
|
|
|
224
277
|
def _get_random_gradient_cover(self) -> str:
|
|
225
278
|
DEFAULT_NOTION_COVERS: Sequence[str] = [
|
|
226
|
-
f"https://www.notion.so/images/page-cover/gradients_{i}.png"
|
|
279
|
+
f"https://www.notion.so/images/page-cover/gradients_{i}.png"
|
|
280
|
+
for i in range(1, 10)
|
|
227
281
|
]
|
|
228
282
|
|
|
229
283
|
return random.choice(DEFAULT_NOTION_COVERS)
|
notionary/shared/models/file.py
CHANGED
|
@@ -1,21 +1,51 @@
|
|
|
1
1
|
from enum import StrEnum
|
|
2
|
-
from typing import Literal, Self
|
|
2
|
+
from typing import Annotated, Literal, Self
|
|
3
3
|
|
|
4
|
-
from pydantic import BaseModel
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class FileType(StrEnum):
|
|
8
8
|
EXTERNAL = "external"
|
|
9
|
+
FILE = "file"
|
|
10
|
+
FILE_UPLOAD = "file_upload"
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
class
|
|
13
|
+
class ExternalFileData(BaseModel):
|
|
12
14
|
url: str
|
|
13
15
|
|
|
14
16
|
|
|
15
|
-
class
|
|
17
|
+
class ExternalFile(BaseModel):
|
|
16
18
|
type: Literal[FileType.EXTERNAL] = FileType.EXTERNAL
|
|
17
|
-
external:
|
|
19
|
+
external: ExternalFileData
|
|
18
20
|
|
|
19
21
|
@classmethod
|
|
20
22
|
def from_url(cls, url: str) -> Self:
|
|
21
|
-
return cls(external=
|
|
23
|
+
return cls(external=ExternalFileData(url=url))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NotionHostedFileData(BaseModel):
|
|
27
|
+
url: str
|
|
28
|
+
expiry_time: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NotionHostedFile(BaseModel):
|
|
32
|
+
type: Literal[FileType.FILE] = FileType.FILE
|
|
33
|
+
file: NotionHostedFileData
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FileUploadedFileData(BaseModel):
|
|
37
|
+
id: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FileUploadFile(BaseModel):
|
|
41
|
+
type: Literal[FileType.FILE_UPLOAD] = FileType.FILE_UPLOAD
|
|
42
|
+
file_upload: FileUploadedFileData
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_id(cls, id: str) -> Self:
|
|
46
|
+
return cls(file_upload=FileUploadedFileData(id=id))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
type File = Annotated[
|
|
50
|
+
ExternalFile | NotionHostedFile | FileUploadFile, Field(discriminator="type")
|
|
51
|
+
]
|
notionary/shared/models/icon.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
from enum import StrEnum
|
|
2
|
-
from typing import Literal
|
|
2
|
+
from typing import Literal
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel
|
|
5
5
|
|
|
6
|
-
from notionary.shared.models.file import
|
|
6
|
+
from notionary.shared.models.file import File
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class IconType(StrEnum):
|
|
10
10
|
EMOJI = "emoji"
|
|
11
11
|
EXTERNAL = "external"
|
|
12
|
+
FILE = "file"
|
|
13
|
+
FILE_UPLOAD = "file_upload"
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class EmojiIcon(BaseModel):
|
|
@@ -16,13 +18,4 @@ class EmojiIcon(BaseModel):
|
|
|
16
18
|
emoji: str
|
|
17
19
|
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
type: Literal[IconType.EXTERNAL] = IconType.EXTERNAL
|
|
21
|
-
external: ExternalFile
|
|
22
|
-
|
|
23
|
-
@classmethod
|
|
24
|
-
def from_url(cls, url: str) -> Self:
|
|
25
|
-
return cls(external=ExternalFile(url=url))
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
Icon = EmojiIcon | ExternalIcon
|
|
21
|
+
type Icon = EmojiIcon | File
|
notionary/user/base.py
CHANGED
|
@@ -54,7 +54,9 @@ class BaseUser:
|
|
|
54
54
|
|
|
55
55
|
expected_type = cls._get_expected_user_type()
|
|
56
56
|
if user_dto.type != expected_type:
|
|
57
|
-
raise ValueError(
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"User {user_id} is not a '{expected_type.value}', but '{user_dto.type.value}'"
|
|
59
|
+
)
|
|
58
60
|
|
|
59
61
|
return cls.from_dto(user_dto)
|
|
60
62
|
|
|
@@ -105,7 +107,9 @@ class BaseUser:
|
|
|
105
107
|
async def _get_all_users_of_type(cls, http_client: UserHttpClient) -> list[Self]:
|
|
106
108
|
all_workspace_user_dtos = await http_client.get_all_workspace_users()
|
|
107
109
|
expected_type = cls._get_expected_user_type()
|
|
108
|
-
filtered_dtos = [
|
|
110
|
+
filtered_dtos = [
|
|
111
|
+
dto for dto in all_workspace_user_dtos if dto.type == expected_type
|
|
112
|
+
]
|
|
109
113
|
return [cls.from_dto(dto) for dto in filtered_dtos]
|
|
110
114
|
|
|
111
115
|
@classmethod
|
notionary/user/bot.py
CHANGED
|
@@ -2,7 +2,13 @@ from typing import Self, cast
|
|
|
2
2
|
|
|
3
3
|
from notionary.user.base import BaseUser
|
|
4
4
|
from notionary.user.client import UserHttpClient
|
|
5
|
-
from notionary.user.schemas import
|
|
5
|
+
from notionary.user.schemas import (
|
|
6
|
+
BotUserDto,
|
|
7
|
+
BotUserResponseDto,
|
|
8
|
+
UserResponseDto,
|
|
9
|
+
UserType,
|
|
10
|
+
WorkspaceOwnerType,
|
|
11
|
+
)
|
|
6
12
|
|
|
7
13
|
|
|
8
14
|
class BotUser(BaseUser):
|
|
@@ -17,9 +23,23 @@ class BotUser(BaseUser):
|
|
|
17
23
|
) -> None:
|
|
18
24
|
super().__init__(id=id, name=name, avatar_url=avatar_url)
|
|
19
25
|
self._workspace_name = workspace_name
|
|
20
|
-
self._workspace_file_upload_limit_in_bytes =
|
|
26
|
+
self._workspace_file_upload_limit_in_bytes = (
|
|
27
|
+
workspace_file_upload_limit_in_bytes
|
|
28
|
+
)
|
|
21
29
|
self._owner_type = owner_type
|
|
22
30
|
|
|
31
|
+
@property
|
|
32
|
+
def workspace_name(self) -> str | None:
|
|
33
|
+
return self._workspace_name
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def workspace_file_upload_limit_in_bytes(self) -> int:
|
|
37
|
+
return self._workspace_file_upload_limit_in_bytes
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def owner_type(self) -> WorkspaceOwnerType | None:
|
|
41
|
+
return self._owner_type
|
|
42
|
+
|
|
23
43
|
@classmethod
|
|
24
44
|
def _get_expected_user_type(cls) -> UserType:
|
|
25
45
|
return UserType.BOT
|
|
@@ -54,17 +74,5 @@ class BotUser(BaseUser):
|
|
|
54
74
|
owner_type=owner_type,
|
|
55
75
|
)
|
|
56
76
|
|
|
57
|
-
@property
|
|
58
|
-
def workspace_name(self) -> str | None:
|
|
59
|
-
return self._workspace_name
|
|
60
|
-
|
|
61
|
-
@property
|
|
62
|
-
def workspace_file_upload_limit_in_bytes(self) -> int:
|
|
63
|
-
return self._workspace_file_upload_limit_in_bytes
|
|
64
|
-
|
|
65
|
-
@property
|
|
66
|
-
def owner_type(self) -> WorkspaceOwnerType | None:
|
|
67
|
-
return self._owner_type
|
|
68
|
-
|
|
69
77
|
def __repr__(self) -> str:
|
|
70
78
|
return f"BotUser(id={self._id!r}, name={self._name!r}, avatar_url={self._avatar_url!r}, workspace_name={self._workspace_name!r}, workspace_file_upload_limit_in_bytes={self._workspace_file_upload_limit_in_bytes!r}, owner_type={self._owner_type!r})"
|
notionary/user/client.py
CHANGED
|
@@ -17,7 +17,9 @@ class UserHttpClient(NotionHttpClient):
|
|
|
17
17
|
return adapter.validate_python(response)
|
|
18
18
|
|
|
19
19
|
async def get_all_workspace_users(self) -> list[UserResponseDto]:
|
|
20
|
-
all_entities = await paginate_notion_api(
|
|
20
|
+
all_entities = await paginate_notion_api(
|
|
21
|
+
self._get_workspace_entities, page_size=100
|
|
22
|
+
)
|
|
21
23
|
|
|
22
24
|
self.logger.info("Fetched %d total workspace users", len(all_entities))
|
|
23
25
|
return all_entities
|
notionary/user/person.py
CHANGED
notionary/user/schemas.py
CHANGED
|
@@ -53,7 +53,9 @@ class BotUserResponseDto(NotionUserBase):
|
|
|
53
53
|
bot: BotUserDto
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
UserResponseDto = Annotated[
|
|
56
|
+
UserResponseDto = Annotated[
|
|
57
|
+
PersonUserResponseDto | BotUserResponseDto, Field(discriminator="type")
|
|
58
|
+
]
|
|
57
59
|
|
|
58
60
|
|
|
59
61
|
class NotionUsersListResponse(BaseModel):
|
notionary/user/service.py
CHANGED
|
@@ -42,14 +42,18 @@ class UserService:
|
|
|
42
42
|
return [
|
|
43
43
|
user
|
|
44
44
|
for user in all_person_users
|
|
45
|
-
if query_lower in (user.name or "").lower()
|
|
45
|
+
if query_lower in (user.name or "").lower()
|
|
46
|
+
or query_lower in (user.email or "").lower()
|
|
46
47
|
]
|
|
47
48
|
|
|
48
49
|
async def search_users_stream(self, query: str) -> AsyncIterator[PersonUser]:
|
|
49
50
|
query_lower = query.lower()
|
|
50
51
|
|
|
51
52
|
async for user in self.list_users_stream():
|
|
52
|
-
if
|
|
53
|
+
if (
|
|
54
|
+
query_lower in (user.name or "").lower()
|
|
55
|
+
or query_lower in (user.email or "").lower()
|
|
56
|
+
):
|
|
53
57
|
yield user
|
|
54
58
|
|
|
55
59
|
async def get_current_bot(self) -> BotUser:
|
notionary/utils/decorators.py
CHANGED
|
@@ -9,10 +9,10 @@ P = ParamSpec("P")
|
|
|
9
9
|
R = TypeVar("R")
|
|
10
10
|
T = TypeVar("T")
|
|
11
11
|
|
|
12
|
-
type
|
|
13
|
-
type
|
|
14
|
-
type
|
|
15
|
-
type
|
|
12
|
+
type _SyncFunc = Callable[P, R]
|
|
13
|
+
type _AsyncFunc = Callable[P, Coroutine[Any, Any, R]]
|
|
14
|
+
type _SyncDecorator = Callable[[_SyncFunc], _SyncFunc]
|
|
15
|
+
type _AsyncDecorator = Callable[[_AsyncFunc], _AsyncFunc]
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def singleton(cls):
|
|
@@ -26,8 +26,10 @@ def singleton(cls):
|
|
|
26
26
|
return wrapper
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def time_execution_sync(
|
|
30
|
-
|
|
29
|
+
def time_execution_sync(
|
|
30
|
+
additional_text: str = "", min_duration_to_log: float = 0.25
|
|
31
|
+
) -> _SyncDecorator:
|
|
32
|
+
def decorator(func: _SyncFunc) -> _SyncFunc:
|
|
31
33
|
@functools.wraps(func)
|
|
32
34
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
33
35
|
start_time = time.perf_counter()
|
|
@@ -49,8 +51,8 @@ def time_execution_sync(additional_text: str = "", min_duration_to_log: float =
|
|
|
49
51
|
def time_execution_async(
|
|
50
52
|
additional_text: str = "",
|
|
51
53
|
min_duration_to_log: float = 0.25,
|
|
52
|
-
) ->
|
|
53
|
-
def decorator(func:
|
|
54
|
+
) -> _AsyncDecorator:
|
|
55
|
+
def decorator(func: _AsyncFunc) -> _AsyncFunc:
|
|
54
56
|
@functools.wraps(func)
|
|
55
57
|
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
56
58
|
start_time = time.perf_counter()
|
|
@@ -106,7 +108,9 @@ def async_retry(
|
|
|
106
108
|
except Exception as e:
|
|
107
109
|
last_exception = e
|
|
108
110
|
|
|
109
|
-
if retry_on_exceptions is not None and not isinstance(
|
|
111
|
+
if retry_on_exceptions is not None and not isinstance(
|
|
112
|
+
e, retry_on_exceptions
|
|
113
|
+
):
|
|
110
114
|
raise
|
|
111
115
|
|
|
112
116
|
if attempt == max_retries:
|
notionary/utils/fuzzy.py
CHANGED
|
@@ -28,7 +28,9 @@ def find_all_matches(
|
|
|
28
28
|
text_extractor: Callable[[T], str],
|
|
29
29
|
min_similarity: float,
|
|
30
30
|
) -> list[T]:
|
|
31
|
-
matches = _find_best_matches(
|
|
31
|
+
matches = _find_best_matches(
|
|
32
|
+
query, items, text_extractor, min_similarity, limit=None
|
|
33
|
+
)
|
|
32
34
|
return [match.item for match in matches]
|
|
33
35
|
|
|
34
36
|
|
|
@@ -56,7 +58,9 @@ def _find_best_matches(
|
|
|
56
58
|
return results
|
|
57
59
|
|
|
58
60
|
|
|
59
|
-
def _sort_by_highest_similarity_first(
|
|
61
|
+
def _sort_by_highest_similarity_first(
|
|
62
|
+
results: list[_MatchResult],
|
|
63
|
+
) -> list[_MatchResult]:
|
|
60
64
|
return sorted(results, key=lambda x: x.similarity, reverse=True)
|
|
61
65
|
|
|
62
66
|
|
|
@@ -19,7 +19,9 @@ def configure_library_logging(level: str = "WARNING") -> None:
|
|
|
19
19
|
library_logger.handlers.clear()
|
|
20
20
|
|
|
21
21
|
handler = logging.StreamHandler()
|
|
22
|
-
handler.setFormatter(
|
|
22
|
+
handler.setFormatter(
|
|
23
|
+
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
24
|
+
)
|
|
23
25
|
|
|
24
26
|
library_logger.setLevel(log_level)
|
|
25
27
|
library_logger.addHandler(handler)
|
notionary/utils/pagination.py
CHANGED
|
@@ -24,7 +24,9 @@ async def _fetch_data(
|
|
|
24
24
|
request_params = _build_request_params(kwargs, next_cursor)
|
|
25
25
|
response = await api_call(**request_params)
|
|
26
26
|
|
|
27
|
-
limited_results = _apply_result_limit(
|
|
27
|
+
limited_results = _apply_result_limit(
|
|
28
|
+
response.results, total_results_limit, total_fetched
|
|
29
|
+
)
|
|
28
30
|
total_fetched += len(limited_results)
|
|
29
31
|
|
|
30
32
|
yield _create_limited_response(response, limited_results, api_page_size)
|
|
@@ -52,7 +54,9 @@ def _build_request_params(
|
|
|
52
54
|
return params
|
|
53
55
|
|
|
54
56
|
|
|
55
|
-
def _apply_result_limit(
|
|
57
|
+
def _apply_result_limit(
|
|
58
|
+
results: list[Any], total_limit: int | None, total_fetched: int
|
|
59
|
+
) -> list[Any]:
|
|
56
60
|
if total_limit is None:
|
|
57
61
|
return results
|
|
58
62
|
|
|
@@ -74,7 +78,11 @@ def _create_limited_response(
|
|
|
74
78
|
results_were_limited_by_client = len(limited_results) < len(original.results)
|
|
75
79
|
api_returned_full_page = len(original.results) == api_page_size
|
|
76
80
|
|
|
77
|
-
has_more_after_limit =
|
|
81
|
+
has_more_after_limit = (
|
|
82
|
+
original.has_more
|
|
83
|
+
and not results_were_limited_by_client
|
|
84
|
+
and api_returned_full_page
|
|
85
|
+
)
|
|
78
86
|
|
|
79
87
|
return PaginatedResponse(
|
|
80
88
|
results=limited_results,
|
|
@@ -89,7 +97,9 @@ async def paginate_notion_api(
|
|
|
89
97
|
**kwargs,
|
|
90
98
|
) -> list[Any]:
|
|
91
99
|
all_results = []
|
|
92
|
-
async for page in _fetch_data(
|
|
100
|
+
async for page in _fetch_data(
|
|
101
|
+
api_call, total_results_limit=total_results_limit, **kwargs
|
|
102
|
+
):
|
|
93
103
|
all_results.extend(page.results)
|
|
94
104
|
return all_results
|
|
95
105
|
|
notionary/workspace/__init__.py
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
from .query import NotionWorkspaceQueryConfigBuilder
|
|
1
|
+
from .query import NotionWorkspaceQueryConfigBuilder, WorkspaceQueryConfig
|
|
2
2
|
from .service import NotionWorkspace
|
|
3
3
|
|
|
4
|
-
__all__ = [
|
|
4
|
+
__all__ = [
|
|
5
|
+
"NotionWorkspace",
|
|
6
|
+
"NotionWorkspaceQueryConfigBuilder",
|
|
7
|
+
"WorkspaceQueryConfig",
|
|
8
|
+
]
|