notionary 0.4.0__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 +44 -1
- notionary/blocks/client.py +37 -11
- 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 +2 -1
- notionary/comments/client.py +19 -6
- notionary/comments/factory.py +10 -3
- notionary/comments/schemas.py +9 -3
- notionary/comments/service.py +12 -4
- notionary/data_source/http/data_source_instance_client.py +59 -17
- notionary/data_source/properties/schemas.py +30 -10
- 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/service.py +74 -23
- notionary/database/client.py +27 -9
- notionary/database/database_metadata_update_client.py +12 -4
- notionary/database/service.py +11 -4
- notionary/exceptions/__init__.py +15 -3
- notionary/exceptions/block_parsing.py +6 -2
- notionary/exceptions/data_source/builder.py +11 -5
- notionary/exceptions/data_source/properties.py +3 -1
- notionary/exceptions/file_upload.py +12 -3
- notionary/exceptions/properties.py +3 -1
- notionary/exceptions/search.py +6 -2
- notionary/file_upload/client.py +5 -1
- notionary/file_upload/config/config.py +10 -3
- notionary/file_upload/query/builder.py +6 -2
- notionary/file_upload/schemas.py +3 -1
- notionary/file_upload/service.py +42 -14
- notionary/file_upload/validation/factory.py +3 -1
- notionary/file_upload/validation/impl/file_name_length.py +3 -1
- notionary/file_upload/validation/models.py +15 -5
- notionary/file_upload/validation/validators/file_extension.py +12 -3
- notionary/http/client.py +27 -8
- 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 +23 -8
- notionary/page/content/parser/parsers/audio.py +7 -2
- 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 -2
- notionary/page/content/parser/parsers/equation.py +8 -4
- notionary/page/content/parser/parsers/file.py +7 -2
- notionary/page/content/parser/parsers/file_like_block.py +30 -10
- notionary/page/content/parser/parsers/heading.py +31 -10
- notionary/page/content/parser/parsers/image.py +7 -2
- 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 +7 -2
- 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 +7 -2
- 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 +14 -5
- 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 +14 -5
- notionary/page/content/renderer/renderers/file_like_block.py +8 -4
- notionary/page/content/renderer/renderers/heading.py +22 -8
- notionary/page/content/renderer/renderers/image.py +13 -4
- 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 +14 -5
- 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 +14 -5
- 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 +45 -15
- notionary/page/properties/factory.py +6 -2
- notionary/page/properties/service.py +110 -36
- notionary/page/service.py +20 -6
- notionary/shared/entity/client.py +6 -2
- notionary/shared/entity/dto_parsers.py +3 -1
- notionary/shared/entity/entity_metadata_update_client.py +9 -3
- notionary/shared/entity/service.py +53 -22
- notionary/shared/models/file.py +3 -1
- notionary/user/base.py +6 -2
- notionary/user/bot.py +10 -2
- 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 +6 -2
- notionary/utils/fuzzy.py +6 -2
- notionary/utils/mixins/logging.py +3 -1
- notionary/utils/pagination.py +14 -4
- notionary/workspace/__init__.py +5 -1
- notionary/workspace/query/service.py +59 -16
- notionary/workspace/service.py +39 -11
- {notionary-0.4.0.dist-info → notionary-0.4.1.dist-info}/METADATA +1 -1
- notionary-0.4.1.dist-info/RECORD +236 -0
- notionary/page/blocks/client.py +0 -1
- notionary/page/content/syntax/__init__.py +0 -5
- notionary/page/content/syntax/models.py +0 -66
- notionary/page/content/syntax/registry.py +0 -371
- notionary-0.4.0.dist-info/RECORD +0 -230
- /notionary/page/content/syntax/{grammar.py → definition/grammar.py} +0 -0
- {notionary-0.4.0.dist-info → notionary-0.4.1.dist-info}/WHEEL +0 -0
- {notionary-0.4.0.dist-info → notionary-0.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,7 +5,9 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Self, cast
|
|
6
6
|
|
|
7
7
|
from notionary.file_upload.service import NotionFileUpload
|
|
8
|
-
from notionary.shared.entity.entity_metadata_update_client import
|
|
8
|
+
from notionary.shared.entity.entity_metadata_update_client import (
|
|
9
|
+
EntityMetadataUpdateClient,
|
|
10
|
+
)
|
|
9
11
|
from notionary.shared.entity.schemas import EntityResponseDto
|
|
10
12
|
from notionary.shared.models.file import ExternalFile, FileType, NotionHostedFile
|
|
11
13
|
from notionary.shared.models.icon import EmojiIcon, IconType
|
|
@@ -165,25 +167,41 @@ class Entity(LoggingMixin, ABC):
|
|
|
165
167
|
return await self._user_service.get_user_by_id(self._last_edited_by.id)
|
|
166
168
|
|
|
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
|
+
)
|
|
174
180
|
self._emoji_icon = None
|
|
175
181
|
self._external_icon_url = self._extract_external_icon_url(entity_response)
|
|
176
182
|
|
|
177
|
-
async def set_icon_from_file(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
await self.
|
|
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)
|
|
184
190
|
|
|
185
|
-
async def
|
|
186
|
-
|
|
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
|
+
)
|
|
187
205
|
self._emoji_icon = None
|
|
188
206
|
self._external_icon_url = self._extract_external_icon_url(entity_response)
|
|
189
207
|
|
|
@@ -193,21 +211,33 @@ class Entity(LoggingMixin, ABC):
|
|
|
193
211
|
self._external_icon_url = None
|
|
194
212
|
|
|
195
213
|
async def set_cover_image_by_url(self, image_url: str) -> None:
|
|
196
|
-
entity_response =
|
|
214
|
+
entity_response = (
|
|
215
|
+
await self._entity_metadata_update_client.patch_external_cover(image_url)
|
|
216
|
+
)
|
|
197
217
|
self._cover_image_url = self._extract_cover_image_url(entity_response)
|
|
198
218
|
|
|
199
|
-
async def set_cover_image_from_file(
|
|
200
|
-
|
|
201
|
-
|
|
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)
|
|
202
226
|
|
|
203
227
|
async def set_cover_image_from_bytes(
|
|
204
228
|
self, file_content: bytes, filename: str, content_type: str | None = None
|
|
205
229
|
) -> None:
|
|
206
|
-
upload_response = await self._file_upload_service.upload_from_bytes(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
+
)
|
|
211
241
|
self._cover_image_url = self._extract_cover_image_url(entity_response)
|
|
212
242
|
|
|
213
243
|
async def set_random_gradient_cover(self) -> None:
|
|
@@ -246,7 +276,8 @@ class Entity(LoggingMixin, ABC):
|
|
|
246
276
|
|
|
247
277
|
def _get_random_gradient_cover(self) -> str:
|
|
248
278
|
DEFAULT_NOTION_COVERS: Sequence[str] = [
|
|
249
|
-
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)
|
|
250
281
|
]
|
|
251
282
|
|
|
252
283
|
return random.choice(DEFAULT_NOTION_COVERS)
|
notionary/shared/models/file.py
CHANGED
|
@@ -46,4 +46,6 @@ class FileUploadFile(BaseModel):
|
|
|
46
46
|
return cls(file_upload=FileUploadedFileData(id=id))
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
type File = Annotated[
|
|
49
|
+
type File = Annotated[
|
|
50
|
+
ExternalFile | NotionHostedFile | FileUploadFile, Field(discriminator="type")
|
|
51
|
+
]
|
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,7 +23,9 @@ 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
|
|
|
23
31
|
@property
|
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
|
@@ -26,7 +26,9 @@ def singleton(cls):
|
|
|
26
26
|
return wrapper
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def time_execution_sync(
|
|
29
|
+
def time_execution_sync(
|
|
30
|
+
additional_text: str = "", min_duration_to_log: float = 0.25
|
|
31
|
+
) -> _SyncDecorator:
|
|
30
32
|
def decorator(func: _SyncFunc) -> _SyncFunc:
|
|
31
33
|
@functools.wraps(func)
|
|
32
34
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
@@ -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
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
|
+
]
|
|
@@ -4,7 +4,11 @@ import asyncio
|
|
|
4
4
|
from collections.abc import AsyncIterator
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
|
-
from notionary.exceptions.search import
|
|
7
|
+
from notionary.exceptions.search import (
|
|
8
|
+
DatabaseNotFound,
|
|
9
|
+
DataSourceNotFound,
|
|
10
|
+
PageNotFound,
|
|
11
|
+
)
|
|
8
12
|
from notionary.utils.fuzzy import find_all_matches
|
|
9
13
|
from notionary.workspace.client import WorkspaceClient
|
|
10
14
|
from notionary.workspace.query.builder import NotionWorkspaceQueryConfigBuilder
|
|
@@ -18,7 +22,9 @@ class WorkspaceQueryService:
|
|
|
18
22
|
def __init__(self, client: WorkspaceClient | None = None) -> None:
|
|
19
23
|
self._client = client or WorkspaceClient()
|
|
20
24
|
|
|
21
|
-
async def get_pages_stream(
|
|
25
|
+
async def get_pages_stream(
|
|
26
|
+
self, search_config: WorkspaceQueryConfig
|
|
27
|
+
) -> AsyncIterator[NotionPage]:
|
|
22
28
|
from notionary import NotionPage
|
|
23
29
|
|
|
24
30
|
async for page_dto in self._client.query_pages_stream(search_config):
|
|
@@ -27,48 +33,81 @@ class WorkspaceQueryService:
|
|
|
27
33
|
async def get_pages(self, search_config: WorkspaceQueryConfig) -> list[NotionPage]:
|
|
28
34
|
from notionary import NotionPage
|
|
29
35
|
|
|
30
|
-
page_dtos = [
|
|
36
|
+
page_dtos = [
|
|
37
|
+
dto async for dto in self._client.query_pages_stream(search_config)
|
|
38
|
+
]
|
|
31
39
|
page_tasks = [NotionPage.from_id(dto.id) for dto in page_dtos]
|
|
32
40
|
return await asyncio.gather(*page_tasks)
|
|
33
41
|
|
|
34
|
-
async def get_data_sources_stream(
|
|
42
|
+
async def get_data_sources_stream(
|
|
43
|
+
self, search_config: WorkspaceQueryConfig
|
|
44
|
+
) -> AsyncIterator[NotionDataSource]:
|
|
35
45
|
from notionary import NotionDataSource
|
|
36
46
|
|
|
37
|
-
async for data_source_dto in self._client.query_data_sources_stream(
|
|
47
|
+
async for data_source_dto in self._client.query_data_sources_stream(
|
|
48
|
+
search_config
|
|
49
|
+
):
|
|
38
50
|
yield await NotionDataSource.from_id(data_source_dto.id)
|
|
39
51
|
|
|
40
|
-
async def get_data_sources(
|
|
52
|
+
async def get_data_sources(
|
|
53
|
+
self, search_config: WorkspaceQueryConfig
|
|
54
|
+
) -> list[NotionDataSource]:
|
|
41
55
|
from notionary import NotionDataSource
|
|
42
56
|
|
|
43
|
-
data_source_dtos = [
|
|
44
|
-
|
|
57
|
+
data_source_dtos = [
|
|
58
|
+
dto async for dto in self._client.query_data_sources_stream(search_config)
|
|
59
|
+
]
|
|
60
|
+
data_source_tasks = [
|
|
61
|
+
NotionDataSource.from_id(dto.id) for dto in data_source_dtos
|
|
62
|
+
]
|
|
45
63
|
return await asyncio.gather(*data_source_tasks)
|
|
46
64
|
|
|
47
65
|
async def find_data_source(self, query: str) -> NotionDataSource:
|
|
48
66
|
config = (
|
|
49
|
-
NotionWorkspaceQueryConfigBuilder()
|
|
67
|
+
NotionWorkspaceQueryConfigBuilder()
|
|
68
|
+
.with_query(query)
|
|
69
|
+
.with_data_sources_only()
|
|
70
|
+
.with_page_size(100)
|
|
71
|
+
.build()
|
|
50
72
|
)
|
|
51
73
|
data_sources = await self.get_data_sources(config)
|
|
52
74
|
return self._find_exact_match(data_sources, query, DataSourceNotFound)
|
|
53
75
|
|
|
54
76
|
async def find_page(self, query: str) -> NotionPage:
|
|
55
|
-
config =
|
|
77
|
+
config = (
|
|
78
|
+
NotionWorkspaceQueryConfigBuilder()
|
|
79
|
+
.with_query(query)
|
|
80
|
+
.with_pages_only()
|
|
81
|
+
.with_page_size(100)
|
|
82
|
+
.build()
|
|
83
|
+
)
|
|
56
84
|
pages = await self.get_pages(config)
|
|
57
85
|
return self._find_exact_match(pages, query, PageNotFound)
|
|
58
86
|
|
|
59
87
|
async def find_database(self, query: str) -> NotionDatabase:
|
|
60
88
|
config = (
|
|
61
|
-
NotionWorkspaceQueryConfigBuilder()
|
|
89
|
+
NotionWorkspaceQueryConfigBuilder()
|
|
90
|
+
.with_query(query)
|
|
91
|
+
.with_data_sources_only()
|
|
92
|
+
.with_page_size(100)
|
|
93
|
+
.build()
|
|
62
94
|
)
|
|
63
95
|
data_sources = await self.get_data_sources(config)
|
|
64
96
|
|
|
65
|
-
parent_database_ids = [
|
|
97
|
+
parent_database_ids = [
|
|
98
|
+
data_sources.get_parent_database_id_if_present()
|
|
99
|
+
for data_sources in data_sources
|
|
100
|
+
]
|
|
66
101
|
# filter none values which should not happen but for safety
|
|
67
102
|
parent_database_ids = [id for id in parent_database_ids if id is not None]
|
|
68
103
|
|
|
69
|
-
parent_database_tasks = [
|
|
104
|
+
parent_database_tasks = [
|
|
105
|
+
NotionDatabase.from_id(db_id) for db_id in parent_database_ids
|
|
106
|
+
]
|
|
70
107
|
parent_databases = await asyncio.gather(*parent_database_tasks)
|
|
71
|
-
potential_databases = [
|
|
108
|
+
potential_databases = [
|
|
109
|
+
database for database in parent_databases if database is not None
|
|
110
|
+
]
|
|
72
111
|
|
|
73
112
|
return self._find_exact_match(potential_databases, query, DatabaseNotFound)
|
|
74
113
|
|
|
@@ -82,7 +121,9 @@ class WorkspaceQueryService:
|
|
|
82
121
|
raise exception_class(query, [])
|
|
83
122
|
|
|
84
123
|
query_lower = query.lower()
|
|
85
|
-
exact_matches = [
|
|
124
|
+
exact_matches = [
|
|
125
|
+
result for result in search_results if result.title.lower() == query_lower
|
|
126
|
+
]
|
|
86
127
|
|
|
87
128
|
if exact_matches:
|
|
88
129
|
return exact_matches[0]
|
|
@@ -90,7 +131,9 @@ class WorkspaceQueryService:
|
|
|
90
131
|
suggestions = self._get_fuzzy_suggestions(search_results, query)
|
|
91
132
|
raise exception_class(query, suggestions)
|
|
92
133
|
|
|
93
|
-
def _get_fuzzy_suggestions(
|
|
134
|
+
def _get_fuzzy_suggestions(
|
|
135
|
+
self, search_results: list[SearchableEntity], query: str
|
|
136
|
+
) -> list[str]:
|
|
94
137
|
sorted_by_similarity = find_all_matches(
|
|
95
138
|
query=query,
|
|
96
139
|
items=search_results,
|
notionary/workspace/service.py
CHANGED
|
@@ -5,7 +5,10 @@ from typing import TYPE_CHECKING, Self
|
|
|
5
5
|
|
|
6
6
|
from notionary.user.service import UserService
|
|
7
7
|
from notionary.workspace.query.builder import NotionWorkspaceQueryConfigBuilder
|
|
8
|
-
from notionary.workspace.query.models import
|
|
8
|
+
from notionary.workspace.query.models import (
|
|
9
|
+
WorkspaceQueryConfig,
|
|
10
|
+
WorkspaceQueryObjectType,
|
|
11
|
+
)
|
|
9
12
|
from notionary.workspace.query.service import WorkspaceQueryService
|
|
10
13
|
|
|
11
14
|
if TYPE_CHECKING:
|
|
@@ -39,56 +42,81 @@ class NotionWorkspace:
|
|
|
39
42
|
async def get_pages(
|
|
40
43
|
self,
|
|
41
44
|
*,
|
|
42
|
-
filter_fn: Callable[
|
|
45
|
+
filter_fn: Callable[
|
|
46
|
+
[NotionWorkspaceQueryConfigBuilder], NotionWorkspaceQueryConfigBuilder
|
|
47
|
+
]
|
|
48
|
+
| None = None,
|
|
43
49
|
query_config: WorkspaceQueryConfig | None = None,
|
|
44
50
|
) -> list[NotionPage]:
|
|
45
51
|
if filter_fn is not None and query_config is not None:
|
|
46
52
|
raise ValueError("Use either filter_fn OR query_config, not both")
|
|
47
53
|
|
|
48
|
-
resolved_config = self._resolve_query_config(
|
|
54
|
+
resolved_config = self._resolve_query_config(
|
|
55
|
+
filter_fn, query_config, WorkspaceQueryObjectType.PAGE
|
|
56
|
+
)
|
|
49
57
|
return await self._query_service.get_pages(resolved_config)
|
|
50
58
|
|
|
51
59
|
async def get_pages_stream(
|
|
52
60
|
self,
|
|
53
61
|
*,
|
|
54
|
-
filter_fn: Callable[
|
|
62
|
+
filter_fn: Callable[
|
|
63
|
+
[NotionWorkspaceQueryConfigBuilder], NotionWorkspaceQueryConfigBuilder
|
|
64
|
+
]
|
|
65
|
+
| None = None,
|
|
55
66
|
query_config: WorkspaceQueryConfig | None = None,
|
|
56
67
|
) -> AsyncIterator[NotionPage]:
|
|
57
68
|
if filter_fn is not None and query_config is not None:
|
|
58
69
|
raise ValueError("Use either filter_fn OR query_config, not both")
|
|
59
70
|
|
|
60
|
-
resolved_config = self._resolve_query_config(
|
|
71
|
+
resolved_config = self._resolve_query_config(
|
|
72
|
+
filter_fn, query_config, WorkspaceQueryObjectType.PAGE
|
|
73
|
+
)
|
|
61
74
|
async for page in self._query_service.get_pages_stream(resolved_config):
|
|
62
75
|
yield page
|
|
63
76
|
|
|
64
77
|
async def get_data_sources(
|
|
65
78
|
self,
|
|
66
79
|
*,
|
|
67
|
-
filter_fn: Callable[
|
|
80
|
+
filter_fn: Callable[
|
|
81
|
+
[NotionWorkspaceQueryConfigBuilder], NotionWorkspaceQueryConfigBuilder
|
|
82
|
+
]
|
|
83
|
+
| None = None,
|
|
68
84
|
query_config: WorkspaceQueryConfig | None = None,
|
|
69
85
|
) -> list[NotionDataSource]:
|
|
70
86
|
if filter_fn is not None and query_config is not None:
|
|
71
87
|
raise ValueError("Use either filter_fn OR query_config, not both")
|
|
72
88
|
|
|
73
|
-
resolved_config = self._resolve_query_config(
|
|
89
|
+
resolved_config = self._resolve_query_config(
|
|
90
|
+
filter_fn, query_config, WorkspaceQueryObjectType.DATA_SOURCE
|
|
91
|
+
)
|
|
74
92
|
return await self._query_service.get_data_sources(resolved_config)
|
|
75
93
|
|
|
76
94
|
async def get_data_sources_stream(
|
|
77
95
|
self,
|
|
78
96
|
*,
|
|
79
|
-
filter_fn: Callable[
|
|
97
|
+
filter_fn: Callable[
|
|
98
|
+
[NotionWorkspaceQueryConfigBuilder], NotionWorkspaceQueryConfigBuilder
|
|
99
|
+
]
|
|
100
|
+
| None = None,
|
|
80
101
|
query_config: WorkspaceQueryConfig | None = None,
|
|
81
102
|
) -> AsyncIterator[NotionDataSource]:
|
|
82
103
|
if filter_fn is not None and query_config is not None:
|
|
83
104
|
raise ValueError("Use either filter_fn OR query_config, not both")
|
|
84
105
|
|
|
85
|
-
resolved_config = self._resolve_query_config(
|
|
86
|
-
|
|
106
|
+
resolved_config = self._resolve_query_config(
|
|
107
|
+
filter_fn, query_config, WorkspaceQueryObjectType.DATA_SOURCE
|
|
108
|
+
)
|
|
109
|
+
async for data_source in self._query_service.get_data_sources_stream(
|
|
110
|
+
resolved_config
|
|
111
|
+
):
|
|
87
112
|
yield data_source
|
|
88
113
|
|
|
89
114
|
def _resolve_query_config(
|
|
90
115
|
self,
|
|
91
|
-
filter_fn: Callable[
|
|
116
|
+
filter_fn: Callable[
|
|
117
|
+
[NotionWorkspaceQueryConfigBuilder], NotionWorkspaceQueryConfigBuilder
|
|
118
|
+
]
|
|
119
|
+
| None,
|
|
92
120
|
query_config: WorkspaceQueryConfig | None,
|
|
93
121
|
expected_object_type: WorkspaceQueryObjectType,
|
|
94
122
|
) -> WorkspaceQueryConfig:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: notionary
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Python library for programmatic Notion workspace management - databases, pages, and content with advanced Markdown support
|
|
5
5
|
Project-URL: Homepage, https://github.com/mathisarends/notionary
|
|
6
6
|
Author-email: Mathis Arends <mathisarends27@gmail.com>
|