notionary 0.3.0__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 (95) hide show
  1. notionary/__init__.py +14 -2
  2. notionary/blocks/enums.py +27 -6
  3. notionary/blocks/schemas.py +32 -78
  4. notionary/comments/client.py +6 -9
  5. notionary/comments/schemas.py +2 -29
  6. notionary/data_source/http/data_source_instance_client.py +4 -4
  7. notionary/data_source/properties/schemas.py +128 -107
  8. notionary/data_source/query/__init__.py +9 -0
  9. notionary/data_source/query/builder.py +12 -3
  10. notionary/data_source/query/schema.py +5 -0
  11. notionary/data_source/schemas.py +2 -2
  12. notionary/data_source/service.py +43 -132
  13. notionary/database/schemas.py +2 -2
  14. notionary/database/service.py +19 -63
  15. notionary/exceptions/__init__.py +10 -2
  16. notionary/exceptions/api.py +2 -2
  17. notionary/exceptions/base.py +1 -1
  18. notionary/exceptions/block_parsing.py +24 -3
  19. notionary/exceptions/data_source/builder.py +2 -2
  20. notionary/exceptions/data_source/properties.py +3 -3
  21. notionary/exceptions/file_upload.py +67 -0
  22. notionary/exceptions/properties.py +4 -4
  23. notionary/exceptions/search.py +4 -4
  24. notionary/file_upload/__init__.py +4 -0
  25. notionary/file_upload/client.py +124 -210
  26. notionary/file_upload/config/__init__.py +17 -0
  27. notionary/file_upload/config/config.py +32 -0
  28. notionary/file_upload/config/constants.py +16 -0
  29. notionary/file_upload/file/reader.py +28 -0
  30. notionary/file_upload/query/__init__.py +7 -0
  31. notionary/file_upload/query/builder.py +54 -0
  32. notionary/file_upload/query/models.py +37 -0
  33. notionary/file_upload/schemas.py +78 -0
  34. notionary/file_upload/service.py +152 -289
  35. notionary/file_upload/validation/factory.py +64 -0
  36. notionary/file_upload/validation/impl/file_name_length.py +23 -0
  37. notionary/file_upload/validation/models.py +124 -0
  38. notionary/file_upload/validation/port.py +7 -0
  39. notionary/file_upload/validation/service.py +17 -0
  40. notionary/file_upload/validation/validators/__init__.py +11 -0
  41. notionary/file_upload/validation/validators/file_exists.py +15 -0
  42. notionary/file_upload/validation/validators/file_extension.py +122 -0
  43. notionary/file_upload/validation/validators/file_name_length.py +21 -0
  44. notionary/file_upload/validation/validators/upload_limit.py +31 -0
  45. notionary/http/client.py +7 -23
  46. notionary/page/content/factory.py +2 -0
  47. notionary/page/content/parser/factory.py +8 -5
  48. notionary/page/content/parser/parsers/audio.py +8 -33
  49. notionary/page/content/parser/parsers/embed.py +0 -2
  50. notionary/page/content/parser/parsers/file.py +8 -35
  51. notionary/page/content/parser/parsers/file_like_block.py +89 -0
  52. notionary/page/content/parser/parsers/image.py +8 -35
  53. notionary/page/content/parser/parsers/pdf.py +8 -35
  54. notionary/page/content/parser/parsers/video.py +8 -35
  55. notionary/page/content/parser/pre_processsing/handlers/__init__.py +2 -0
  56. notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +12 -8
  57. notionary/page/content/parser/pre_processsing/handlers/indentation.py +2 -0
  58. notionary/page/content/parser/pre_processsing/handlers/video_syntax.py +66 -0
  59. notionary/page/content/parser/pre_processsing/handlers/whitespace.py +2 -0
  60. notionary/page/content/renderer/renderers/audio.py +9 -21
  61. notionary/page/content/renderer/renderers/file.py +9 -21
  62. notionary/page/content/renderer/renderers/file_like_block.py +43 -0
  63. notionary/page/content/renderer/renderers/image.py +9 -21
  64. notionary/page/content/renderer/renderers/pdf.py +9 -21
  65. notionary/page/content/renderer/renderers/video.py +9 -21
  66. notionary/page/content/syntax/__init__.py +2 -1
  67. notionary/page/content/syntax/registry.py +38 -60
  68. notionary/page/properties/client.py +3 -3
  69. notionary/page/properties/{models.py → schemas.py} +93 -107
  70. notionary/page/properties/service.py +15 -4
  71. notionary/page/schemas.py +3 -3
  72. notionary/page/service.py +18 -79
  73. notionary/shared/entity/dto_parsers.py +1 -36
  74. notionary/shared/entity/entity_metadata_update_client.py +18 -4
  75. notionary/shared/entity/schemas.py +6 -6
  76. notionary/shared/entity/service.py +121 -40
  77. notionary/shared/models/file.py +34 -6
  78. notionary/shared/models/icon.py +5 -12
  79. notionary/user/bot.py +12 -12
  80. notionary/utils/decorators.py +8 -8
  81. notionary/utils/pagination.py +36 -32
  82. notionary/workspace/__init__.py +2 -2
  83. notionary/workspace/client.py +2 -0
  84. notionary/workspace/query/__init__.py +3 -2
  85. notionary/workspace/query/builder.py +25 -1
  86. notionary/workspace/query/models.py +9 -1
  87. notionary/workspace/query/service.py +15 -11
  88. notionary/workspace/service.py +46 -36
  89. {notionary-0.3.0.dist-info → notionary-0.4.0.dist-info}/METADATA +9 -5
  90. {notionary-0.3.0.dist-info → notionary-0.4.0.dist-info}/RECORD +92 -71
  91. notionary/file_upload/models.py +0 -69
  92. notionary/page/page_context.py +0 -50
  93. notionary/shared/models/cover.py +0 -20
  94. {notionary-0.3.0.dist-info → notionary-0.4.0.dist-info}/WHEEL +0 -0
  95. {notionary-0.3.0.dist-info → notionary-0.4.0.dist-info}/licenses/LICENSE +0 -0
notionary/page/service.py CHANGED
@@ -11,62 +11,34 @@ 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
- from notionary.shared.entity.dto_parsers import (
18
- extract_cover_image_url_from_dto,
19
- extract_emoji_icon_from_dto,
20
- extract_external_icon_url_from_dto,
21
- )
22
17
  from notionary.shared.entity.service import Entity
23
- from notionary.user.schemas import PartialUserDto
24
18
  from notionary.workspace.query.service import WorkspaceQueryService
25
19
 
26
20
 
27
21
  class NotionPage(Entity):
28
22
  def __init__(
29
23
  self,
30
- id: str,
24
+ dto: NotionPageDto,
31
25
  title: str,
32
- created_time: str,
33
- created_by: PartialUserDto,
34
- last_edited_time: str,
35
- last_edited_by: PartialUserDto,
36
- url: str,
37
- archived: bool,
38
- in_trash: bool,
39
26
  page_property_handler: PagePropertyHandler,
40
27
  block_client: NotionBlockHttpClient,
41
28
  comment_service: CommentService,
42
29
  page_content_service: PageContentService,
43
30
  metadata_update_client: PageMetadataUpdateClient,
44
- public_url: str | None = None,
45
- emoji_icon: str | None = None,
46
- external_icon_url: str | None = None,
47
- cover_image_url: str | None = None,
48
31
  ) -> None:
49
- super().__init__(
50
- id=id,
51
- created_time=created_time,
52
- created_by=created_by,
53
- last_edited_time=last_edited_time,
54
- last_edited_by=last_edited_by,
55
- in_trash=in_trash,
56
- emoji_icon=emoji_icon,
57
- external_icon_url=external_icon_url,
58
- cover_image_url=cover_image_url,
59
- )
32
+ super().__init__(dto=dto)
33
+
60
34
  self._title = title
61
- self._archived = archived
62
- self._url = url
63
- self._public_url = public_url
35
+ self._archived = dto.archived
64
36
 
65
37
  self._block_client = block_client
66
38
  self._comment_service = comment_service
67
39
  self._page_content_service = page_content_service
68
- self.properties = page_property_handler
69
40
  self._metadata_update_client = metadata_update_client
41
+ self.properties = page_property_handler
70
42
 
71
43
  @classmethod
72
44
  async def from_id(
@@ -75,8 +47,8 @@ class NotionPage(Entity):
75
47
  page_property_handler_factory: PagePropertyHandlerFactory | None = None,
76
48
  ) -> Self:
77
49
  factory = page_property_handler_factory or PagePropertyHandlerFactory()
78
- response = await cls._fetch_page_dto(page_id)
79
- return await cls._create_from_dto(response, factory)
50
+ dto = await cls._fetch_page_dto(page_id)
51
+ return await cls._create_from_dto(dto, factory)
80
52
 
81
53
  @classmethod
82
54
  async def from_title(
@@ -95,76 +67,43 @@ class NotionPage(Entity):
95
67
  @classmethod
96
68
  async def _create_from_dto(
97
69
  cls,
98
- response: NotionPageDto,
70
+ dto: NotionPageDto,
99
71
  page_property_handler_factory: PagePropertyHandlerFactory,
100
72
  ) -> Self:
101
- title_task = cls._extract_title_from_dto(response)
102
- page_property_handler = page_property_handler_factory.create_from_page_response(response)
73
+ title_task = cls._extract_title_from_dto(dto)
74
+ page_property_handler = page_property_handler_factory.create_from_page_response(dto)
103
75
 
104
76
  title = await title_task
105
77
 
106
78
  return cls._create_with_dependencies(
107
- id=response.id,
79
+ dto=dto,
108
80
  title=title,
109
- created_time=response.created_time,
110
- created_by=response.created_by,
111
- last_edited_time=response.last_edited_time,
112
- last_edited_by=response.last_edited_by,
113
- archived=response.archived,
114
- in_trash=response.in_trash,
115
- url=response.url,
116
81
  page_property_handler=page_property_handler,
117
- public_url=response.public_url,
118
- emoji_icon=extract_emoji_icon_from_dto(response),
119
- external_icon_url=extract_external_icon_url_from_dto(response),
120
- cover_image_url=extract_cover_image_url_from_dto(response),
121
82
  )
122
83
 
123
84
  @classmethod
124
85
  def _create_with_dependencies(
125
86
  cls,
126
- id: str,
87
+ dto: NotionPageDto,
127
88
  title: str,
128
- created_time: str,
129
- created_by: PartialUserDto,
130
- last_edited_time: str,
131
- last_edited_by: PartialUserDto,
132
- url: str,
133
- archived: bool,
134
- in_trash: bool,
135
89
  page_property_handler: PagePropertyHandler,
136
- public_url: str | None = None,
137
- emoji_icon: str | None = None,
138
- external_icon_url: str | None = None,
139
- cover_image_url: str | None = None,
140
90
  ) -> Self:
141
91
  block_client = NotionBlockHttpClient()
142
92
  comment_service = CommentService()
143
93
 
144
94
  page_content_service_factory = PageContentServiceFactory()
145
- page_content_service = page_content_service_factory.create(page_id=id, block_client=block_client)
95
+ page_content_service = page_content_service_factory.create(page_id=dto.id, block_client=block_client)
146
96
 
147
- metadata_update_client = PageMetadataUpdateClient(page_id=id)
97
+ metadata_update_client = PageMetadataUpdateClient(page_id=dto.id)
148
98
 
149
99
  return cls(
150
- id=id,
100
+ dto=dto,
151
101
  title=title,
152
- created_time=created_time,
153
- created_by=created_by,
154
- last_edited_time=last_edited_time,
155
- last_edited_by=last_edited_by,
156
- url=url,
157
- archived=archived,
158
- in_trash=in_trash,
159
102
  page_property_handler=page_property_handler,
160
103
  block_client=block_client,
161
104
  comment_service=comment_service,
162
105
  page_content_service=page_content_service,
163
106
  metadata_update_client=metadata_update_client,
164
- public_url=public_url,
165
- emoji_icon=emoji_icon,
166
- external_icon_url=external_icon_url,
167
- cover_image_url=cover_image_url,
168
107
  )
169
108
 
170
109
  @staticmethod
@@ -185,8 +124,8 @@ class NotionPage(Entity):
185
124
  return self._title
186
125
 
187
126
  @property
188
- def url(self) -> str:
189
- return self._url
127
+ def archived(self) -> bool:
128
+ return self._archived
190
129
 
191
130
  @property
192
131
  def markdown_builder() -> MarkdownBuilder:
@@ -1,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,44 +1,82 @@
1
- from __future__ import annotations
2
-
3
1
  import random
4
2
  from abc import ABC, abstractmethod
5
3
  from collections.abc import Sequence
6
- from typing import TYPE_CHECKING, Self
4
+ from pathlib import Path
5
+ from typing import Self, cast
7
6
 
7
+ from notionary.file_upload.service import NotionFileUpload
8
8
  from notionary.shared.entity.entity_metadata_update_client import EntityMetadataUpdateClient
9
- from notionary.user.schemas import PartialUserDto
9
+ from notionary.shared.entity.schemas import EntityResponseDto
10
+ from notionary.shared.models.file import ExternalFile, FileType, NotionHostedFile
11
+ from notionary.shared.models.icon import EmojiIcon, IconType
12
+ from notionary.shared.models.parent import ParentType
13
+ from notionary.user.base import BaseUser
10
14
  from notionary.user.service import UserService
11
15
  from notionary.utils.mixins.logging import LoggingMixin
12
16
  from notionary.utils.uuid_utils import extract_uuid
13
17
 
14
- if TYPE_CHECKING:
15
- from notionary.user.base import BaseUser
16
-
17
18
 
18
19
  class Entity(LoggingMixin, ABC):
19
20
  def __init__(
20
21
  self,
21
- id: str,
22
- created_time: str,
23
- created_by: PartialUserDto,
24
- last_edited_time: str,
25
- last_edited_by: PartialUserDto,
26
- in_trash: bool,
27
- emoji_icon: str | None = None,
28
- external_icon_url: str | None = None,
29
- cover_image_url: str | None = None,
22
+ dto: EntityResponseDto,
30
23
  user_service: UserService | None = None,
24
+ file_upload_service: NotionFileUpload | None = None,
31
25
  ) -> None:
32
- self._id = id
33
- self._created_time = created_time
34
- self._created_by = created_by
35
- self._last_edited_time = last_edited_time
36
- self._last_edited_by = last_edited_by
37
- self._emoji_icon = emoji_icon
38
- self._external_icon_url = external_icon_url
39
- self._cover_image_url = cover_image_url
40
- self._in_trash = in_trash
26
+ self._id = dto.id
27
+ self._created_time = dto.created_time
28
+ self._created_by = dto.created_by
29
+ self._last_edited_time = dto.last_edited_time
30
+ self._last_edited_by = dto.last_edited_by
31
+ self._in_trash = dto.in_trash
32
+ self._parent = dto.parent
33
+ self._url = dto.url
34
+ self._public_url = dto.public_url
35
+
36
+ self._emoji_icon = self._extract_emoji_icon(dto)
37
+ self._external_icon_url = self._extract_external_icon_url(dto)
38
+ self._cover_image_url = self._extract_cover_image_url(dto)
39
+
41
40
  self._user_service = user_service or UserService()
41
+ self._file_upload_service = file_upload_service or NotionFileUpload()
42
+
43
+ @staticmethod
44
+ def _extract_emoji_icon(dto: EntityResponseDto) -> str | None:
45
+ if dto.icon is None:
46
+ return None
47
+ if dto.icon.type is not IconType.EMOJI:
48
+ return None
49
+
50
+ emoji_icon = cast(EmojiIcon, dto.icon)
51
+ return emoji_icon.emoji
52
+
53
+ @staticmethod
54
+ def _extract_external_icon_url(dto: EntityResponseDto) -> str | None:
55
+ if dto.icon is None:
56
+ return None
57
+
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
66
+
67
+ @staticmethod
68
+ def _extract_cover_image_url(dto: EntityResponseDto) -> str | None:
69
+ if dto.cover is None:
70
+ return None
71
+
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
42
80
 
43
81
  @classmethod
44
82
  @abstractmethod
@@ -59,10 +97,7 @@ class Entity(LoggingMixin, ABC):
59
97
 
60
98
  @property
61
99
  @abstractmethod
62
- def _entity_metadata_update_client(self) -> EntityMetadataUpdateClient:
63
- # functionality for updating properties like title, icon, cover, archive status depends on interface for template like implementation
64
- # has to be implementated by inheritants to correctly use the methods below
65
- ...
100
+ def _entity_metadata_update_client(self) -> EntityMetadataUpdateClient: ...
66
101
 
67
102
  @property
68
103
  def id(self) -> str:
@@ -93,12 +128,35 @@ class Entity(LoggingMixin, ABC):
93
128
  return self._cover_image_url
94
129
 
95
130
  @property
96
- def created_by(self) -> PartialUserDto:
97
- return self._created_by
131
+ def url(self) -> str:
132
+ return self._url
98
133
 
99
134
  @property
100
- def last_edited_by(self) -> PartialUserDto:
101
- return self._last_edited_by
135
+ def public_url(self) -> str | None:
136
+ return self._public_url
137
+
138
+ def get_parent_database_id_if_present(self) -> str | None:
139
+ if self._parent.type == ParentType.DATABASE_ID:
140
+ return self._parent.database_id
141
+ return None
142
+
143
+ def get_parent_data_source_id_if_present(self) -> str | None:
144
+ if self._parent.type == ParentType.DATA_SOURCE_ID:
145
+ return self._parent.data_source_id
146
+ return None
147
+
148
+ def get_parent_page_id_if_present(self) -> str | None:
149
+ if self._parent.type == ParentType.PAGE_ID:
150
+ return self._parent.page_id
151
+ return None
152
+
153
+ def get_parent_block_id_if_present(self) -> str | None:
154
+ if self._parent.type == ParentType.BLOCK_ID:
155
+ return self._parent.block_id
156
+ return None
157
+
158
+ def is_workspace_parent(self) -> bool:
159
+ return self._parent.type == ParentType.WORKSPACE
102
160
 
103
161
  async def get_created_by_user(self) -> BaseUser | None:
104
162
  return await self._user_service.get_user_by_id(self._created_by.id)
@@ -108,15 +166,26 @@ class Entity(LoggingMixin, ABC):
108
166
 
109
167
  async def set_emoji_icon(self, emoji: str) -> None:
110
168
  entity_response = await self._entity_metadata_update_client.patch_emoji_icon(emoji)
111
- self._emoji_icon = entity_response.icon.emoji if entity_response.icon else None
169
+ self._emoji_icon = self._extract_emoji_icon(entity_response)
112
170
  self._external_icon_url = None
113
171
 
114
172
  async def set_external_icon(self, icon_url: str) -> None:
115
173
  entity_response = await self._entity_metadata_update_client.patch_external_icon(icon_url)
116
174
  self._emoji_icon = None
117
- self._external_icon_url = (
118
- entity_response.icon.external.url if entity_response.icon and entity_response.icon.external else None
119
- )
175
+ self._external_icon_url = self._extract_external_icon_url(entity_response)
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)
120
189
 
121
190
  async def remove_icon(self) -> None:
122
191
  await self._entity_metadata_update_client.remove_icon()
@@ -125,9 +194,21 @@ class Entity(LoggingMixin, ABC):
125
194
 
126
195
  async def set_cover_image_by_url(self, image_url: str) -> None:
127
196
  entity_response = await self._entity_metadata_update_client.patch_external_cover(image_url)
128
- self._cover_image_url = (
129
- entity_response.cover.external.url if entity_response.cover and entity_response.cover.external else None
130
- )
197
+ self._cover_image_url = self._extract_cover_image_url(entity_response)
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)
131
212
 
132
213
  async def set_random_gradient_cover(self) -> None:
133
214
  random_cover_url = self._get_random_gradient_cover()
@@ -1,21 +1,49 @@
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[ExternalFile | NotionHostedFile | FileUploadFile, Field(discriminator="type")]
@@ -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/bot.py CHANGED
@@ -20,6 +20,18 @@ class BotUser(BaseUser):
20
20
  self._workspace_file_upload_limit_in_bytes = workspace_file_upload_limit_in_bytes
21
21
  self._owner_type = owner_type
22
22
 
23
+ @property
24
+ def workspace_name(self) -> str | None:
25
+ return self._workspace_name
26
+
27
+ @property
28
+ def workspace_file_upload_limit_in_bytes(self) -> int:
29
+ return self._workspace_file_upload_limit_in_bytes
30
+
31
+ @property
32
+ def owner_type(self) -> WorkspaceOwnerType | None:
33
+ return self._owner_type
34
+
23
35
  @classmethod
24
36
  def _get_expected_user_type(cls) -> UserType:
25
37
  return UserType.BOT
@@ -54,17 +66,5 @@ class BotUser(BaseUser):
54
66
  owner_type=owner_type,
55
67
  )
56
68
 
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
69
  def __repr__(self) -> str:
70
70
  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})"
@@ -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,8 @@ 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(additional_text: str = "", min_duration_to_log: float = 0.25) -> _SyncDecorator:
30
+ def decorator(func: _SyncFunc) -> _SyncFunc:
31
31
  @functools.wraps(func)
32
32
  def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
33
33
  start_time = time.perf_counter()
@@ -49,8 +49,8 @@ def time_execution_sync(additional_text: str = "", min_duration_to_log: float =
49
49
  def time_execution_async(
50
50
  additional_text: str = "",
51
51
  min_duration_to_log: float = 0.25,
52
- ) -> AsyncDecorator:
53
- def decorator(func: AsyncFunc) -> AsyncFunc:
52
+ ) -> _AsyncDecorator:
53
+ def decorator(func: _AsyncFunc) -> _AsyncFunc:
54
54
  @functools.wraps(func)
55
55
  async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
56
56
  start_time = time.perf_counter()