notionary 0.3.1__py3-none-any.whl → 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. notionary/__init__.py +49 -1
  2. notionary/blocks/client.py +37 -11
  3. notionary/blocks/enums.py +0 -6
  4. notionary/blocks/rich_text/markdown_rich_text_converter.py +49 -15
  5. notionary/blocks/rich_text/models.py +13 -4
  6. notionary/blocks/rich_text/name_id_resolver/data_source.py +9 -3
  7. notionary/blocks/rich_text/name_id_resolver/person.py +6 -2
  8. notionary/blocks/rich_text/rich_text_markdown_converter.py +10 -3
  9. notionary/blocks/schemas.py +33 -78
  10. notionary/comments/client.py +19 -6
  11. notionary/comments/factory.py +10 -3
  12. notionary/comments/schemas.py +10 -31
  13. notionary/comments/service.py +12 -4
  14. notionary/data_source/http/data_source_instance_client.py +59 -17
  15. notionary/data_source/properties/schemas.py +156 -115
  16. notionary/data_source/query/builder.py +67 -18
  17. notionary/data_source/query/resolver.py +16 -5
  18. notionary/data_source/query/schema.py +24 -6
  19. notionary/data_source/query/validator.py +18 -6
  20. notionary/data_source/schema/registry.py +31 -12
  21. notionary/data_source/schema/service.py +66 -20
  22. notionary/data_source/schemas.py +2 -2
  23. notionary/data_source/service.py +103 -43
  24. notionary/database/client.py +27 -9
  25. notionary/database/database_metadata_update_client.py +12 -4
  26. notionary/database/schemas.py +2 -2
  27. notionary/database/service.py +14 -9
  28. notionary/exceptions/__init__.py +20 -4
  29. notionary/exceptions/api.py +2 -2
  30. notionary/exceptions/base.py +1 -1
  31. notionary/exceptions/block_parsing.py +9 -5
  32. notionary/exceptions/data_source/builder.py +13 -7
  33. notionary/exceptions/data_source/properties.py +6 -4
  34. notionary/exceptions/file_upload.py +76 -0
  35. notionary/exceptions/properties.py +7 -5
  36. notionary/exceptions/search.py +10 -6
  37. notionary/file_upload/__init__.py +4 -0
  38. notionary/file_upload/client.py +128 -210
  39. notionary/file_upload/config/__init__.py +17 -0
  40. notionary/file_upload/config/config.py +39 -0
  41. notionary/file_upload/config/constants.py +16 -0
  42. notionary/file_upload/file/reader.py +28 -0
  43. notionary/file_upload/query/__init__.py +7 -0
  44. notionary/file_upload/query/builder.py +58 -0
  45. notionary/file_upload/query/models.py +37 -0
  46. notionary/file_upload/schemas.py +80 -0
  47. notionary/file_upload/service.py +182 -291
  48. notionary/file_upload/validation/factory.py +66 -0
  49. notionary/file_upload/validation/impl/file_name_length.py +25 -0
  50. notionary/file_upload/validation/models.py +134 -0
  51. notionary/file_upload/validation/port.py +7 -0
  52. notionary/file_upload/validation/service.py +17 -0
  53. notionary/file_upload/validation/validators/__init__.py +11 -0
  54. notionary/file_upload/validation/validators/file_exists.py +15 -0
  55. notionary/file_upload/validation/validators/file_extension.py +131 -0
  56. notionary/file_upload/validation/validators/file_name_length.py +21 -0
  57. notionary/file_upload/validation/validators/upload_limit.py +31 -0
  58. notionary/http/client.py +33 -30
  59. notionary/page/content/__init__.py +9 -0
  60. notionary/page/content/factory.py +21 -7
  61. notionary/page/content/markdown/builder.py +85 -23
  62. notionary/page/content/markdown/nodes/audio.py +8 -4
  63. notionary/page/content/markdown/nodes/base.py +3 -3
  64. notionary/page/content/markdown/nodes/bookmark.py +5 -3
  65. notionary/page/content/markdown/nodes/breadcrumb.py +2 -2
  66. notionary/page/content/markdown/nodes/bulleted_list.py +5 -3
  67. notionary/page/content/markdown/nodes/callout.py +2 -2
  68. notionary/page/content/markdown/nodes/code.py +5 -3
  69. notionary/page/content/markdown/nodes/columns.py +3 -3
  70. notionary/page/content/markdown/nodes/container.py +9 -5
  71. notionary/page/content/markdown/nodes/divider.py +2 -2
  72. notionary/page/content/markdown/nodes/embed.py +8 -4
  73. notionary/page/content/markdown/nodes/equation.py +4 -2
  74. notionary/page/content/markdown/nodes/file.py +8 -4
  75. notionary/page/content/markdown/nodes/heading.py +2 -2
  76. notionary/page/content/markdown/nodes/image.py +8 -4
  77. notionary/page/content/markdown/nodes/mixins/caption.py +5 -3
  78. notionary/page/content/markdown/nodes/numbered_list.py +5 -3
  79. notionary/page/content/markdown/nodes/paragraph.py +4 -2
  80. notionary/page/content/markdown/nodes/pdf.py +8 -4
  81. notionary/page/content/markdown/nodes/quote.py +2 -2
  82. notionary/page/content/markdown/nodes/space.py +2 -2
  83. notionary/page/content/markdown/nodes/table.py +8 -5
  84. notionary/page/content/markdown/nodes/table_of_contents.py +2 -2
  85. notionary/page/content/markdown/nodes/todo.py +15 -7
  86. notionary/page/content/markdown/nodes/toggle.py +2 -2
  87. notionary/page/content/markdown/nodes/video.py +8 -4
  88. notionary/page/content/markdown/structured_output/__init__.py +73 -0
  89. notionary/page/content/markdown/structured_output/models.py +391 -0
  90. notionary/page/content/markdown/structured_output/service.py +211 -0
  91. notionary/page/content/parser/context.py +1 -1
  92. notionary/page/content/parser/factory.py +26 -8
  93. notionary/page/content/parser/parsers/audio.py +12 -32
  94. notionary/page/content/parser/parsers/base.py +2 -2
  95. notionary/page/content/parser/parsers/bookmark.py +2 -2
  96. notionary/page/content/parser/parsers/breadcrumb.py +2 -2
  97. notionary/page/content/parser/parsers/bulleted_list.py +19 -6
  98. notionary/page/content/parser/parsers/callout.py +15 -5
  99. notionary/page/content/parser/parsers/caption.py +9 -3
  100. notionary/page/content/parser/parsers/code.py +21 -7
  101. notionary/page/content/parser/parsers/column.py +8 -4
  102. notionary/page/content/parser/parsers/column_list.py +19 -7
  103. notionary/page/content/parser/parsers/divider.py +2 -2
  104. notionary/page/content/parser/parsers/embed.py +2 -4
  105. notionary/page/content/parser/parsers/equation.py +8 -4
  106. notionary/page/content/parser/parsers/file.py +12 -34
  107. notionary/page/content/parser/parsers/file_like_block.py +109 -0
  108. notionary/page/content/parser/parsers/heading.py +31 -10
  109. notionary/page/content/parser/parsers/image.py +12 -34
  110. notionary/page/content/parser/parsers/numbered_list.py +18 -6
  111. notionary/page/content/parser/parsers/paragraph.py +3 -1
  112. notionary/page/content/parser/parsers/pdf.py +12 -34
  113. notionary/page/content/parser/parsers/quote.py +28 -9
  114. notionary/page/content/parser/parsers/space.py +2 -2
  115. notionary/page/content/parser/parsers/table.py +31 -10
  116. notionary/page/content/parser/parsers/table_of_contents.py +7 -3
  117. notionary/page/content/parser/parsers/todo.py +15 -5
  118. notionary/page/content/parser/parsers/toggle.py +15 -5
  119. notionary/page/content/parser/parsers/video.py +12 -34
  120. notionary/page/content/parser/post_processing/handlers/rich_text_length.py +8 -2
  121. notionary/page/content/parser/post_processing/handlers/rich_text_length_truncation.py +8 -2
  122. notionary/page/content/parser/post_processing/service.py +3 -1
  123. notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +21 -7
  124. notionary/page/content/parser/pre_processsing/handlers/indentation.py +11 -4
  125. notionary/page/content/parser/pre_processsing/handlers/video_syntax.py +13 -6
  126. notionary/page/content/parser/service.py +4 -1
  127. notionary/page/content/renderer/context.py +15 -5
  128. notionary/page/content/renderer/factory.py +12 -6
  129. notionary/page/content/renderer/post_processing/handlers/numbered_list.py +19 -9
  130. notionary/page/content/renderer/renderers/audio.py +20 -23
  131. notionary/page/content/renderer/renderers/base.py +3 -3
  132. notionary/page/content/renderer/renderers/bookmark.py +3 -1
  133. notionary/page/content/renderer/renderers/bulleted_list.py +11 -5
  134. notionary/page/content/renderer/renderers/callout.py +19 -7
  135. notionary/page/content/renderer/renderers/captioned_block.py +11 -5
  136. notionary/page/content/renderer/renderers/code.py +6 -2
  137. notionary/page/content/renderer/renderers/column.py +3 -1
  138. notionary/page/content/renderer/renderers/column_list.py +3 -1
  139. notionary/page/content/renderer/renderers/embed.py +3 -1
  140. notionary/page/content/renderer/renderers/equation.py +3 -1
  141. notionary/page/content/renderer/renderers/file.py +20 -23
  142. notionary/page/content/renderer/renderers/file_like_block.py +47 -0
  143. notionary/page/content/renderer/renderers/heading.py +22 -8
  144. notionary/page/content/renderer/renderers/image.py +20 -23
  145. notionary/page/content/renderer/renderers/numbered_list.py +8 -3
  146. notionary/page/content/renderer/renderers/paragraph.py +12 -4
  147. notionary/page/content/renderer/renderers/pdf.py +20 -23
  148. notionary/page/content/renderer/renderers/quote.py +14 -6
  149. notionary/page/content/renderer/renderers/table.py +15 -5
  150. notionary/page/content/renderer/renderers/todo.py +16 -6
  151. notionary/page/content/renderer/renderers/toggle.py +8 -4
  152. notionary/page/content/renderer/renderers/video.py +20 -23
  153. notionary/page/content/renderer/service.py +9 -3
  154. notionary/page/content/service.py +21 -7
  155. notionary/page/content/syntax/definition/__init__.py +11 -0
  156. notionary/page/content/syntax/definition/models.py +57 -0
  157. notionary/page/content/syntax/definition/registry.py +371 -0
  158. notionary/page/content/syntax/prompts/__init__.py +4 -0
  159. notionary/page/content/syntax/prompts/models.py +11 -0
  160. notionary/page/content/syntax/prompts/registry.py +703 -0
  161. notionary/page/page_metadata_update_client.py +12 -4
  162. notionary/page/properties/client.py +46 -16
  163. notionary/page/properties/factory.py +6 -2
  164. notionary/page/properties/{models.py → schemas.py} +93 -107
  165. notionary/page/properties/service.py +111 -37
  166. notionary/page/schemas.py +3 -3
  167. notionary/page/service.py +21 -7
  168. notionary/shared/entity/client.py +6 -2
  169. notionary/shared/entity/dto_parsers.py +4 -37
  170. notionary/shared/entity/entity_metadata_update_client.py +25 -5
  171. notionary/shared/entity/schemas.py +6 -6
  172. notionary/shared/entity/service.py +89 -35
  173. notionary/shared/models/file.py +36 -6
  174. notionary/shared/models/icon.py +5 -12
  175. notionary/user/base.py +6 -2
  176. notionary/user/bot.py +22 -14
  177. notionary/user/client.py +3 -1
  178. notionary/user/person.py +3 -1
  179. notionary/user/schemas.py +3 -1
  180. notionary/user/service.py +6 -2
  181. notionary/utils/decorators.py +13 -9
  182. notionary/utils/fuzzy.py +6 -2
  183. notionary/utils/mixins/logging.py +3 -1
  184. notionary/utils/pagination.py +14 -4
  185. notionary/workspace/__init__.py +6 -2
  186. notionary/workspace/query/__init__.py +2 -1
  187. notionary/workspace/query/service.py +42 -13
  188. notionary/workspace/service.py +74 -46
  189. {notionary-0.3.1.dist-info → notionary-0.4.1.dist-info}/METADATA +1 -1
  190. notionary-0.4.1.dist-info/RECORD +236 -0
  191. notionary/file_upload/models.py +0 -69
  192. notionary/page/blocks/client.py +0 -1
  193. notionary/page/content/syntax/__init__.py +0 -4
  194. notionary/page/content/syntax/models.py +0 -66
  195. notionary/page/content/syntax/registry.py +0 -393
  196. notionary/page/page_context.py +0 -50
  197. notionary/shared/models/cover.py +0 -20
  198. notionary-0.3.1.dist-info/RECORD +0 -211
  199. /notionary/page/content/syntax/{grammar.py → definition/grammar.py} +0 -0
  200. {notionary-0.3.1.dist-info → notionary-0.4.1.dist-info}/WHEEL +0 -0
  201. {notionary-0.3.1.dist-info → notionary-0.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,12 +1,16 @@
1
1
  import random
2
2
  from abc import ABC, abstractmethod
3
3
  from collections.abc import Sequence
4
- from typing import Self
4
+ from pathlib import Path
5
+ from typing import Self, cast
5
6
 
6
- from notionary.shared.entity.entity_metadata_update_client import EntityMetadataUpdateClient
7
+ from notionary.file_upload.service import NotionFileUpload
8
+ from notionary.shared.entity.entity_metadata_update_client import (
9
+ EntityMetadataUpdateClient,
10
+ )
7
11
  from notionary.shared.entity.schemas import EntityResponseDto
8
- from notionary.shared.models.cover import CoverType
9
- from notionary.shared.models.icon import IconType
12
+ from notionary.shared.models.file import ExternalFile, FileType, NotionHostedFile
13
+ from notionary.shared.models.icon import EmojiIcon, IconType
10
14
  from notionary.shared.models.parent import ParentType
11
15
  from notionary.user.base import BaseUser
12
16
  from notionary.user.service import UserService
@@ -19,6 +23,7 @@ class Entity(LoggingMixin, ABC):
19
23
  self,
20
24
  dto: EntityResponseDto,
21
25
  user_service: UserService | None = None,
26
+ file_upload_service: NotionFileUpload | None = None,
22
27
  ) -> None:
23
28
  self._id = dto.id
24
29
  self._created_time = dto.created_time
@@ -35,6 +40,7 @@ class Entity(LoggingMixin, ABC):
35
40
  self._cover_image_url = self._extract_cover_image_url(dto)
36
41
 
37
42
  self._user_service = user_service or UserService()
43
+ self._file_upload_service = file_upload_service or NotionFileUpload()
38
44
 
39
45
  @staticmethod
40
46
  def _extract_emoji_icon(dto: EntityResponseDto) -> str | None:
@@ -43,25 +49,36 @@ class Entity(LoggingMixin, ABC):
43
49
  if dto.icon.type is not IconType.EMOJI:
44
50
  return None
45
51
 
46
- return dto.icon.emoji
52
+ emoji_icon = cast(EmojiIcon, dto.icon)
53
+ return emoji_icon.emoji
47
54
 
48
55
  @staticmethod
49
56
  def _extract_external_icon_url(dto: EntityResponseDto) -> str | None:
50
57
  if dto.icon is None:
51
58
  return None
52
- if dto.icon.type is not IconType.EXTERNAL:
53
- return None
54
59
 
55
- return dto.icon.external.url
60
+ if dto.icon.type == IconType.EXTERNAL:
61
+ external_icon = cast(ExternalFile, dto.icon)
62
+ return external_icon.external.url
63
+ elif dto.icon.type == IconType.FILE:
64
+ notion_file_icon = cast(NotionHostedFile, dto.icon)
65
+ return notion_file_icon.file.url
66
+
67
+ return None
56
68
 
57
69
  @staticmethod
58
70
  def _extract_cover_image_url(dto: EntityResponseDto) -> str | None:
59
71
  if dto.cover is None:
60
72
  return None
61
- if dto.cover.type is not CoverType.EXTERNAL:
62
- return None
63
73
 
64
- return dto.cover.external.url
74
+ if dto.cover.type == FileType.EXTERNAL:
75
+ external_cover = cast(ExternalFile, dto.cover)
76
+ return external_cover.external.url
77
+ elif dto.cover.type == FileType.FILE:
78
+ notion_file_cover = cast(NotionHostedFile, dto.cover)
79
+ return notion_file_cover.file.url
80
+
81
+ return None
65
82
 
66
83
  @classmethod
67
84
  @abstractmethod
@@ -82,10 +99,7 @@ class Entity(LoggingMixin, ABC):
82
99
 
83
100
  @property
84
101
  @abstractmethod
85
- def _entity_metadata_update_client(self) -> EntityMetadataUpdateClient:
86
- # functionality for updating properties like title, icon, cover, archive status depends on interface for template like implementation
87
- # has to be implementated by inheritants to correctly use the methods below
88
- ...
102
+ def _entity_metadata_update_client(self) -> EntityMetadataUpdateClient: ...
89
103
 
90
104
  @property
91
105
  def id(self) -> str:
@@ -123,10 +137,6 @@ class Entity(LoggingMixin, ABC):
123
137
  def public_url(self) -> str | None:
124
138
  return self._public_url
125
139
 
126
- # =========================================================================
127
- # Parent ID Getters
128
- # =========================================================================
129
-
130
140
  def get_parent_database_id_if_present(self) -> str | None:
131
141
  if self._parent.type == ParentType.DATABASE_ID:
132
142
  return self._parent.database_id
@@ -150,27 +160,48 @@ class Entity(LoggingMixin, ABC):
150
160
  def is_workspace_parent(self) -> bool:
151
161
  return self._parent.type == ParentType.WORKSPACE
152
162
 
153
- # =========================================================================
154
- # User Methods
155
- # =========================================================================
156
-
157
163
  async def get_created_by_user(self) -> BaseUser | None:
158
164
  return await self._user_service.get_user_by_id(self._created_by.id)
159
165
 
160
166
  async def get_last_edited_by_user(self) -> BaseUser | None:
161
167
  return await self._user_service.get_user_by_id(self._last_edited_by.id)
162
168
 
163
- # =========================================================================
164
- # Icon & Cover Methods
165
- # =========================================================================
166
-
167
169
  async def set_emoji_icon(self, emoji: str) -> None:
168
- entity_response = await self._entity_metadata_update_client.patch_emoji_icon(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
+ )
180
+ self._emoji_icon = None
181
+ self._external_icon_url = self._extract_external_icon_url(entity_response)
182
+
183
+ async def set_icon_from_file(
184
+ self, file_path: Path, filename: str | None = None
185
+ ) -> None:
186
+ upload_response = await self._file_upload_service.upload_file(
187
+ file_path, filename
188
+ )
189
+ await self.set_icon_from_file_upload(upload_response.id)
190
+
191
+ async def set_icon_from_bytes(
192
+ self, file_content: bytes, filename: str, content_type: str | None = None
193
+ ) -> None:
194
+ upload_response = await self._file_upload_service.upload_from_bytes(
195
+ file_content, filename, content_type
196
+ )
197
+ await self.set_icon_from_file_upload(upload_response.id)
198
+
199
+ async def set_icon_from_file_upload(self, file_upload_id: str) -> None:
200
+ entity_response = (
201
+ await self._entity_metadata_update_client.patch_icon_from_file_upload(
202
+ file_upload_id
203
+ )
204
+ )
174
205
  self._emoji_icon = None
175
206
  self._external_icon_url = self._extract_external_icon_url(entity_response)
176
207
 
@@ -180,7 +211,33 @@ class Entity(LoggingMixin, ABC):
180
211
  self._external_icon_url = None
181
212
 
182
213
  async def set_cover_image_by_url(self, image_url: str) -> None:
183
- entity_response = 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
+ )
217
+ self._cover_image_url = self._extract_cover_image_url(entity_response)
218
+
219
+ async def set_cover_image_from_file(
220
+ self, file_path: Path, filename: str | None = None
221
+ ) -> None:
222
+ upload_response = await self._file_upload_service.upload_file(
223
+ file_path, filename
224
+ )
225
+ await self.set_cover_image_from_file_upload(upload_response.id)
226
+
227
+ async def set_cover_image_from_bytes(
228
+ self, file_content: bytes, filename: str, content_type: str | None = None
229
+ ) -> None:
230
+ upload_response = await self._file_upload_service.upload_from_bytes(
231
+ file_content, filename, content_type
232
+ )
233
+ await self.set_cover_image_from_file_upload(upload_response.id)
234
+
235
+ async def set_cover_image_from_file_upload(self, file_upload_id: str) -> None:
236
+ entity_response = (
237
+ await self._entity_metadata_update_client.patch_cover_from_file_upload(
238
+ file_upload_id
239
+ )
240
+ )
184
241
  self._cover_image_url = self._extract_cover_image_url(entity_response)
185
242
 
186
243
  async def set_random_gradient_cover(self) -> None:
@@ -191,10 +248,6 @@ class Entity(LoggingMixin, ABC):
191
248
  await self._entity_metadata_update_client.remove_cover()
192
249
  self._cover_image_url = None
193
250
 
194
- # =========================================================================
195
- # Trash Methods
196
- # =========================================================================
197
-
198
251
  async def move_to_trash(self) -> None:
199
252
  if self._in_trash:
200
253
  self.logger.warning("Entity is already in trash.")
@@ -223,7 +276,8 @@ class Entity(LoggingMixin, ABC):
223
276
 
224
277
  def _get_random_gradient_cover(self) -> str:
225
278
  DEFAULT_NOTION_COVERS: Sequence[str] = [
226
- f"https://www.notion.so/images/page-cover/gradients_{i}.png" 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)
227
281
  ]
228
282
 
229
283
  return random.choice(DEFAULT_NOTION_COVERS)
@@ -1,21 +1,51 @@
1
1
  from enum import StrEnum
2
- from typing import Literal, Self
2
+ from typing import Annotated, Literal, Self
3
3
 
4
- from pydantic import BaseModel
4
+ from pydantic import BaseModel, Field
5
5
 
6
6
 
7
7
  class FileType(StrEnum):
8
8
  EXTERNAL = "external"
9
+ FILE = "file"
10
+ FILE_UPLOAD = "file_upload"
9
11
 
10
12
 
11
- class ExternalFile(BaseModel):
13
+ class ExternalFileData(BaseModel):
12
14
  url: str
13
15
 
14
16
 
15
- class ExternalRessource(BaseModel):
17
+ class ExternalFile(BaseModel):
16
18
  type: Literal[FileType.EXTERNAL] = FileType.EXTERNAL
17
- external: ExternalFile
19
+ external: ExternalFileData
18
20
 
19
21
  @classmethod
20
22
  def from_url(cls, url: str) -> Self:
21
- return cls(external=ExternalFile(url=url))
23
+ return cls(external=ExternalFileData(url=url))
24
+
25
+
26
+ class NotionHostedFileData(BaseModel):
27
+ url: str
28
+ expiry_time: str
29
+
30
+
31
+ class NotionHostedFile(BaseModel):
32
+ type: Literal[FileType.FILE] = FileType.FILE
33
+ file: NotionHostedFileData
34
+
35
+
36
+ class FileUploadedFileData(BaseModel):
37
+ id: str
38
+
39
+
40
+ class FileUploadFile(BaseModel):
41
+ type: Literal[FileType.FILE_UPLOAD] = FileType.FILE_UPLOAD
42
+ file_upload: FileUploadedFileData
43
+
44
+ @classmethod
45
+ def from_id(cls, id: str) -> Self:
46
+ return cls(file_upload=FileUploadedFileData(id=id))
47
+
48
+
49
+ type File = Annotated[
50
+ ExternalFile | NotionHostedFile | FileUploadFile, Field(discriminator="type")
51
+ ]
@@ -1,14 +1,16 @@
1
1
  from enum import StrEnum
2
- from typing import Literal, Self
2
+ from typing import Literal
3
3
 
4
4
  from pydantic import BaseModel
5
5
 
6
- from notionary.shared.models.file import ExternalFile
6
+ from notionary.shared.models.file import File
7
7
 
8
8
 
9
9
  class IconType(StrEnum):
10
10
  EMOJI = "emoji"
11
11
  EXTERNAL = "external"
12
+ FILE = "file"
13
+ FILE_UPLOAD = "file_upload"
12
14
 
13
15
 
14
16
  class EmojiIcon(BaseModel):
@@ -16,13 +18,4 @@ class EmojiIcon(BaseModel):
16
18
  emoji: str
17
19
 
18
20
 
19
- class ExternalIcon(BaseModel):
20
- type: Literal[IconType.EXTERNAL] = IconType.EXTERNAL
21
- external: ExternalFile
22
-
23
- @classmethod
24
- def from_url(cls, url: str) -> Self:
25
- return cls(external=ExternalFile(url=url))
26
-
27
-
28
- Icon = EmojiIcon | ExternalIcon
21
+ type Icon = EmojiIcon | File
notionary/user/base.py CHANGED
@@ -54,7 +54,9 @@ class BaseUser:
54
54
 
55
55
  expected_type = cls._get_expected_user_type()
56
56
  if user_dto.type != expected_type:
57
- raise ValueError(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,9 +23,23 @@ class BotUser(BaseUser):
17
23
  ) -> None:
18
24
  super().__init__(id=id, name=name, avatar_url=avatar_url)
19
25
  self._workspace_name = workspace_name
20
- self._workspace_file_upload_limit_in_bytes = workspace_file_upload_limit_in_bytes
26
+ self._workspace_file_upload_limit_in_bytes = (
27
+ workspace_file_upload_limit_in_bytes
28
+ )
21
29
  self._owner_type = owner_type
22
30
 
31
+ @property
32
+ def workspace_name(self) -> str | None:
33
+ return self._workspace_name
34
+
35
+ @property
36
+ def workspace_file_upload_limit_in_bytes(self) -> int:
37
+ return self._workspace_file_upload_limit_in_bytes
38
+
39
+ @property
40
+ def owner_type(self) -> WorkspaceOwnerType | None:
41
+ return self._owner_type
42
+
23
43
  @classmethod
24
44
  def _get_expected_user_type(cls) -> UserType:
25
45
  return UserType.BOT
@@ -54,17 +74,5 @@ class BotUser(BaseUser):
54
74
  owner_type=owner_type,
55
75
  )
56
76
 
57
- @property
58
- def workspace_name(self) -> str | None:
59
- return self._workspace_name
60
-
61
- @property
62
- def workspace_file_upload_limit_in_bytes(self) -> int:
63
- return self._workspace_file_upload_limit_in_bytes
64
-
65
- @property
66
- def owner_type(self) -> WorkspaceOwnerType | None:
67
- return self._owner_type
68
-
69
77
  def __repr__(self) -> str:
70
78
  return f"BotUser(id={self._id!r}, name={self._name!r}, avatar_url={self._avatar_url!r}, workspace_name={self._workspace_name!r}, workspace_file_upload_limit_in_bytes={self._workspace_file_upload_limit_in_bytes!r}, owner_type={self._owner_type!r})"
notionary/user/client.py CHANGED
@@ -17,7 +17,9 @@ class UserHttpClient(NotionHttpClient):
17
17
  return adapter.validate_python(response)
18
18
 
19
19
  async def get_all_workspace_users(self) -> list[UserResponseDto]:
20
- all_entities = await paginate_notion_api(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:
@@ -9,10 +9,10 @@ P = ParamSpec("P")
9
9
  R = TypeVar("R")
10
10
  T = TypeVar("T")
11
11
 
12
- type SyncFunc = Callable[P, R]
13
- type AsyncFunc = Callable[P, Coroutine[Any, Any, R]]
14
- type SyncDecorator = Callable[[SyncFunc], SyncFunc]
15
- type AsyncDecorator = Callable[[AsyncFunc], AsyncFunc]
12
+ type _SyncFunc = Callable[P, R]
13
+ type _AsyncFunc = Callable[P, Coroutine[Any, Any, R]]
14
+ type _SyncDecorator = Callable[[_SyncFunc], _SyncFunc]
15
+ type _AsyncDecorator = Callable[[_AsyncFunc], _AsyncFunc]
16
16
 
17
17
 
18
18
  def singleton(cls):
@@ -26,8 +26,10 @@ def singleton(cls):
26
26
  return wrapper
27
27
 
28
28
 
29
- def time_execution_sync(additional_text: str = "", min_duration_to_log: float = 0.25) -> SyncDecorator:
30
- def decorator(func: SyncFunc) -> SyncFunc:
29
+ def time_execution_sync(
30
+ additional_text: str = "", min_duration_to_log: float = 0.25
31
+ ) -> _SyncDecorator:
32
+ def decorator(func: _SyncFunc) -> _SyncFunc:
31
33
  @functools.wraps(func)
32
34
  def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
33
35
  start_time = time.perf_counter()
@@ -49,8 +51,8 @@ def time_execution_sync(additional_text: str = "", min_duration_to_log: float =
49
51
  def time_execution_async(
50
52
  additional_text: str = "",
51
53
  min_duration_to_log: float = 0.25,
52
- ) -> AsyncDecorator:
53
- def decorator(func: AsyncFunc) -> AsyncFunc:
54
+ ) -> _AsyncDecorator:
55
+ def decorator(func: _AsyncFunc) -> _AsyncFunc:
54
56
  @functools.wraps(func)
55
57
  async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
56
58
  start_time = time.perf_counter()
@@ -106,7 +108,9 @@ def async_retry(
106
108
  except Exception as e:
107
109
  last_exception = e
108
110
 
109
- if retry_on_exceptions is not None and not isinstance(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
- from .query import NotionWorkspaceQueryConfigBuilder
1
+ from .query import NotionWorkspaceQueryConfigBuilder, WorkspaceQueryConfig
2
2
  from .service import NotionWorkspace
3
3
 
4
- __all__ = ["NotionWorkspace", "NotionWorkspaceQueryConfigBuilder"]
4
+ __all__ = [
5
+ "NotionWorkspace",
6
+ "NotionWorkspaceQueryConfigBuilder",
7
+ "WorkspaceQueryConfig",
8
+ ]
@@ -1,3 +1,4 @@
1
1
  from .builder import NotionWorkspaceQueryConfigBuilder
2
+ from .models import WorkspaceQueryConfig
2
3
 
3
- __all__ = ["NotionWorkspaceQueryConfigBuilder"]
4
+ __all__ = ["NotionWorkspaceQueryConfigBuilder", "WorkspaceQueryConfig"]