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.
Files changed (34) hide show
  1. notionary/__init__.py +9 -2
  2. notionary/blocks/enums.py +27 -0
  3. notionary/comments/client.py +6 -9
  4. notionary/data_source/http/data_source_instance_client.py +4 -4
  5. notionary/data_source/query/__init__.py +9 -0
  6. notionary/data_source/query/builder.py +12 -3
  7. notionary/data_source/query/schema.py +5 -0
  8. notionary/data_source/service.py +14 -112
  9. notionary/database/service.py +16 -58
  10. notionary/exceptions/__init__.py +4 -0
  11. notionary/exceptions/block_parsing.py +21 -0
  12. notionary/http/client.py +1 -1
  13. notionary/page/content/factory.py +2 -0
  14. notionary/page/content/parser/pre_processsing/handlers/__init__.py +2 -0
  15. notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +12 -8
  16. notionary/page/content/parser/pre_processsing/handlers/indentation.py +2 -0
  17. notionary/page/content/parser/pre_processsing/handlers/video_syntax.py +66 -0
  18. notionary/page/content/parser/pre_processsing/handlers/whitespace.py +2 -0
  19. notionary/page/properties/client.py +2 -2
  20. notionary/page/properties/service.py +14 -3
  21. notionary/page/service.py +17 -78
  22. notionary/shared/entity/service.py +94 -36
  23. notionary/utils/pagination.py +36 -32
  24. notionary/workspace/__init__.py +2 -2
  25. notionary/workspace/client.py +2 -0
  26. notionary/workspace/query/__init__.py +2 -2
  27. notionary/workspace/query/builder.py +25 -1
  28. notionary/workspace/query/models.py +9 -1
  29. notionary/workspace/query/service.py +29 -11
  30. notionary/workspace/service.py +31 -21
  31. {notionary-0.3.0.dist-info → notionary-0.3.1.dist-info}/METADATA +9 -5
  32. {notionary-0.3.0.dist-info → notionary-0.3.1.dist-info}/RECORD +34 -32
  33. {notionary-0.3.0.dist-info → notionary-0.3.1.dist-info}/WHEEL +0 -0
  34. {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("title", title, PageTitleProperty)
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, property_name: str, title: str) -> None:
151
- self._get_typed_property_or_raise(property_name, PageTitleProperty)
152
- updated_page = await self._property_http_client.patch_title(property_name, title)
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
- id: str,
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
- id=id,
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
- response = await cls._fetch_page_dto(page_id)
79
- return await cls._create_from_dto(response, factory)
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
- response: NotionPageDto,
70
+ dto: NotionPageDto,
99
71
  page_property_handler_factory: PagePropertyHandlerFactory,
100
72
  ) -> Self:
101
- title_task = cls._extract_title_from_dto(response)
102
- page_property_handler = page_property_handler_factory.create_from_page_response(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
- id=response.id,
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
- id: str,
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
- id=id,
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 url(self) -> str:
189
- return self._url
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 TYPE_CHECKING, Self
4
+ from typing import Self
7
5
 
8
6
  from notionary.shared.entity.entity_metadata_update_client import EntityMetadataUpdateClient
9
- from notionary.user.schemas import PartialUserDto
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
- id: str,
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._emoji_icon = emoji_icon
38
- self._external_icon_url = external_icon_url
39
- self._cover_image_url = cover_image_url
40
- self._in_trash = in_trash
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 created_by(self) -> PartialUserDto:
97
- return self._created_by
119
+ def url(self) -> str:
120
+ return self._url
98
121
 
99
122
  @property
100
- def last_edited_by(self) -> PartialUserDto:
101
- return self._last_edited_by
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 = entity_response.icon.emoji if entity_response.icon else None
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.")
@@ -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
- page_size: int | None = None,
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(page_size, total_fetched):
23
- request_params = _build_request_params(kwargs, next_cursor, page_size)
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, page_size, total_fetched)
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(page_size, total_fetched):
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(page_size: int | None, total_fetched: int) -> bool:
39
- if page_size is None:
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 < page_size
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], page_size: int | None, total_fetched: int) -> list[Any]:
61
- if page_size is None:
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
- remaining = page_size - total_fetched
65
- return results[:remaining]
59
+ remaining_space = total_limit - total_fetched
60
+ return results[:remaining_space]
66
61
 
67
62
 
68
- def _has_reached_limit(page_size: int | None, total_fetched: int) -> bool:
69
- if page_size is None:
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 >= page_size
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=original.has_more and len(limited_results) == len(original.results),
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
- page_size: int | None = None,
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, page_size=page_size, **kwargs):
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
- page_size: int | None = None,
99
+ total_results_limit: int | None = None,
96
100
  **kwargs,
97
101
  ) -> AsyncGenerator[Any]:
98
- async for page in _fetch_data(api_call, page_size=page_size, **kwargs):
102
+ async for page in _fetch_data(api_call, total_results_limit, **kwargs):
99
103
  for item in page.results:
100
104
  yield item
@@ -1,4 +1,4 @@
1
- from .query import WorkspaceQueryConfigBuilder
1
+ from .query import NotionWorkspaceQueryConfigBuilder
2
2
  from .service import NotionWorkspace
3
3
 
4
- __all__ = ["NotionWorkspace", "WorkspaceQueryConfigBuilder"]
4
+ __all__ = ["NotionWorkspace", "NotionWorkspaceQueryConfigBuilder"]
@@ -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 WorkspaceQueryConfigBuilder
1
+ from .builder import NotionWorkspaceQueryConfigBuilder
2
2
 
3
- __all__ = ["WorkspaceQueryConfigBuilder"]
3
+ __all__ = ["NotionWorkspaceQueryConfigBuilder"]