notionary 0.3.1__py3-none-any.whl → 0.4.0__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 (80) hide show
  1. notionary/__init__.py +6 -1
  2. notionary/blocks/enums.py +0 -6
  3. notionary/blocks/schemas.py +32 -78
  4. notionary/comments/schemas.py +2 -29
  5. notionary/data_source/properties/schemas.py +128 -107
  6. notionary/data_source/schemas.py +2 -2
  7. notionary/data_source/service.py +32 -23
  8. notionary/database/schemas.py +2 -2
  9. notionary/database/service.py +3 -5
  10. notionary/exceptions/__init__.py +6 -2
  11. notionary/exceptions/api.py +2 -2
  12. notionary/exceptions/base.py +1 -1
  13. notionary/exceptions/block_parsing.py +3 -3
  14. notionary/exceptions/data_source/builder.py +2 -2
  15. notionary/exceptions/data_source/properties.py +3 -3
  16. notionary/exceptions/file_upload.py +67 -0
  17. notionary/exceptions/properties.py +4 -4
  18. notionary/exceptions/search.py +4 -4
  19. notionary/file_upload/__init__.py +4 -0
  20. notionary/file_upload/client.py +124 -210
  21. notionary/file_upload/config/__init__.py +17 -0
  22. notionary/file_upload/config/config.py +32 -0
  23. notionary/file_upload/config/constants.py +16 -0
  24. notionary/file_upload/file/reader.py +28 -0
  25. notionary/file_upload/query/__init__.py +7 -0
  26. notionary/file_upload/query/builder.py +54 -0
  27. notionary/file_upload/query/models.py +37 -0
  28. notionary/file_upload/schemas.py +78 -0
  29. notionary/file_upload/service.py +152 -289
  30. notionary/file_upload/validation/factory.py +64 -0
  31. notionary/file_upload/validation/impl/file_name_length.py +23 -0
  32. notionary/file_upload/validation/models.py +124 -0
  33. notionary/file_upload/validation/port.py +7 -0
  34. notionary/file_upload/validation/service.py +17 -0
  35. notionary/file_upload/validation/validators/__init__.py +11 -0
  36. notionary/file_upload/validation/validators/file_exists.py +15 -0
  37. notionary/file_upload/validation/validators/file_extension.py +122 -0
  38. notionary/file_upload/validation/validators/file_name_length.py +21 -0
  39. notionary/file_upload/validation/validators/upload_limit.py +31 -0
  40. notionary/http/client.py +6 -22
  41. notionary/page/content/parser/factory.py +8 -5
  42. notionary/page/content/parser/parsers/audio.py +8 -33
  43. notionary/page/content/parser/parsers/embed.py +0 -2
  44. notionary/page/content/parser/parsers/file.py +8 -35
  45. notionary/page/content/parser/parsers/file_like_block.py +89 -0
  46. notionary/page/content/parser/parsers/image.py +8 -35
  47. notionary/page/content/parser/parsers/pdf.py +8 -35
  48. notionary/page/content/parser/parsers/video.py +8 -35
  49. notionary/page/content/renderer/renderers/audio.py +9 -21
  50. notionary/page/content/renderer/renderers/file.py +9 -21
  51. notionary/page/content/renderer/renderers/file_like_block.py +43 -0
  52. notionary/page/content/renderer/renderers/image.py +9 -21
  53. notionary/page/content/renderer/renderers/pdf.py +9 -21
  54. notionary/page/content/renderer/renderers/video.py +9 -21
  55. notionary/page/content/syntax/__init__.py +2 -1
  56. notionary/page/content/syntax/registry.py +38 -60
  57. notionary/page/properties/client.py +1 -1
  58. notionary/page/properties/{models.py → schemas.py} +93 -107
  59. notionary/page/properties/service.py +1 -1
  60. notionary/page/schemas.py +3 -3
  61. notionary/page/service.py +1 -1
  62. notionary/shared/entity/dto_parsers.py +1 -36
  63. notionary/shared/entity/entity_metadata_update_client.py +18 -4
  64. notionary/shared/entity/schemas.py +6 -6
  65. notionary/shared/entity/service.py +53 -30
  66. notionary/shared/models/file.py +34 -6
  67. notionary/shared/models/icon.py +5 -12
  68. notionary/user/bot.py +12 -12
  69. notionary/utils/decorators.py +8 -8
  70. notionary/workspace/__init__.py +2 -2
  71. notionary/workspace/query/__init__.py +2 -1
  72. notionary/workspace/query/service.py +3 -17
  73. notionary/workspace/service.py +45 -45
  74. {notionary-0.3.1.dist-info → notionary-0.4.0.dist-info}/METADATA +1 -1
  75. {notionary-0.3.1.dist-info → notionary-0.4.0.dist-info}/RECORD +77 -58
  76. notionary/file_upload/models.py +0 -69
  77. notionary/page/page_context.py +0 -50
  78. notionary/shared/models/cover.py +0 -20
  79. {notionary-0.3.1.dist-info → notionary-0.4.0.dist-info}/WHEEL +0 -0
  80. {notionary-0.3.1.dist-info → notionary-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,10 @@
1
1
  from enum import StrEnum
2
- from typing import Annotated, Any, Literal, TypeVar
2
+ from typing import Any, Literal, TypeVar
3
3
 
4
- from pydantic import BaseModel, Field
4
+ from pydantic import BaseModel, ConfigDict, Field
5
5
 
6
6
  from notionary.blocks.rich_text.models import RichText
7
+ from notionary.shared.models.file import File
7
8
  from notionary.shared.properties.type import PropertyType
8
9
  from notionary.shared.typings import JsonDict
9
10
  from notionary.user.schemas import PersonUserResponseDto, UserResponseDto
@@ -38,38 +39,6 @@ class DateValue(BaseModel):
38
39
  time_zone: str | None = None
39
40
 
40
41
 
41
- # ============================================================================
42
- # File Models
43
- # ============================================================================
44
-
45
-
46
- class FileType(StrEnum):
47
- EXTERNAL = "external"
48
- FILE = "file"
49
-
50
-
51
- class ExternalFile(BaseModel):
52
- """External file hosted outside of Notion."""
53
-
54
- url: str
55
-
56
-
57
- class NotionFile(BaseModel):
58
- """File uploaded to Notion with expiration."""
59
-
60
- url: str
61
- expiry_time: str
62
-
63
-
64
- class FileObject(BaseModel):
65
- """File object can be external or uploaded to Notion."""
66
-
67
- name: str
68
- type: FileType
69
- external: ExternalFile | None = None
70
- file: NotionFile | None = None
71
-
72
-
73
42
  # ============================================================================
74
43
  # Formula Models
75
44
  # ============================================================================
@@ -142,25 +111,9 @@ class VerificationValue(BaseModel):
142
111
  # ============================================================================
143
112
 
144
113
 
145
- class PageStatusProperty(PageProperty):
146
- type: Literal[PropertyType.STATUS] = PropertyType.STATUS
147
- status: StatusOption | None = None
148
- options: list[StatusOption] = Field(default_factory=list)
149
-
150
- @property
151
- def option_names(self) -> list[str]:
152
- return [option.name for option in self.options]
153
-
154
-
155
- class PageRelationProperty(PageProperty):
156
- type: Literal[PropertyType.RELATION] = PropertyType.RELATION
157
- relation: list[RelationItem] = Field(default_factory=list)
158
- has_more: bool = False
159
-
160
-
161
- class PageURLProperty(PageProperty):
162
- type: Literal[PropertyType.URL] = PropertyType.URL
163
- url: str | None = None
114
+ class PageTitleProperty(PageProperty):
115
+ type: Literal[PropertyType.TITLE] = PropertyType.TITLE
116
+ title: list[RichText] = Field(default_factory=list)
164
117
 
165
118
 
166
119
  class PageRichTextProperty(PageProperty):
@@ -168,6 +121,16 @@ class PageRichTextProperty(PageProperty):
168
121
  rich_text: list[RichText] = Field(default_factory=list)
169
122
 
170
123
 
124
+ class PageSelectProperty(PageProperty):
125
+ type: Literal[PropertyType.SELECT] = PropertyType.SELECT
126
+ select: SelectOption | None = None
127
+ options: list[SelectOption] = Field(default_factory=list)
128
+
129
+ @property
130
+ def option_names(self) -> list[str]:
131
+ return [option.name for option in self.options]
132
+
133
+
171
134
  class PageMultiSelectProperty(PageProperty):
172
135
  type: Literal[PropertyType.MULTI_SELECT] = PropertyType.MULTI_SELECT
173
136
  multi_select: list[SelectOption] = Field(default_factory=list)
@@ -178,19 +141,19 @@ class PageMultiSelectProperty(PageProperty):
178
141
  return [option.name for option in self.options]
179
142
 
180
143
 
181
- class PageSelectProperty(PageProperty):
182
- type: Literal[PropertyType.SELECT] = PropertyType.SELECT
183
- select: SelectOption | None = None
184
- options: list[SelectOption] = Field(default_factory=list)
144
+ class PageStatusProperty(PageProperty):
145
+ type: Literal[PropertyType.STATUS] = PropertyType.STATUS
146
+ status: StatusOption | None = None
147
+ options: list[StatusOption] = Field(default_factory=list)
185
148
 
186
149
  @property
187
150
  def option_names(self) -> list[str]:
188
151
  return [option.name for option in self.options]
189
152
 
190
153
 
191
- class PagePeopleProperty(PageProperty):
192
- type: Literal[PropertyType.PEOPLE] = PropertyType.PEOPLE
193
- people: list[PersonUserResponseDto] = Field(default_factory=list)
154
+ class PageNumberProperty(PageProperty):
155
+ type: Literal[PropertyType.NUMBER] = PropertyType.NUMBER
156
+ number: float | None = None
194
157
 
195
158
 
196
159
  class PageDateProperty(PageProperty):
@@ -198,21 +161,16 @@ class PageDateProperty(PageProperty):
198
161
  date: DateValue | None = None
199
162
 
200
163
 
201
- class PageTitleProperty(PageProperty):
202
- type: Literal[PropertyType.TITLE] = PropertyType.TITLE
203
- title: list[RichText] = Field(default_factory=list)
204
-
205
-
206
- class PageNumberProperty(PageProperty):
207
- type: Literal[PropertyType.NUMBER] = PropertyType.NUMBER
208
- number: float | None = None
209
-
210
-
211
164
  class PageCheckboxProperty(PageProperty):
212
165
  type: Literal[PropertyType.CHECKBOX] = PropertyType.CHECKBOX
213
166
  checkbox: bool = False
214
167
 
215
168
 
169
+ class PageURLProperty(PageProperty):
170
+ type: Literal[PropertyType.URL] = PropertyType.URL
171
+ url: str | None = None
172
+
173
+
216
174
  class PageEmailProperty(PageProperty):
217
175
  type: Literal[PropertyType.EMAIL] = PropertyType.EMAIL
218
176
  email: str | None = None
@@ -223,29 +181,34 @@ class PagePhoneNumberProperty(PageProperty):
223
181
  phone_number: str | None = None
224
182
 
225
183
 
226
- class PageCreatedTimeProperty(PageProperty):
227
- type: Literal[PropertyType.CREATED_TIME] = PropertyType.CREATED_TIME
228
- created_time: str # ISO 8601 datetime - read-only
229
-
230
-
231
- class PageLastEditedTimeProperty(PageProperty):
232
- type: Literal[PropertyType.LAST_EDITED_TIME] = PropertyType.LAST_EDITED_TIME
233
- last_edited_time: str # ISO 8601 datetime - read-only
184
+ class PagePeopleProperty(PageProperty):
185
+ type: Literal[PropertyType.PEOPLE] = PropertyType.PEOPLE
186
+ people: list[PersonUserResponseDto] = Field(default_factory=list)
234
187
 
235
188
 
236
189
  class PageCreatedByProperty(PageProperty):
237
190
  type: Literal[PropertyType.CREATED_BY] = PropertyType.CREATED_BY
238
- created_by: UserResponseDto # User object - read-only
191
+ created_by: UserResponseDto
239
192
 
240
193
 
241
194
  class PageLastEditedByProperty(PageProperty):
242
195
  type: Literal[PropertyType.LAST_EDITED_BY] = PropertyType.LAST_EDITED_BY
243
- last_edited_by: UserResponseDto # User object - read-only
196
+ last_edited_by: UserResponseDto
244
197
 
245
198
 
246
- class PageFilesProperty(PageProperty):
247
- type: Literal[PropertyType.FILES] = PropertyType.FILES
248
- files: list[FileObject] = Field(default_factory=list)
199
+ class PageCreatedTimeProperty(PageProperty):
200
+ type: Literal[PropertyType.CREATED_TIME] = PropertyType.CREATED_TIME
201
+ created_time: str
202
+
203
+
204
+ class PageLastEditedTimeProperty(PageProperty):
205
+ type: Literal[PropertyType.LAST_EDITED_TIME] = PropertyType.LAST_EDITED_TIME
206
+ last_edited_time: str
207
+
208
+
209
+ class PageLastVisitedTimeProperty(PageProperty):
210
+ type: Literal[PropertyType.LAST_VISITED_TIME] = PropertyType.LAST_VISITED_TIME
211
+ last_visited_time: str | None = None
249
212
 
250
213
 
251
214
  class PageFormulaProperty(PageProperty):
@@ -258,14 +221,15 @@ class PageRollupProperty(PageProperty):
258
221
  rollup: RollupValue
259
222
 
260
223
 
261
- class PageUniqueIdProperty(PageProperty):
262
- type: Literal[PropertyType.UNIQUE_ID] = PropertyType.UNIQUE_ID
263
- unique_id: UniqueIdValue
224
+ class PageFilesProperty(PageProperty):
225
+ type: Literal[PropertyType.FILES] = PropertyType.FILES
226
+ files: list[File] = Field(default_factory=list)
264
227
 
265
228
 
266
- class PageVerificationProperty(PageProperty):
267
- type: Literal[PropertyType.VERIFICATION] = PropertyType.VERIFICATION
268
- verification: VerificationValue
229
+ class PageRelationProperty(PageProperty):
230
+ type: Literal[PropertyType.RELATION] = PropertyType.RELATION
231
+ relation: list[RelationItem] = Field(default_factory=list)
232
+ has_more: bool = False
269
233
 
270
234
 
271
235
  class PageButtonProperty(PageProperty):
@@ -273,36 +237,58 @@ class PageButtonProperty(PageProperty):
273
237
  button: JsonDict = Field(default_factory=dict)
274
238
 
275
239
 
276
- # ============================================================================
277
- # Discriminated Union
278
- # ============================================================================
240
+ class PageLocationProperty(PageProperty):
241
+ type: Literal[PropertyType.LOCATION] = PropertyType.LOCATION
242
+ location: JsonDict | None = None
279
243
 
280
244
 
281
- DiscriminatedPageProperty = Annotated[
282
- PageStatusProperty
283
- | PageRelationProperty
284
- | PageURLProperty
245
+ class PagePlaceProperty(PageProperty):
246
+ type: Literal[PropertyType.PLACE] = PropertyType.PLACE
247
+ place: JsonDict | None = None
248
+
249
+
250
+ class PageVerificationProperty(PageProperty):
251
+ type: Literal[PropertyType.VERIFICATION] = PropertyType.VERIFICATION
252
+ verification: VerificationValue
253
+
254
+
255
+ class PageUniqueIdProperty(PageProperty):
256
+ type: Literal[PropertyType.UNIQUE_ID] = PropertyType.UNIQUE_ID
257
+ unique_id: UniqueIdValue
258
+
259
+
260
+ class PageUnknownProperty(PageProperty):
261
+ model_config = ConfigDict(extra="allow")
262
+
263
+
264
+ type AnyPageProperty = (
265
+ PageTitleProperty
285
266
  | PageRichTextProperty
286
- | PageMultiSelectProperty
287
267
  | PageSelectProperty
288
- | PagePeopleProperty
289
- | PageDateProperty
290
- | PageTitleProperty
268
+ | PageMultiSelectProperty
269
+ | PageStatusProperty
291
270
  | PageNumberProperty
271
+ | PageDateProperty
292
272
  | PageCheckboxProperty
273
+ | PageURLProperty
293
274
  | PageEmailProperty
294
275
  | PagePhoneNumberProperty
295
- | PageCreatedTimeProperty
296
- | PageLastEditedTimeProperty
276
+ | PagePeopleProperty
297
277
  | PageCreatedByProperty
298
278
  | PageLastEditedByProperty
299
- | PageFilesProperty
279
+ | PageCreatedTimeProperty
280
+ | PageLastEditedTimeProperty
281
+ | PageLastVisitedTimeProperty
300
282
  | PageFormulaProperty
301
283
  | PageRollupProperty
302
- | PageUniqueIdProperty
284
+ | PageFilesProperty
285
+ | PageRelationProperty
286
+ | PageButtonProperty
287
+ | PageLocationProperty
288
+ | PagePlaceProperty
303
289
  | PageVerificationProperty
304
- | PageButtonProperty,
305
- Field(discriminator="type"),
306
- ]
290
+ | PageUniqueIdProperty
291
+ | PageUnknownProperty
292
+ )
307
293
 
308
294
  PagePropertyT = TypeVar("PagePropertyT", bound=PageProperty)
@@ -10,7 +10,7 @@ from notionary.exceptions.properties import (
10
10
  PagePropertyTypeError,
11
11
  )
12
12
  from notionary.page.properties.client import PagePropertyHttpClient
13
- from notionary.page.properties.models import (
13
+ from notionary.page.properties.schemas import (
14
14
  PageCheckboxProperty,
15
15
  PageCreatedTimeProperty,
16
16
  PageDateProperty,
notionary/page/schemas.py CHANGED
@@ -1,13 +1,13 @@
1
1
  from pydantic import BaseModel
2
2
 
3
- from notionary.page.properties.models import DiscriminatedPageProperty
3
+ from notionary.page.properties.schemas import AnyPageProperty
4
4
  from notionary.shared.entity.schemas import EntityResponseDto
5
5
 
6
6
 
7
7
  class NotionPageDto(EntityResponseDto):
8
8
  archived: bool
9
- properties: dict[str, DiscriminatedPageProperty]
9
+ properties: dict[str, AnyPageProperty]
10
10
 
11
11
 
12
12
  class PgePropertiesUpdateDto(BaseModel):
13
- properties: dict[str, DiscriminatedPageProperty]
13
+ properties: dict[str, AnyPageProperty]
notionary/page/service.py CHANGED
@@ -11,7 +11,7 @@ from notionary.page.content.service import PageContentService
11
11
  from notionary.page.page_http_client import NotionPageHttpClient
12
12
  from notionary.page.page_metadata_update_client import PageMetadataUpdateClient
13
13
  from notionary.page.properties.factory import PagePropertyHandlerFactory
14
- from notionary.page.properties.models import PageTitleProperty
14
+ from notionary.page.properties.schemas import PageTitleProperty
15
15
  from notionary.page.properties.service import PagePropertyHandler
16
16
  from notionary.page.schemas import NotionPageDto
17
17
  from notionary.shared.entity.service import Entity
@@ -1,40 +1,5 @@
1
- from typing import cast
2
-
3
1
  from notionary.blocks.rich_text.rich_text_markdown_converter import RichTextToMarkdownConverter
4
- from notionary.shared.entity.schemas import Describable, EntityResponseDto, Titled
5
- from notionary.shared.models.cover import CoverType
6
- from notionary.shared.models.icon import IconType
7
- from notionary.shared.models.parent import DatabaseParent, DataSourceParent, ParentType
8
-
9
-
10
- def extract_emoji_icon_from_dto(entity_dto: EntityResponseDto) -> str | None:
11
- if not entity_dto.icon or entity_dto.icon.type != IconType.EMOJI:
12
- return None
13
- return entity_dto.icon.emoji
14
-
15
-
16
- def extract_external_icon_url_from_dto(entity_dto: EntityResponseDto) -> str | None:
17
- if not entity_dto.icon or entity_dto.icon.type != IconType.EXTERNAL:
18
- return None
19
- return entity_dto.icon.external.url if entity_dto.icon.external else None
20
-
21
-
22
- def extract_cover_image_url_from_dto(entity_dto: EntityResponseDto) -> str | None:
23
- if not entity_dto.cover or entity_dto.cover.type != CoverType.EXTERNAL:
24
- return None
25
- return entity_dto.cover.external.url if entity_dto.cover.external else None
26
-
27
-
28
- def extract_database_id(entity_dto: EntityResponseDto) -> str | None:
29
- if entity_dto.parent.type == ParentType.DATA_SOURCE_ID:
30
- data_source_parent = cast(DataSourceParent, entity_dto.parent)
31
- return data_source_parent.database_id if data_source_parent else None
32
-
33
- if entity_dto.parent.type == ParentType.DATABASE_ID:
34
- database_parent = cast(DatabaseParent, entity_dto.parent)
35
- return database_parent.database_id if database_parent else None
36
-
37
- return None
2
+ from notionary.shared.entity.schemas import Describable, Titled
38
3
 
39
4
 
40
5
  async def extract_title(
@@ -1,8 +1,8 @@
1
1
  from abc import ABC, abstractmethod
2
2
 
3
3
  from notionary.shared.entity.schemas import EntityResponseDto, NotionEntityUpdateDto
4
- from notionary.shared.models.cover import NotionCover
5
- from notionary.shared.models.icon import EmojiIcon, ExternalIcon
4
+ from notionary.shared.models.file import ExternalFile, FileUploadFile
5
+ from notionary.shared.models.icon import EmojiIcon, Icon
6
6
 
7
7
 
8
8
  class EntityMetadataUpdateClient(ABC):
@@ -15,7 +15,14 @@ class EntityMetadataUpdateClient(ABC):
15
15
  return await self.patch_metadata(update_dto)
16
16
 
17
17
  async def patch_external_icon(self, icon_url: str) -> EntityResponseDto:
18
- icon = ExternalIcon.from_url(icon_url)
18
+ icon = ExternalFile.from_url(icon_url)
19
+ return await self._patch_icon(icon)
20
+
21
+ async def patch_icon_from_file_upload(self, file_upload_id: str) -> EntityResponseDto:
22
+ icon = FileUploadFile.from_id(id=file_upload_id)
23
+ return await self._patch_icon(icon)
24
+
25
+ async def _patch_icon(self, icon: Icon) -> EntityResponseDto:
19
26
  update_dto = NotionEntityUpdateDto(icon=icon)
20
27
  return await self.patch_metadata(update_dto)
21
28
 
@@ -24,7 +31,14 @@ class EntityMetadataUpdateClient(ABC):
24
31
  return await self.patch_metadata(update_dto)
25
32
 
26
33
  async def patch_external_cover(self, cover_url: str) -> EntityResponseDto:
27
- cover = NotionCover.from_url(cover_url)
34
+ cover = ExternalFile.from_url(cover_url)
35
+ return await self._patch_cover(cover)
36
+
37
+ async def patch_cover_from_file_upload(self, file_upload_id: str) -> EntityResponseDto:
38
+ cover = FileUploadFile.from_id(id=file_upload_id)
39
+ return await self._patch_cover(cover)
40
+
41
+ async def _patch_cover(self, cover: Icon) -> EntityResponseDto:
28
42
  update_dto = NotionEntityUpdateDto(cover=cover)
29
43
  return await self.patch_metadata(update_dto)
30
44
 
@@ -4,26 +4,26 @@ from typing import Protocol
4
4
  from pydantic import BaseModel
5
5
 
6
6
  from notionary.blocks.rich_text.models import RichText
7
- from notionary.shared.models.cover import NotionCover
7
+ from notionary.shared.models.file import File
8
8
  from notionary.shared.models.icon import Icon
9
9
  from notionary.shared.models.parent import Parent
10
10
  from notionary.user.schemas import PartialUserDto
11
11
 
12
12
 
13
- class EntityWorkspaceQueryObjectType(StrEnum):
13
+ class _EntityType(StrEnum):
14
14
  PAGE = "page"
15
15
  DATA_SOURCE = "data_source"
16
16
  DATABASE = "database"
17
17
 
18
18
 
19
19
  class EntityResponseDto(BaseModel):
20
- object: EntityWorkspaceQueryObjectType
20
+ object: _EntityType
21
21
  id: str
22
22
  created_time: str
23
23
  created_by: PartialUserDto
24
24
  last_edited_time: str
25
25
  last_edited_by: PartialUserDto
26
- cover: NotionCover | None = None
26
+ cover: File | None = None
27
27
  icon: Icon | None = None
28
28
  parent: Parent
29
29
  in_trash: bool
@@ -32,8 +32,8 @@ class EntityResponseDto(BaseModel):
32
32
 
33
33
 
34
34
  class NotionEntityUpdateDto(BaseModel):
35
- icon: Icon | None = None
36
- cover: NotionCover | None = None
35
+ icon: File | None = None
36
+ cover: File | None = None
37
37
  in_trash: bool | None = None
38
38
 
39
39
 
@@ -1,12 +1,14 @@
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
 
7
+ from notionary.file_upload.service import NotionFileUpload
6
8
  from notionary.shared.entity.entity_metadata_update_client import EntityMetadataUpdateClient
7
9
  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.file import ExternalFile, FileType, NotionHostedFile
11
+ from notionary.shared.models.icon import EmojiIcon, IconType
10
12
  from notionary.shared.models.parent import ParentType
11
13
  from notionary.user.base import BaseUser
12
14
  from notionary.user.service import UserService
@@ -19,6 +21,7 @@ class Entity(LoggingMixin, ABC):
19
21
  self,
20
22
  dto: EntityResponseDto,
21
23
  user_service: UserService | None = None,
24
+ file_upload_service: NotionFileUpload | None = None,
22
25
  ) -> None:
23
26
  self._id = dto.id
24
27
  self._created_time = dto.created_time
@@ -35,6 +38,7 @@ class Entity(LoggingMixin, ABC):
35
38
  self._cover_image_url = self._extract_cover_image_url(dto)
36
39
 
37
40
  self._user_service = user_service or UserService()
41
+ self._file_upload_service = file_upload_service or NotionFileUpload()
38
42
 
39
43
  @staticmethod
40
44
  def _extract_emoji_icon(dto: EntityResponseDto) -> str | None:
@@ -43,25 +47,36 @@ class Entity(LoggingMixin, ABC):
43
47
  if dto.icon.type is not IconType.EMOJI:
44
48
  return None
45
49
 
46
- return dto.icon.emoji
50
+ emoji_icon = cast(EmojiIcon, dto.icon)
51
+ return emoji_icon.emoji
47
52
 
48
53
  @staticmethod
49
54
  def _extract_external_icon_url(dto: EntityResponseDto) -> str | None:
50
55
  if dto.icon is None:
51
56
  return None
52
- if dto.icon.type is not IconType.EXTERNAL:
53
- return None
54
57
 
55
- return dto.icon.external.url
58
+ if dto.icon.type == IconType.EXTERNAL:
59
+ external_icon = cast(ExternalFile, dto.icon)
60
+ return external_icon.external.url
61
+ elif dto.icon.type == IconType.FILE:
62
+ notion_file_icon = cast(NotionHostedFile, dto.icon)
63
+ return notion_file_icon.file.url
64
+
65
+ return None
56
66
 
57
67
  @staticmethod
58
68
  def _extract_cover_image_url(dto: EntityResponseDto) -> str | None:
59
69
  if dto.cover is None:
60
70
  return None
61
- if dto.cover.type is not CoverType.EXTERNAL:
62
- return None
63
71
 
64
- return dto.cover.external.url
72
+ if dto.cover.type == FileType.EXTERNAL:
73
+ external_cover = cast(ExternalFile, dto.cover)
74
+ return external_cover.external.url
75
+ elif dto.cover.type == FileType.FILE:
76
+ notion_file_cover = cast(NotionHostedFile, dto.cover)
77
+ return notion_file_cover.file.url
78
+
79
+ return None
65
80
 
66
81
  @classmethod
67
82
  @abstractmethod
@@ -82,10 +97,7 @@ class Entity(LoggingMixin, ABC):
82
97
 
83
98
  @property
84
99
  @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
- ...
100
+ def _entity_metadata_update_client(self) -> EntityMetadataUpdateClient: ...
89
101
 
90
102
  @property
91
103
  def id(self) -> str:
@@ -123,10 +135,6 @@ class Entity(LoggingMixin, ABC):
123
135
  def public_url(self) -> str | None:
124
136
  return self._public_url
125
137
 
126
- # =========================================================================
127
- # Parent ID Getters
128
- # =========================================================================
129
-
130
138
  def get_parent_database_id_if_present(self) -> str | None:
131
139
  if self._parent.type == ParentType.DATABASE_ID:
132
140
  return self._parent.database_id
@@ -150,20 +158,12 @@ class Entity(LoggingMixin, ABC):
150
158
  def is_workspace_parent(self) -> bool:
151
159
  return self._parent.type == ParentType.WORKSPACE
152
160
 
153
- # =========================================================================
154
- # User Methods
155
- # =========================================================================
156
-
157
161
  async def get_created_by_user(self) -> BaseUser | None:
158
162
  return await self._user_service.get_user_by_id(self._created_by.id)
159
163
 
160
164
  async def get_last_edited_by_user(self) -> BaseUser | None:
161
165
  return await self._user_service.get_user_by_id(self._last_edited_by.id)
162
166
 
163
- # =========================================================================
164
- # Icon & Cover Methods
165
- # =========================================================================
166
-
167
167
  async def set_emoji_icon(self, emoji: str) -> None:
168
168
  entity_response = await self._entity_metadata_update_client.patch_emoji_icon(emoji)
169
169
  self._emoji_icon = self._extract_emoji_icon(entity_response)
@@ -174,6 +174,19 @@ class Entity(LoggingMixin, ABC):
174
174
  self._emoji_icon = None
175
175
  self._external_icon_url = self._extract_external_icon_url(entity_response)
176
176
 
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)
184
+
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)
187
+ self._emoji_icon = None
188
+ self._external_icon_url = self._extract_external_icon_url(entity_response)
189
+
177
190
  async def remove_icon(self) -> None:
178
191
  await self._entity_metadata_update_client.remove_icon()
179
192
  self._emoji_icon = None
@@ -183,6 +196,20 @@ class Entity(LoggingMixin, ABC):
183
196
  entity_response = await self._entity_metadata_update_client.patch_external_cover(image_url)
184
197
  self._cover_image_url = self._extract_cover_image_url(entity_response)
185
198
 
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)
202
+
203
+ async def set_cover_image_from_bytes(
204
+ self, file_content: bytes, filename: str, content_type: str | None = None
205
+ ) -> 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)
211
+ self._cover_image_url = self._extract_cover_image_url(entity_response)
212
+
186
213
  async def set_random_gradient_cover(self) -> None:
187
214
  random_cover_url = self._get_random_gradient_cover()
188
215
  await self.set_cover_image_by_url(random_cover_url)
@@ -191,10 +218,6 @@ class Entity(LoggingMixin, ABC):
191
218
  await self._entity_metadata_update_client.remove_cover()
192
219
  self._cover_image_url = None
193
220
 
194
- # =========================================================================
195
- # Trash Methods
196
- # =========================================================================
197
-
198
221
  async def move_to_trash(self) -> None:
199
222
  if self._in_trash:
200
223
  self.logger.warning("Entity is already in trash.")