notionary 0.3.0__py3-none-any.whl → 0.3.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 +9 -2
- notionary/blocks/enums.py +27 -0
- notionary/comments/client.py +6 -9
- notionary/data_source/http/data_source_instance_client.py +4 -4
- 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/service.py +14 -112
- notionary/database/service.py +16 -58
- notionary/exceptions/__init__.py +4 -0
- notionary/exceptions/block_parsing.py +21 -0
- notionary/http/client.py +1 -1
- notionary/page/content/factory.py +2 -0
- 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/properties/client.py +2 -2
- notionary/page/properties/service.py +14 -3
- notionary/page/service.py +17 -78
- notionary/shared/entity/service.py +94 -36
- notionary/utils/pagination.py +36 -32
- notionary/workspace/__init__.py +2 -2
- notionary/workspace/client.py +2 -0
- notionary/workspace/query/__init__.py +2 -2
- notionary/workspace/query/builder.py +25 -1
- notionary/workspace/query/models.py +9 -1
- notionary/workspace/query/service.py +29 -11
- notionary/workspace/service.py +31 -21
- {notionary-0.3.0.dist-info → notionary-0.3.1.dist-info}/METADATA +9 -5
- {notionary-0.3.0.dist-info → notionary-0.3.1.dist-info}/RECORD +34 -32
- {notionary-0.3.0.dist-info → notionary-0.3.1.dist-info}/WHEEL +0 -0
- {notionary-0.3.0.dist-info → notionary-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import override
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
from notionary.blocks.enums import VideoFileType
|
|
6
|
+
from notionary.exceptions import UnsupportedVideoFormatError
|
|
7
|
+
from notionary.page.content.parser.pre_processsing.handlers.port import PreProcessor
|
|
8
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
9
|
+
from notionary.utils.decorators import time_execution_sync
|
|
10
|
+
from notionary.utils.mixins.logging import LoggingMixin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VideoFormatPreProcessor(PreProcessor, LoggingMixin):
|
|
14
|
+
YOUTUBE_WATCH_PATTERN = re.compile(r"^https?://(?:www\.)?youtube\.com/watch\?.*v=[\w-]+", re.IGNORECASE)
|
|
15
|
+
YOUTUBE_EMBED_PATTERN = re.compile(r"^https?://(?:www\.)?youtube\.com/embed/[\w-]+", re.IGNORECASE)
|
|
16
|
+
|
|
17
|
+
def __init__(self, syntax_registry: SyntaxRegistry | None = None) -> None:
|
|
18
|
+
super().__init__()
|
|
19
|
+
self._syntax_registry = syntax_registry or SyntaxRegistry()
|
|
20
|
+
self._video_syntax = self._syntax_registry.get_video_syntax()
|
|
21
|
+
|
|
22
|
+
@override
|
|
23
|
+
@time_execution_sync()
|
|
24
|
+
def process(self, markdown_text: str) -> str:
|
|
25
|
+
lines = markdown_text.split("\n")
|
|
26
|
+
validated_lines = [self._validate_or_reject_line(line) for line in lines]
|
|
27
|
+
return "\n".join(validated_lines)
|
|
28
|
+
|
|
29
|
+
def _validate_or_reject_line(self, line: str) -> str:
|
|
30
|
+
if not self._contains_video_block(line):
|
|
31
|
+
return line
|
|
32
|
+
|
|
33
|
+
url = self._extract_url_from_video_block(line)
|
|
34
|
+
|
|
35
|
+
if self._is_supported_video_url(url):
|
|
36
|
+
return line
|
|
37
|
+
|
|
38
|
+
supported_formats = list(VideoFileType.get_all_extensions())
|
|
39
|
+
raise UnsupportedVideoFormatError(url, supported_formats)
|
|
40
|
+
|
|
41
|
+
def _contains_video_block(self, line: str) -> bool:
|
|
42
|
+
return self._video_syntax.regex_pattern.search(line) is not None
|
|
43
|
+
|
|
44
|
+
def _extract_url_from_video_block(self, line: str) -> str:
|
|
45
|
+
match = self._video_syntax.regex_pattern.search(line)
|
|
46
|
+
return match.group(1).strip() if match else ""
|
|
47
|
+
|
|
48
|
+
def _is_supported_video_url(self, url: str) -> bool:
|
|
49
|
+
return (
|
|
50
|
+
self._is_youtube_video(url)
|
|
51
|
+
or self._has_valid_video_extension(url)
|
|
52
|
+
or self._url_path_has_valid_extension(url)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def _is_youtube_video(self, url: str) -> bool:
|
|
56
|
+
return bool(self.YOUTUBE_WATCH_PATTERN.match(url) or self.YOUTUBE_EMBED_PATTERN.match(url))
|
|
57
|
+
|
|
58
|
+
def _has_valid_video_extension(self, url: str) -> bool:
|
|
59
|
+
return VideoFileType.is_valid_extension(url)
|
|
60
|
+
|
|
61
|
+
def _url_path_has_valid_extension(self, url: str) -> bool:
|
|
62
|
+
try:
|
|
63
|
+
parsed_url = urlparse(url)
|
|
64
|
+
return VideoFileType.is_valid_extension(parsed_url.path.lower())
|
|
65
|
+
except Exception:
|
|
66
|
+
return False
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
3
|
from notionary.page.content.parser.pre_processsing.handlers.port import PreProcessor
|
|
4
|
+
from notionary.utils.decorators import time_execution_sync
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class WhitespacePreProcessor(PreProcessor):
|
|
7
8
|
@override
|
|
9
|
+
@time_execution_sync()
|
|
8
10
|
def process(self, markdown_text: str) -> str:
|
|
9
11
|
if not markdown_text:
|
|
10
12
|
return ""
|
|
@@ -51,8 +51,8 @@ class PagePropertyHttpClient(NotionHttpClient):
|
|
|
51
51
|
|
|
52
52
|
return await self.patch_page(update_dto)
|
|
53
53
|
|
|
54
|
-
async def patch_title(self, title: str) -> NotionPageDto:
|
|
55
|
-
return await self._patch_property(
|
|
54
|
+
async def patch_title(self, property_name: str, title: str) -> NotionPageDto:
|
|
55
|
+
return await self._patch_property(property_name, title, PageTitleProperty)
|
|
56
56
|
|
|
57
57
|
async def patch_rich_text_property(self, property_name: str, text: str) -> NotionPageDto:
|
|
58
58
|
return await self._patch_property(property_name, text, PageRichTextProperty)
|
|
@@ -147,11 +147,22 @@ class PagePropertyHandler:
|
|
|
147
147
|
# Writer Methods
|
|
148
148
|
# =========================================================================
|
|
149
149
|
|
|
150
|
-
async def set_title_property(self,
|
|
151
|
-
self.
|
|
152
|
-
|
|
150
|
+
async def set_title_property(self, title: str) -> None:
|
|
151
|
+
title_property_name = self._extract_title_property_name()
|
|
152
|
+
|
|
153
|
+
self._get_typed_property_or_raise(title_property_name, PageTitleProperty)
|
|
154
|
+
updated_page = await self._property_http_client.patch_title(title_property_name, title)
|
|
153
155
|
self._properties = updated_page.properties
|
|
154
156
|
|
|
157
|
+
def _extract_title_property_name(self) -> str | None:
|
|
158
|
+
if not self._properties:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
return next(
|
|
162
|
+
(key for key, prop in self._properties.items() if isinstance(prop, PageTitleProperty)),
|
|
163
|
+
None,
|
|
164
|
+
)
|
|
165
|
+
|
|
155
166
|
async def set_rich_text_property(self, property_name: str, text: str) -> None:
|
|
156
167
|
self._get_typed_property_or_raise(property_name, PageRichTextProperty)
|
|
157
168
|
updated_page = await self._property_http_client.patch_rich_text_property(property_name, text)
|
notionary/page/service.py
CHANGED
|
@@ -14,59 +14,31 @@ from notionary.page.properties.factory import PagePropertyHandlerFactory
|
|
|
14
14
|
from notionary.page.properties.models import PageTitleProperty
|
|
15
15
|
from notionary.page.properties.service import PagePropertyHandler
|
|
16
16
|
from notionary.page.schemas import NotionPageDto
|
|
17
|
-
from notionary.shared.entity.dto_parsers import (
|
|
18
|
-
extract_cover_image_url_from_dto,
|
|
19
|
-
extract_emoji_icon_from_dto,
|
|
20
|
-
extract_external_icon_url_from_dto,
|
|
21
|
-
)
|
|
22
17
|
from notionary.shared.entity.service import Entity
|
|
23
|
-
from notionary.user.schemas import PartialUserDto
|
|
24
18
|
from notionary.workspace.query.service import WorkspaceQueryService
|
|
25
19
|
|
|
26
20
|
|
|
27
21
|
class NotionPage(Entity):
|
|
28
22
|
def __init__(
|
|
29
23
|
self,
|
|
30
|
-
|
|
24
|
+
dto: NotionPageDto,
|
|
31
25
|
title: str,
|
|
32
|
-
created_time: str,
|
|
33
|
-
created_by: PartialUserDto,
|
|
34
|
-
last_edited_time: str,
|
|
35
|
-
last_edited_by: PartialUserDto,
|
|
36
|
-
url: str,
|
|
37
|
-
archived: bool,
|
|
38
|
-
in_trash: bool,
|
|
39
26
|
page_property_handler: PagePropertyHandler,
|
|
40
27
|
block_client: NotionBlockHttpClient,
|
|
41
28
|
comment_service: CommentService,
|
|
42
29
|
page_content_service: PageContentService,
|
|
43
30
|
metadata_update_client: PageMetadataUpdateClient,
|
|
44
|
-
public_url: str | None = None,
|
|
45
|
-
emoji_icon: str | None = None,
|
|
46
|
-
external_icon_url: str | None = None,
|
|
47
|
-
cover_image_url: str | None = None,
|
|
48
31
|
) -> None:
|
|
49
|
-
super().__init__(
|
|
50
|
-
|
|
51
|
-
created_time=created_time,
|
|
52
|
-
created_by=created_by,
|
|
53
|
-
last_edited_time=last_edited_time,
|
|
54
|
-
last_edited_by=last_edited_by,
|
|
55
|
-
in_trash=in_trash,
|
|
56
|
-
emoji_icon=emoji_icon,
|
|
57
|
-
external_icon_url=external_icon_url,
|
|
58
|
-
cover_image_url=cover_image_url,
|
|
59
|
-
)
|
|
32
|
+
super().__init__(dto=dto)
|
|
33
|
+
|
|
60
34
|
self._title = title
|
|
61
|
-
self._archived = archived
|
|
62
|
-
self._url = url
|
|
63
|
-
self._public_url = public_url
|
|
35
|
+
self._archived = dto.archived
|
|
64
36
|
|
|
65
37
|
self._block_client = block_client
|
|
66
38
|
self._comment_service = comment_service
|
|
67
39
|
self._page_content_service = page_content_service
|
|
68
|
-
self.properties = page_property_handler
|
|
69
40
|
self._metadata_update_client = metadata_update_client
|
|
41
|
+
self.properties = page_property_handler
|
|
70
42
|
|
|
71
43
|
@classmethod
|
|
72
44
|
async def from_id(
|
|
@@ -75,8 +47,8 @@ class NotionPage(Entity):
|
|
|
75
47
|
page_property_handler_factory: PagePropertyHandlerFactory | None = None,
|
|
76
48
|
) -> Self:
|
|
77
49
|
factory = page_property_handler_factory or PagePropertyHandlerFactory()
|
|
78
|
-
|
|
79
|
-
return await cls._create_from_dto(
|
|
50
|
+
dto = await cls._fetch_page_dto(page_id)
|
|
51
|
+
return await cls._create_from_dto(dto, factory)
|
|
80
52
|
|
|
81
53
|
@classmethod
|
|
82
54
|
async def from_title(
|
|
@@ -95,76 +67,43 @@ class NotionPage(Entity):
|
|
|
95
67
|
@classmethod
|
|
96
68
|
async def _create_from_dto(
|
|
97
69
|
cls,
|
|
98
|
-
|
|
70
|
+
dto: NotionPageDto,
|
|
99
71
|
page_property_handler_factory: PagePropertyHandlerFactory,
|
|
100
72
|
) -> Self:
|
|
101
|
-
title_task = cls._extract_title_from_dto(
|
|
102
|
-
page_property_handler = page_property_handler_factory.create_from_page_response(
|
|
73
|
+
title_task = cls._extract_title_from_dto(dto)
|
|
74
|
+
page_property_handler = page_property_handler_factory.create_from_page_response(dto)
|
|
103
75
|
|
|
104
76
|
title = await title_task
|
|
105
77
|
|
|
106
78
|
return cls._create_with_dependencies(
|
|
107
|
-
|
|
79
|
+
dto=dto,
|
|
108
80
|
title=title,
|
|
109
|
-
created_time=response.created_time,
|
|
110
|
-
created_by=response.created_by,
|
|
111
|
-
last_edited_time=response.last_edited_time,
|
|
112
|
-
last_edited_by=response.last_edited_by,
|
|
113
|
-
archived=response.archived,
|
|
114
|
-
in_trash=response.in_trash,
|
|
115
|
-
url=response.url,
|
|
116
81
|
page_property_handler=page_property_handler,
|
|
117
|
-
public_url=response.public_url,
|
|
118
|
-
emoji_icon=extract_emoji_icon_from_dto(response),
|
|
119
|
-
external_icon_url=extract_external_icon_url_from_dto(response),
|
|
120
|
-
cover_image_url=extract_cover_image_url_from_dto(response),
|
|
121
82
|
)
|
|
122
83
|
|
|
123
84
|
@classmethod
|
|
124
85
|
def _create_with_dependencies(
|
|
125
86
|
cls,
|
|
126
|
-
|
|
87
|
+
dto: NotionPageDto,
|
|
127
88
|
title: str,
|
|
128
|
-
created_time: str,
|
|
129
|
-
created_by: PartialUserDto,
|
|
130
|
-
last_edited_time: str,
|
|
131
|
-
last_edited_by: PartialUserDto,
|
|
132
|
-
url: str,
|
|
133
|
-
archived: bool,
|
|
134
|
-
in_trash: bool,
|
|
135
89
|
page_property_handler: PagePropertyHandler,
|
|
136
|
-
public_url: str | None = None,
|
|
137
|
-
emoji_icon: str | None = None,
|
|
138
|
-
external_icon_url: str | None = None,
|
|
139
|
-
cover_image_url: str | None = None,
|
|
140
90
|
) -> Self:
|
|
141
91
|
block_client = NotionBlockHttpClient()
|
|
142
92
|
comment_service = CommentService()
|
|
143
93
|
|
|
144
94
|
page_content_service_factory = PageContentServiceFactory()
|
|
145
|
-
page_content_service = page_content_service_factory.create(page_id=id, block_client=block_client)
|
|
95
|
+
page_content_service = page_content_service_factory.create(page_id=dto.id, block_client=block_client)
|
|
146
96
|
|
|
147
|
-
metadata_update_client = PageMetadataUpdateClient(page_id=id)
|
|
97
|
+
metadata_update_client = PageMetadataUpdateClient(page_id=dto.id)
|
|
148
98
|
|
|
149
99
|
return cls(
|
|
150
|
-
|
|
100
|
+
dto=dto,
|
|
151
101
|
title=title,
|
|
152
|
-
created_time=created_time,
|
|
153
|
-
created_by=created_by,
|
|
154
|
-
last_edited_time=last_edited_time,
|
|
155
|
-
last_edited_by=last_edited_by,
|
|
156
|
-
url=url,
|
|
157
|
-
archived=archived,
|
|
158
|
-
in_trash=in_trash,
|
|
159
102
|
page_property_handler=page_property_handler,
|
|
160
103
|
block_client=block_client,
|
|
161
104
|
comment_service=comment_service,
|
|
162
105
|
page_content_service=page_content_service,
|
|
163
106
|
metadata_update_client=metadata_update_client,
|
|
164
|
-
public_url=public_url,
|
|
165
|
-
emoji_icon=emoji_icon,
|
|
166
|
-
external_icon_url=external_icon_url,
|
|
167
|
-
cover_image_url=cover_image_url,
|
|
168
107
|
)
|
|
169
108
|
|
|
170
109
|
@staticmethod
|
|
@@ -185,8 +124,8 @@ class NotionPage(Entity):
|
|
|
185
124
|
return self._title
|
|
186
125
|
|
|
187
126
|
@property
|
|
188
|
-
def
|
|
189
|
-
return self.
|
|
127
|
+
def archived(self) -> bool:
|
|
128
|
+
return self._archived
|
|
190
129
|
|
|
191
130
|
@property
|
|
192
131
|
def markdown_builder() -> MarkdownBuilder:
|
|
@@ -1,45 +1,68 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import random
|
|
4
2
|
from abc import ABC, abstractmethod
|
|
5
3
|
from collections.abc import Sequence
|
|
6
|
-
from typing import
|
|
4
|
+
from typing import Self
|
|
7
5
|
|
|
8
6
|
from notionary.shared.entity.entity_metadata_update_client import EntityMetadataUpdateClient
|
|
9
|
-
from notionary.
|
|
7
|
+
from notionary.shared.entity.schemas import EntityResponseDto
|
|
8
|
+
from notionary.shared.models.cover import CoverType
|
|
9
|
+
from notionary.shared.models.icon import IconType
|
|
10
|
+
from notionary.shared.models.parent import ParentType
|
|
11
|
+
from notionary.user.base import BaseUser
|
|
10
12
|
from notionary.user.service import UserService
|
|
11
13
|
from notionary.utils.mixins.logging import LoggingMixin
|
|
12
14
|
from notionary.utils.uuid_utils import extract_uuid
|
|
13
15
|
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from notionary.user.base import BaseUser
|
|
16
|
-
|
|
17
16
|
|
|
18
17
|
class Entity(LoggingMixin, ABC):
|
|
19
18
|
def __init__(
|
|
20
19
|
self,
|
|
21
|
-
|
|
22
|
-
created_time: str,
|
|
23
|
-
created_by: PartialUserDto,
|
|
24
|
-
last_edited_time: str,
|
|
25
|
-
last_edited_by: PartialUserDto,
|
|
26
|
-
in_trash: bool,
|
|
27
|
-
emoji_icon: str | None = None,
|
|
28
|
-
external_icon_url: str | None = None,
|
|
29
|
-
cover_image_url: str | None = None,
|
|
20
|
+
dto: EntityResponseDto,
|
|
30
21
|
user_service: UserService | None = None,
|
|
31
22
|
) -> None:
|
|
32
|
-
self._id = id
|
|
33
|
-
self._created_time = created_time
|
|
34
|
-
self._created_by = created_by
|
|
35
|
-
self._last_edited_time = last_edited_time
|
|
36
|
-
self._last_edited_by = last_edited_by
|
|
37
|
-
self.
|
|
38
|
-
self.
|
|
39
|
-
self.
|
|
40
|
-
self.
|
|
23
|
+
self._id = dto.id
|
|
24
|
+
self._created_time = dto.created_time
|
|
25
|
+
self._created_by = dto.created_by
|
|
26
|
+
self._last_edited_time = dto.last_edited_time
|
|
27
|
+
self._last_edited_by = dto.last_edited_by
|
|
28
|
+
self._in_trash = dto.in_trash
|
|
29
|
+
self._parent = dto.parent
|
|
30
|
+
self._url = dto.url
|
|
31
|
+
self._public_url = dto.public_url
|
|
32
|
+
|
|
33
|
+
self._emoji_icon = self._extract_emoji_icon(dto)
|
|
34
|
+
self._external_icon_url = self._extract_external_icon_url(dto)
|
|
35
|
+
self._cover_image_url = self._extract_cover_image_url(dto)
|
|
36
|
+
|
|
41
37
|
self._user_service = user_service or UserService()
|
|
42
38
|
|
|
39
|
+
@staticmethod
|
|
40
|
+
def _extract_emoji_icon(dto: EntityResponseDto) -> str | None:
|
|
41
|
+
if dto.icon is None:
|
|
42
|
+
return None
|
|
43
|
+
if dto.icon.type is not IconType.EMOJI:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
return dto.icon.emoji
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def _extract_external_icon_url(dto: EntityResponseDto) -> str | None:
|
|
50
|
+
if dto.icon is None:
|
|
51
|
+
return None
|
|
52
|
+
if dto.icon.type is not IconType.EXTERNAL:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
return dto.icon.external.url
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _extract_cover_image_url(dto: EntityResponseDto) -> str | None:
|
|
59
|
+
if dto.cover is None:
|
|
60
|
+
return None
|
|
61
|
+
if dto.cover.type is not CoverType.EXTERNAL:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
return dto.cover.external.url
|
|
65
|
+
|
|
43
66
|
@classmethod
|
|
44
67
|
@abstractmethod
|
|
45
68
|
async def from_id(cls, id: str) -> Self:
|
|
@@ -93,12 +116,43 @@ class Entity(LoggingMixin, ABC):
|
|
|
93
116
|
return self._cover_image_url
|
|
94
117
|
|
|
95
118
|
@property
|
|
96
|
-
def
|
|
97
|
-
return self.
|
|
119
|
+
def url(self) -> str:
|
|
120
|
+
return self._url
|
|
98
121
|
|
|
99
122
|
@property
|
|
100
|
-
def
|
|
101
|
-
return self.
|
|
123
|
+
def public_url(self) -> str | None:
|
|
124
|
+
return self._public_url
|
|
125
|
+
|
|
126
|
+
# =========================================================================
|
|
127
|
+
# Parent ID Getters
|
|
128
|
+
# =========================================================================
|
|
129
|
+
|
|
130
|
+
def get_parent_database_id_if_present(self) -> str | None:
|
|
131
|
+
if self._parent.type == ParentType.DATABASE_ID:
|
|
132
|
+
return self._parent.database_id
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
def get_parent_data_source_id_if_present(self) -> str | None:
|
|
136
|
+
if self._parent.type == ParentType.DATA_SOURCE_ID:
|
|
137
|
+
return self._parent.data_source_id
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
def get_parent_page_id_if_present(self) -> str | None:
|
|
141
|
+
if self._parent.type == ParentType.PAGE_ID:
|
|
142
|
+
return self._parent.page_id
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
def get_parent_block_id_if_present(self) -> str | None:
|
|
146
|
+
if self._parent.type == ParentType.BLOCK_ID:
|
|
147
|
+
return self._parent.block_id
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
def is_workspace_parent(self) -> bool:
|
|
151
|
+
return self._parent.type == ParentType.WORKSPACE
|
|
152
|
+
|
|
153
|
+
# =========================================================================
|
|
154
|
+
# User Methods
|
|
155
|
+
# =========================================================================
|
|
102
156
|
|
|
103
157
|
async def get_created_by_user(self) -> BaseUser | None:
|
|
104
158
|
return await self._user_service.get_user_by_id(self._created_by.id)
|
|
@@ -106,17 +160,19 @@ class Entity(LoggingMixin, ABC):
|
|
|
106
160
|
async def get_last_edited_by_user(self) -> BaseUser | None:
|
|
107
161
|
return await self._user_service.get_user_by_id(self._last_edited_by.id)
|
|
108
162
|
|
|
163
|
+
# =========================================================================
|
|
164
|
+
# Icon & Cover Methods
|
|
165
|
+
# =========================================================================
|
|
166
|
+
|
|
109
167
|
async def set_emoji_icon(self, emoji: str) -> None:
|
|
110
168
|
entity_response = await self._entity_metadata_update_client.patch_emoji_icon(emoji)
|
|
111
|
-
self._emoji_icon =
|
|
169
|
+
self._emoji_icon = self._extract_emoji_icon(entity_response)
|
|
112
170
|
self._external_icon_url = None
|
|
113
171
|
|
|
114
172
|
async def set_external_icon(self, icon_url: str) -> None:
|
|
115
173
|
entity_response = await self._entity_metadata_update_client.patch_external_icon(icon_url)
|
|
116
174
|
self._emoji_icon = None
|
|
117
|
-
self._external_icon_url = (
|
|
118
|
-
entity_response.icon.external.url if entity_response.icon and entity_response.icon.external else None
|
|
119
|
-
)
|
|
175
|
+
self._external_icon_url = self._extract_external_icon_url(entity_response)
|
|
120
176
|
|
|
121
177
|
async def remove_icon(self) -> None:
|
|
122
178
|
await self._entity_metadata_update_client.remove_icon()
|
|
@@ -125,9 +181,7 @@ class Entity(LoggingMixin, ABC):
|
|
|
125
181
|
|
|
126
182
|
async def set_cover_image_by_url(self, image_url: str) -> None:
|
|
127
183
|
entity_response = await self._entity_metadata_update_client.patch_external_cover(image_url)
|
|
128
|
-
self._cover_image_url = (
|
|
129
|
-
entity_response.cover.external.url if entity_response.cover and entity_response.cover.external else None
|
|
130
|
-
)
|
|
184
|
+
self._cover_image_url = self._extract_cover_image_url(entity_response)
|
|
131
185
|
|
|
132
186
|
async def set_random_gradient_cover(self) -> None:
|
|
133
187
|
random_cover_url = self._get_random_gradient_cover()
|
|
@@ -137,6 +191,10 @@ class Entity(LoggingMixin, ABC):
|
|
|
137
191
|
await self._entity_metadata_update_client.remove_cover()
|
|
138
192
|
self._cover_image_url = None
|
|
139
193
|
|
|
194
|
+
# =========================================================================
|
|
195
|
+
# Trash Methods
|
|
196
|
+
# =========================================================================
|
|
197
|
+
|
|
140
198
|
async def move_to_trash(self) -> None:
|
|
141
199
|
if self._in_trash:
|
|
142
200
|
self.logger.warning("Entity is already in trash.")
|
notionary/utils/pagination.py
CHANGED
|
@@ -12,89 +12,93 @@ class PaginatedResponse(BaseModel):
|
|
|
12
12
|
|
|
13
13
|
async def _fetch_data(
|
|
14
14
|
api_call: Callable[..., Coroutine[Any, Any, PaginatedResponse]],
|
|
15
|
-
|
|
15
|
+
total_results_limit: int | None = None,
|
|
16
16
|
**kwargs,
|
|
17
17
|
) -> AsyncGenerator[PaginatedResponse]:
|
|
18
|
-
next_cursor = None
|
|
19
|
-
has_more = True
|
|
20
|
-
total_fetched = 0
|
|
18
|
+
next_cursor: str | None = None
|
|
19
|
+
has_more: bool = True
|
|
20
|
+
total_fetched: int = 0
|
|
21
|
+
api_page_size: int = kwargs.get("page_size", 100)
|
|
21
22
|
|
|
22
|
-
while has_more and _should_continue_fetching(
|
|
23
|
-
request_params = _build_request_params(kwargs, next_cursor
|
|
23
|
+
while has_more and _should_continue_fetching(total_results_limit, total_fetched):
|
|
24
|
+
request_params = _build_request_params(kwargs, next_cursor)
|
|
24
25
|
response = await api_call(**request_params)
|
|
25
26
|
|
|
26
|
-
limited_results = _apply_result_limit(response.results,
|
|
27
|
+
limited_results = _apply_result_limit(response.results, total_results_limit, total_fetched)
|
|
27
28
|
total_fetched += len(limited_results)
|
|
28
29
|
|
|
29
|
-
yield _create_limited_response(response, limited_results)
|
|
30
|
+
yield _create_limited_response(response, limited_results, api_page_size)
|
|
30
31
|
|
|
31
|
-
if _has_reached_limit(
|
|
32
|
+
if _has_reached_limit(total_results_limit, total_fetched):
|
|
32
33
|
break
|
|
33
34
|
|
|
34
35
|
has_more = response.has_more
|
|
35
36
|
next_cursor = response.next_cursor
|
|
36
37
|
|
|
37
38
|
|
|
38
|
-
def _should_continue_fetching(
|
|
39
|
-
if
|
|
39
|
+
def _should_continue_fetching(total_limit: int | None, total_fetched: int) -> bool:
|
|
40
|
+
if total_limit is None:
|
|
40
41
|
return True
|
|
41
|
-
return total_fetched <
|
|
42
|
+
return total_fetched < total_limit
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
def _build_request_params(
|
|
45
46
|
base_kwargs: dict[str, Any],
|
|
46
47
|
cursor: str | None,
|
|
47
|
-
page_size: int | None,
|
|
48
48
|
) -> dict[str, Any]:
|
|
49
49
|
params = base_kwargs.copy()
|
|
50
|
-
|
|
51
50
|
if cursor:
|
|
52
51
|
params["start_cursor"] = cursor
|
|
53
|
-
|
|
54
|
-
if page_size:
|
|
55
|
-
params["page_size"] = page_size
|
|
56
|
-
|
|
57
52
|
return params
|
|
58
53
|
|
|
59
54
|
|
|
60
|
-
def _apply_result_limit(results: list[Any],
|
|
61
|
-
if
|
|
55
|
+
def _apply_result_limit(results: list[Any], total_limit: int | None, total_fetched: int) -> list[Any]:
|
|
56
|
+
if total_limit is None:
|
|
62
57
|
return results
|
|
63
58
|
|
|
64
|
-
|
|
65
|
-
return results[:
|
|
59
|
+
remaining_space = total_limit - total_fetched
|
|
60
|
+
return results[:remaining_space]
|
|
66
61
|
|
|
67
62
|
|
|
68
|
-
def _has_reached_limit(
|
|
69
|
-
if
|
|
63
|
+
def _has_reached_limit(total_limit: int | None, total_fetched: int) -> bool:
|
|
64
|
+
if total_limit is None:
|
|
70
65
|
return False
|
|
71
|
-
return total_fetched >=
|
|
66
|
+
return total_fetched >= total_limit
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _create_limited_response(
|
|
70
|
+
original: PaginatedResponse,
|
|
71
|
+
limited_results: list[Any],
|
|
72
|
+
api_page_size: int,
|
|
73
|
+
) -> PaginatedResponse:
|
|
74
|
+
results_were_limited_by_client = len(limited_results) < len(original.results)
|
|
75
|
+
api_returned_full_page = len(original.results) == api_page_size
|
|
72
76
|
|
|
77
|
+
has_more_after_limit = original.has_more and not results_were_limited_by_client and api_returned_full_page
|
|
73
78
|
|
|
74
|
-
def _create_limited_response(original: PaginatedResponse, limited_results: list[Any]) -> PaginatedResponse:
|
|
75
79
|
return PaginatedResponse(
|
|
76
80
|
results=limited_results,
|
|
77
|
-
has_more=
|
|
78
|
-
next_cursor=original.next_cursor,
|
|
81
|
+
has_more=has_more_after_limit,
|
|
82
|
+
next_cursor=original.next_cursor if has_more_after_limit else None,
|
|
79
83
|
)
|
|
80
84
|
|
|
81
85
|
|
|
82
86
|
async def paginate_notion_api(
|
|
83
87
|
api_call: Callable[..., Coroutine[Any, Any, PaginatedResponse]],
|
|
84
|
-
|
|
88
|
+
total_results_limit: int | None = None,
|
|
85
89
|
**kwargs,
|
|
86
90
|
) -> list[Any]:
|
|
87
91
|
all_results = []
|
|
88
|
-
async for page in _fetch_data(api_call,
|
|
92
|
+
async for page in _fetch_data(api_call, total_results_limit=total_results_limit, **kwargs):
|
|
89
93
|
all_results.extend(page.results)
|
|
90
94
|
return all_results
|
|
91
95
|
|
|
92
96
|
|
|
93
97
|
async def paginate_notion_api_generator(
|
|
94
98
|
api_call: Callable[..., Coroutine[Any, Any, PaginatedResponse]],
|
|
95
|
-
|
|
99
|
+
total_results_limit: int | None = None,
|
|
96
100
|
**kwargs,
|
|
97
101
|
) -> AsyncGenerator[Any]:
|
|
98
|
-
async for page in _fetch_data(api_call,
|
|
102
|
+
async for page in _fetch_data(api_call, total_results_limit, **kwargs):
|
|
99
103
|
for item in page.results:
|
|
100
104
|
yield item
|
notionary/workspace/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from .query import
|
|
1
|
+
from .query import NotionWorkspaceQueryConfigBuilder
|
|
2
2
|
from .service import NotionWorkspace
|
|
3
3
|
|
|
4
|
-
__all__ = ["NotionWorkspace", "
|
|
4
|
+
__all__ = ["NotionWorkspace", "NotionWorkspaceQueryConfigBuilder"]
|
notionary/workspace/client.py
CHANGED
|
@@ -22,6 +22,7 @@ class WorkspaceClient:
|
|
|
22
22
|
async for page in paginate_notion_api_generator(
|
|
23
23
|
self._query_pages,
|
|
24
24
|
search_config=search_config,
|
|
25
|
+
total_results_limit=search_config.total_results_limit,
|
|
25
26
|
):
|
|
26
27
|
yield page
|
|
27
28
|
|
|
@@ -32,6 +33,7 @@ class WorkspaceClient:
|
|
|
32
33
|
async for data_source in paginate_notion_api_generator(
|
|
33
34
|
self._query_data_sources,
|
|
34
35
|
search_config=search_config,
|
|
36
|
+
total_results_limit=search_config.total_results_limit,
|
|
35
37
|
):
|
|
36
38
|
yield data_source
|
|
37
39
|
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
from .builder import
|
|
1
|
+
from .builder import NotionWorkspaceQueryConfigBuilder
|
|
2
2
|
|
|
3
|
-
__all__ = ["
|
|
3
|
+
__all__ = ["NotionWorkspaceQueryConfigBuilder"]
|