notionary 0.4.0__py3-none-any.whl → 0.4.2__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 (178) hide show
  1. notionary/__init__.py +44 -1
  2. notionary/blocks/client.py +37 -11
  3. notionary/blocks/rich_text/markdown_rich_text_converter.py +49 -15
  4. notionary/blocks/rich_text/models.py +13 -4
  5. notionary/blocks/rich_text/name_id_resolver/data_source.py +9 -3
  6. notionary/blocks/rich_text/name_id_resolver/person.py +6 -2
  7. notionary/blocks/rich_text/rich_text_markdown_converter.py +10 -3
  8. notionary/blocks/schemas.py +2 -1
  9. notionary/comments/client.py +19 -6
  10. notionary/comments/factory.py +10 -3
  11. notionary/comments/schemas.py +9 -3
  12. notionary/comments/service.py +12 -4
  13. notionary/data_source/http/data_source_instance_client.py +59 -17
  14. notionary/data_source/properties/schemas.py +30 -10
  15. notionary/data_source/query/builder.py +67 -18
  16. notionary/data_source/query/resolver.py +16 -5
  17. notionary/data_source/query/schema.py +24 -6
  18. notionary/data_source/query/validator.py +18 -6
  19. notionary/data_source/schema/registry.py +31 -12
  20. notionary/data_source/schema/service.py +66 -20
  21. notionary/data_source/service.py +74 -23
  22. notionary/database/client.py +27 -9
  23. notionary/database/database_metadata_update_client.py +12 -4
  24. notionary/database/service.py +11 -4
  25. notionary/exceptions/__init__.py +15 -3
  26. notionary/exceptions/block_parsing.py +6 -2
  27. notionary/exceptions/data_source/builder.py +11 -5
  28. notionary/exceptions/data_source/properties.py +3 -1
  29. notionary/exceptions/file_upload.py +12 -3
  30. notionary/exceptions/properties.py +3 -1
  31. notionary/exceptions/search.py +6 -2
  32. notionary/file_upload/client.py +5 -1
  33. notionary/file_upload/config/config.py +10 -3
  34. notionary/file_upload/query/builder.py +6 -2
  35. notionary/file_upload/schemas.py +3 -1
  36. notionary/file_upload/service.py +42 -14
  37. notionary/file_upload/validation/factory.py +3 -1
  38. notionary/file_upload/validation/impl/file_name_length.py +3 -1
  39. notionary/file_upload/validation/models.py +15 -5
  40. notionary/file_upload/validation/validators/file_extension.py +12 -3
  41. notionary/http/client.py +27 -8
  42. notionary/page/content/__init__.py +9 -0
  43. notionary/page/content/factory.py +21 -7
  44. notionary/page/content/markdown/builder.py +85 -23
  45. notionary/page/content/markdown/nodes/audio.py +8 -4
  46. notionary/page/content/markdown/nodes/base.py +3 -3
  47. notionary/page/content/markdown/nodes/bookmark.py +5 -3
  48. notionary/page/content/markdown/nodes/breadcrumb.py +2 -2
  49. notionary/page/content/markdown/nodes/bulleted_list.py +5 -3
  50. notionary/page/content/markdown/nodes/callout.py +2 -2
  51. notionary/page/content/markdown/nodes/code.py +5 -3
  52. notionary/page/content/markdown/nodes/columns.py +3 -3
  53. notionary/page/content/markdown/nodes/container.py +9 -5
  54. notionary/page/content/markdown/nodes/divider.py +2 -2
  55. notionary/page/content/markdown/nodes/embed.py +8 -4
  56. notionary/page/content/markdown/nodes/equation.py +4 -2
  57. notionary/page/content/markdown/nodes/file.py +8 -4
  58. notionary/page/content/markdown/nodes/heading.py +2 -2
  59. notionary/page/content/markdown/nodes/image.py +8 -4
  60. notionary/page/content/markdown/nodes/mixins/caption.py +5 -3
  61. notionary/page/content/markdown/nodes/numbered_list.py +5 -3
  62. notionary/page/content/markdown/nodes/paragraph.py +4 -2
  63. notionary/page/content/markdown/nodes/pdf.py +8 -4
  64. notionary/page/content/markdown/nodes/quote.py +2 -2
  65. notionary/page/content/markdown/nodes/space.py +2 -2
  66. notionary/page/content/markdown/nodes/table.py +8 -5
  67. notionary/page/content/markdown/nodes/table_of_contents.py +2 -2
  68. notionary/page/content/markdown/nodes/todo.py +15 -7
  69. notionary/page/content/markdown/nodes/toggle.py +2 -2
  70. notionary/page/content/markdown/nodes/video.py +8 -4
  71. notionary/page/content/markdown/structured_output/__init__.py +73 -0
  72. notionary/page/content/markdown/structured_output/models.py +391 -0
  73. notionary/page/content/markdown/structured_output/service.py +211 -0
  74. notionary/page/content/parser/context.py +1 -1
  75. notionary/page/content/parser/factory.py +23 -8
  76. notionary/page/content/parser/parsers/audio.py +7 -2
  77. notionary/page/content/parser/parsers/base.py +2 -2
  78. notionary/page/content/parser/parsers/bookmark.py +2 -2
  79. notionary/page/content/parser/parsers/breadcrumb.py +2 -2
  80. notionary/page/content/parser/parsers/bulleted_list.py +19 -6
  81. notionary/page/content/parser/parsers/callout.py +15 -5
  82. notionary/page/content/parser/parsers/caption.py +9 -3
  83. notionary/page/content/parser/parsers/code.py +21 -7
  84. notionary/page/content/parser/parsers/column.py +8 -4
  85. notionary/page/content/parser/parsers/column_list.py +19 -7
  86. notionary/page/content/parser/parsers/divider.py +2 -2
  87. notionary/page/content/parser/parsers/embed.py +2 -2
  88. notionary/page/content/parser/parsers/equation.py +8 -4
  89. notionary/page/content/parser/parsers/file.py +7 -2
  90. notionary/page/content/parser/parsers/file_like_block.py +30 -10
  91. notionary/page/content/parser/parsers/heading.py +31 -10
  92. notionary/page/content/parser/parsers/image.py +7 -2
  93. notionary/page/content/parser/parsers/numbered_list.py +18 -6
  94. notionary/page/content/parser/parsers/paragraph.py +3 -1
  95. notionary/page/content/parser/parsers/pdf.py +7 -2
  96. notionary/page/content/parser/parsers/quote.py +28 -9
  97. notionary/page/content/parser/parsers/space.py +2 -2
  98. notionary/page/content/parser/parsers/table.py +31 -10
  99. notionary/page/content/parser/parsers/table_of_contents.py +7 -3
  100. notionary/page/content/parser/parsers/todo.py +15 -5
  101. notionary/page/content/parser/parsers/toggle.py +15 -5
  102. notionary/page/content/parser/parsers/video.py +7 -2
  103. notionary/page/content/parser/post_processing/handlers/rich_text_length.py +8 -2
  104. notionary/page/content/parser/post_processing/handlers/rich_text_length_truncation.py +8 -2
  105. notionary/page/content/parser/post_processing/service.py +3 -1
  106. notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +21 -7
  107. notionary/page/content/parser/pre_processsing/handlers/indentation.py +11 -4
  108. notionary/page/content/parser/pre_processsing/handlers/video_syntax.py +13 -6
  109. notionary/page/content/parser/service.py +4 -1
  110. notionary/page/content/renderer/context.py +15 -5
  111. notionary/page/content/renderer/factory.py +12 -6
  112. notionary/page/content/renderer/post_processing/handlers/numbered_list.py +19 -9
  113. notionary/page/content/renderer/renderers/audio.py +14 -5
  114. notionary/page/content/renderer/renderers/base.py +3 -3
  115. notionary/page/content/renderer/renderers/bookmark.py +3 -1
  116. notionary/page/content/renderer/renderers/bulleted_list.py +11 -5
  117. notionary/page/content/renderer/renderers/callout.py +19 -7
  118. notionary/page/content/renderer/renderers/captioned_block.py +11 -5
  119. notionary/page/content/renderer/renderers/code.py +6 -2
  120. notionary/page/content/renderer/renderers/column.py +3 -1
  121. notionary/page/content/renderer/renderers/column_list.py +3 -1
  122. notionary/page/content/renderer/renderers/embed.py +3 -1
  123. notionary/page/content/renderer/renderers/equation.py +3 -1
  124. notionary/page/content/renderer/renderers/file.py +14 -5
  125. notionary/page/content/renderer/renderers/file_like_block.py +8 -4
  126. notionary/page/content/renderer/renderers/heading.py +22 -8
  127. notionary/page/content/renderer/renderers/image.py +13 -4
  128. notionary/page/content/renderer/renderers/numbered_list.py +8 -3
  129. notionary/page/content/renderer/renderers/paragraph.py +12 -4
  130. notionary/page/content/renderer/renderers/pdf.py +14 -5
  131. notionary/page/content/renderer/renderers/quote.py +14 -6
  132. notionary/page/content/renderer/renderers/table.py +15 -5
  133. notionary/page/content/renderer/renderers/todo.py +16 -6
  134. notionary/page/content/renderer/renderers/toggle.py +8 -4
  135. notionary/page/content/renderer/renderers/video.py +14 -5
  136. notionary/page/content/renderer/service.py +9 -3
  137. notionary/page/content/service.py +21 -7
  138. notionary/page/content/syntax/definition/__init__.py +11 -0
  139. notionary/page/content/syntax/definition/models.py +57 -0
  140. notionary/page/content/syntax/definition/registry.py +371 -0
  141. notionary/page/content/syntax/prompts/__init__.py +4 -0
  142. notionary/page/content/syntax/prompts/models.py +11 -0
  143. notionary/page/content/syntax/prompts/registry.py +703 -0
  144. notionary/page/page_metadata_update_client.py +12 -4
  145. notionary/page/properties/client.py +45 -15
  146. notionary/page/properties/factory.py +6 -2
  147. notionary/page/properties/service.py +110 -36
  148. notionary/page/service.py +20 -6
  149. notionary/shared/entity/client.py +6 -2
  150. notionary/shared/entity/dto_parsers.py +3 -1
  151. notionary/shared/entity/entity_metadata_update_client.py +9 -3
  152. notionary/shared/entity/schemas.py +1 -1
  153. notionary/shared/entity/service.py +53 -22
  154. notionary/shared/models/file.py +3 -1
  155. notionary/shared/models/icon.py +6 -4
  156. notionary/user/base.py +6 -2
  157. notionary/user/bot.py +10 -2
  158. notionary/user/client.py +3 -1
  159. notionary/user/person.py +3 -1
  160. notionary/user/schemas.py +3 -1
  161. notionary/user/service.py +6 -2
  162. notionary/utils/decorators.py +6 -2
  163. notionary/utils/fuzzy.py +6 -2
  164. notionary/utils/mixins/logging.py +3 -1
  165. notionary/utils/pagination.py +14 -4
  166. notionary/workspace/__init__.py +5 -1
  167. notionary/workspace/query/service.py +59 -16
  168. notionary/workspace/service.py +39 -11
  169. {notionary-0.4.0.dist-info → notionary-0.4.2.dist-info}/METADATA +1 -1
  170. notionary-0.4.2.dist-info/RECORD +236 -0
  171. notionary/page/blocks/client.py +0 -1
  172. notionary/page/content/syntax/__init__.py +0 -5
  173. notionary/page/content/syntax/models.py +0 -66
  174. notionary/page/content/syntax/registry.py +0 -371
  175. notionary-0.4.0.dist-info/RECORD +0 -230
  176. /notionary/page/content/syntax/{grammar.py → definition/grammar.py} +0 -0
  177. {notionary-0.4.0.dist-info → notionary-0.4.2.dist-info}/WHEEL +0 -0
  178. {notionary-0.4.0.dist-info → notionary-0.4.2.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 EntityMetadataUpdateClient
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(emoji)
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(icon_url)
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(self, file_path: Path, filename: str | None = None) -> None:
178
- upload_response = await self._file_upload_service.upload_file(file_path, filename)
179
- await self._set_icon_from_file_upload(upload_response.id)
180
-
181
- async def set_icon_from_bytes(self, file_content: bytes, filename: str, content_type: str | None = None) -> None:
182
- upload_response = await self._file_upload_service.upload_from_bytes(file_content, filename, content_type)
183
- await self._set_icon_from_file_upload(upload_response.id)
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 _set_icon_from_file_upload(self, file_upload_id: str) -> None:
186
- entity_response = await self._entity_metadata_update_client.patch_icon_from_file_upload(file_upload_id)
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 = await self._entity_metadata_update_client.patch_external_cover(image_url)
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(self, file_path: Path, filename: str | None = None) -> None:
200
- upload_response = await self._file_upload_service.upload_file(file_path, filename)
201
- await self._set_cover_image_from_file_upload(upload_response.id)
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(file_content, filename, content_type)
207
- await self._set_cover_image_from_file_upload(upload_response.id)
208
-
209
- async def _set_cover_image_from_file_upload(self, file_upload_id: str) -> None:
210
- entity_response = await self._entity_metadata_update_client.patch_cover_from_file_upload(file_upload_id)
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" for i in range(1, 10)
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)
@@ -46,4 +46,6 @@ class FileUploadFile(BaseModel):
46
46
  return cls(file_upload=FileUploadedFileData(id=id))
47
47
 
48
48
 
49
- type File = Annotated[ExternalFile | NotionHostedFile | FileUploadFile, Field(discriminator="type")]
49
+ type File = Annotated[
50
+ ExternalFile | NotionHostedFile | FileUploadFile, Field(discriminator="type")
51
+ ]
@@ -1,9 +1,11 @@
1
1
  from enum import StrEnum
2
- from typing import Literal
2
+ from typing import Annotated, Literal
3
3
 
4
- from pydantic import BaseModel
4
+ from pydantic import BaseModel, Field
5
5
 
6
- from notionary.shared.models.file import File
6
+ from notionary.shared.models.file import (
7
+ File,
8
+ )
7
9
 
8
10
 
9
11
  class IconType(StrEnum):
@@ -18,4 +20,4 @@ class EmojiIcon(BaseModel):
18
20
  emoji: str
19
21
 
20
22
 
21
- type Icon = EmojiIcon | File
23
+ type Icon = Annotated[EmojiIcon | File, Field(discriminator="type")]
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(f"User {user_id} is not a '{expected_type.value}', but '{user_dto.type.value}'")
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 = [dto for dto in all_workspace_user_dtos if dto.type == expected_type]
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 BotUserDto, BotUserResponseDto, UserResponseDto, UserType, WorkspaceOwnerType
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 = 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(self._get_workspace_entities, page_size=100)
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
@@ -38,4 +38,6 @@ class PersonUser(BaseUser):
38
38
  return self._email
39
39
 
40
40
  def __repr__(self) -> str:
41
- return f"PersonUser(id={self._id!r}, name={self._name!r}, email={self._email!r})"
41
+ return (
42
+ f"PersonUser(id={self._id!r}, name={self._name!r}, email={self._email!r})"
43
+ )
notionary/user/schemas.py CHANGED
@@ -53,7 +53,9 @@ class BotUserResponseDto(NotionUserBase):
53
53
  bot: BotUserDto
54
54
 
55
55
 
56
- UserResponseDto = Annotated[PersonUserResponseDto | BotUserResponseDto, Field(discriminator="type")]
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() or query_lower in (user.email 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 query_lower in (user.name or "").lower() or query_lower in (user.email or "").lower():
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:
@@ -26,7 +26,9 @@ def singleton(cls):
26
26
  return wrapper
27
27
 
28
28
 
29
- def time_execution_sync(additional_text: str = "", min_duration_to_log: float = 0.25) -> _SyncDecorator:
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(e, retry_on_exceptions):
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(query, items, text_extractor, min_similarity, limit=None)
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(results: list[_MatchResult]) -> list[_MatchResult]:
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(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
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)
@@ -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(response.results, total_results_limit, total_fetched)
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(results: list[Any], total_limit: int | None, total_fetched: int) -> list[Any]:
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 = original.has_more and not results_were_limited_by_client and api_returned_full_page
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(api_call, total_results_limit=total_results_limit, **kwargs):
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
 
@@ -1,4 +1,8 @@
1
1
  from .query import NotionWorkspaceQueryConfigBuilder, WorkspaceQueryConfig
2
2
  from .service import NotionWorkspace
3
3
 
4
- __all__ = ["NotionWorkspace", "NotionWorkspaceQueryConfigBuilder", "WorkspaceQueryConfig"]
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 DatabaseNotFound, DataSourceNotFound, PageNotFound
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(self, search_config: WorkspaceQueryConfig) -> AsyncIterator[NotionPage]:
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 = [dto async for dto in self._client.query_pages_stream(search_config)]
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(self, search_config: WorkspaceQueryConfig) -> AsyncIterator[NotionDataSource]:
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(search_config):
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(self, search_config: WorkspaceQueryConfig) -> list[NotionDataSource]:
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 = [dto async for dto in self._client.query_data_sources_stream(search_config)]
44
- data_source_tasks = [NotionDataSource.from_id(dto.id) for dto in data_source_dtos]
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().with_query(query).with_data_sources_only().with_page_size(100).build()
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 = NotionWorkspaceQueryConfigBuilder().with_query(query).with_pages_only().with_page_size(100).build()
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().with_query(query).with_data_sources_only().with_page_size(100).build()
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 = [data_sources.get_parent_database_id_if_present() for data_sources in data_sources]
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 = [NotionDatabase.from_id(db_id) for db_id in parent_database_ids]
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 = [database for database in parent_databases if database is not None]
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 = [result for result in search_results if result.title.lower() == query_lower]
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(self, search_results: list[SearchableEntity], query: str) -> list[str]:
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,
@@ -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 WorkspaceQueryConfig, WorkspaceQueryObjectType
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[[NotionWorkspaceQueryConfigBuilder], NotionWorkspaceQueryConfigBuilder] | None = None,
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(filter_fn, query_config, WorkspaceQueryObjectType.PAGE)
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[[NotionWorkspaceQueryConfigBuilder], NotionWorkspaceQueryConfigBuilder] | None = None,
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(filter_fn, query_config, WorkspaceQueryObjectType.PAGE)
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[[NotionWorkspaceQueryConfigBuilder], NotionWorkspaceQueryConfigBuilder] | None = None,
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(filter_fn, query_config, WorkspaceQueryObjectType.DATA_SOURCE)
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[[NotionWorkspaceQueryConfigBuilder], NotionWorkspaceQueryConfigBuilder] | None = None,
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(filter_fn, query_config, WorkspaceQueryObjectType.DATA_SOURCE)
86
- async for data_source in self._query_service.get_data_sources_stream(resolved_config):
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[[NotionWorkspaceQueryConfigBuilder], NotionWorkspaceQueryConfigBuilder] | None,
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.0
3
+ Version: 0.4.2
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>