notionary 0.3.0__py3-none-any.whl → 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- notionary/__init__.py +9 -2
- notionary/blocks/enums.py +27 -0
- notionary/comments/client.py +6 -9
- notionary/data_source/http/data_source_instance_client.py +4 -4
- notionary/data_source/query/__init__.py +9 -0
- notionary/data_source/query/builder.py +12 -3
- notionary/data_source/query/schema.py +5 -0
- notionary/data_source/service.py +14 -112
- notionary/database/service.py +16 -58
- notionary/exceptions/__init__.py +4 -0
- notionary/exceptions/block_parsing.py +21 -0
- notionary/http/client.py +1 -1
- notionary/page/content/factory.py +2 -0
- notionary/page/content/parser/pre_processsing/handlers/__init__.py +2 -0
- notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +12 -8
- notionary/page/content/parser/pre_processsing/handlers/indentation.py +2 -0
- notionary/page/content/parser/pre_processsing/handlers/video_syntax.py +66 -0
- notionary/page/content/parser/pre_processsing/handlers/whitespace.py +2 -0
- notionary/page/properties/client.py +2 -2
- notionary/page/properties/service.py +14 -3
- notionary/page/service.py +17 -78
- notionary/shared/entity/service.py +94 -36
- notionary/utils/pagination.py +36 -32
- notionary/workspace/__init__.py +2 -2
- notionary/workspace/client.py +2 -0
- notionary/workspace/query/__init__.py +2 -2
- notionary/workspace/query/builder.py +25 -1
- notionary/workspace/query/models.py +9 -1
- notionary/workspace/query/service.py +29 -11
- notionary/workspace/service.py +31 -21
- {notionary-0.3.0.dist-info → notionary-0.3.1.dist-info}/METADATA +9 -5
- {notionary-0.3.0.dist-info → notionary-0.3.1.dist-info}/RECORD +34 -32
- {notionary-0.3.0.dist-info → notionary-0.3.1.dist-info}/WHEEL +0 -0
- {notionary-0.3.0.dist-info → notionary-0.3.1.dist-info}/licenses/LICENSE +0 -0
notionary/__init__.py
CHANGED
|
@@ -2,6 +2,13 @@ from .data_source.service import NotionDataSource
|
|
|
2
2
|
from .database.service import NotionDatabase
|
|
3
3
|
from .page.content.markdown.builder import MarkdownBuilder
|
|
4
4
|
from .page.service import NotionPage
|
|
5
|
-
from .workspace import NotionWorkspace
|
|
5
|
+
from .workspace import NotionWorkspace, NotionWorkspaceQueryConfigBuilder
|
|
6
6
|
|
|
7
|
-
__all__ = [
|
|
7
|
+
__all__ = [
|
|
8
|
+
"MarkdownBuilder",
|
|
9
|
+
"NotionDataSource",
|
|
10
|
+
"NotionDatabase",
|
|
11
|
+
"NotionPage",
|
|
12
|
+
"NotionWorkspace",
|
|
13
|
+
"NotionWorkspaceQueryConfigBuilder",
|
|
14
|
+
]
|
notionary/blocks/enums.py
CHANGED
|
@@ -165,3 +165,30 @@ class CodingLanguage(StrEnum):
|
|
|
165
165
|
return member
|
|
166
166
|
|
|
167
167
|
return default if default is not None else cls.PLAIN_TEXT
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class VideoFileType(StrEnum):
|
|
171
|
+
AMV = ".amv"
|
|
172
|
+
ASF = ".asf"
|
|
173
|
+
AVI = ".avi"
|
|
174
|
+
F4V = ".f4v"
|
|
175
|
+
FLV = ".flv"
|
|
176
|
+
GIFV = ".gifv"
|
|
177
|
+
MKV = ".mkv"
|
|
178
|
+
MOV = ".mov"
|
|
179
|
+
MPG = ".mpg"
|
|
180
|
+
MPEG = ".mpeg"
|
|
181
|
+
MPV = ".mpv"
|
|
182
|
+
MP4 = ".mp4"
|
|
183
|
+
M4V = ".m4v"
|
|
184
|
+
QT = ".qt"
|
|
185
|
+
WMV = ".wmv"
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def get_all_extensions(cls) -> set[str]:
|
|
189
|
+
return {ext.value for ext in cls}
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def is_valid_extension(cls, filename: str) -> bool:
|
|
193
|
+
lower_filename = filename.lower()
|
|
194
|
+
return any(lower_filename.endswith(ext.value) for ext in cls)
|
notionary/comments/client.py
CHANGED
|
@@ -18,20 +18,17 @@ class CommentClient(NotionHttpClient):
|
|
|
18
18
|
async def iter_comments(
|
|
19
19
|
self,
|
|
20
20
|
block_id: str,
|
|
21
|
-
|
|
22
|
-
page_size: int = 100,
|
|
21
|
+
total_results_limit: int | None = None,
|
|
23
22
|
) -> AsyncGenerator[CommentDto]:
|
|
24
|
-
"""
|
|
25
|
-
Iterates through all comments for a block, yielding each comment individually.
|
|
26
|
-
Uses pagination to handle large result sets efficiently without loading everything into memory.
|
|
27
|
-
"""
|
|
28
23
|
async for comment in paginate_notion_api_generator(
|
|
29
|
-
self._list_comments_page, block_id=block_id,
|
|
24
|
+
self._list_comments_page, block_id=block_id, total_results_limit=total_results_limit
|
|
30
25
|
):
|
|
31
26
|
yield comment
|
|
32
27
|
|
|
33
|
-
async def get_all_comments(self, block_id: str, *,
|
|
34
|
-
all_comments = await paginate_notion_api(
|
|
28
|
+
async def get_all_comments(self, block_id: str, *, total_results_limit: int | None = None) -> list[CommentDto]:
|
|
29
|
+
all_comments = await paginate_notion_api(
|
|
30
|
+
self._list_comments_page, block_id=block_id, total_results_limit=total_results_limit
|
|
31
|
+
)
|
|
35
32
|
|
|
36
33
|
self.logger.debug("Retrieved %d total comments for block %s", len(all_comments), block_id)
|
|
37
34
|
return all_comments
|
|
@@ -58,10 +58,10 @@ class DataSourceInstanceClient(NotionHttpClient, EntityMetadataUpdateClient):
|
|
|
58
58
|
|
|
59
59
|
async def query(self, query_params: DataSourceQueryParams | None = None) -> QueryDataSourceResponse:
|
|
60
60
|
query_params_dict = query_params.to_api_params() if query_params else {}
|
|
61
|
-
|
|
61
|
+
total_result_limit = query_params.total_results_limit if query_params else None
|
|
62
62
|
|
|
63
63
|
all_results = await paginate_notion_api(
|
|
64
|
-
self._make_query_request, query_data=query_params_dict or {},
|
|
64
|
+
self._make_query_request, query_data=query_params_dict or {}, total_result_limit=total_result_limit
|
|
65
65
|
)
|
|
66
66
|
|
|
67
67
|
return QueryDataSourceResponse(
|
|
@@ -72,10 +72,10 @@ class DataSourceInstanceClient(NotionHttpClient, EntityMetadataUpdateClient):
|
|
|
72
72
|
|
|
73
73
|
async def query_stream(self, query_params: DataSourceQueryParams | None = None) -> AsyncIterator[Any]:
|
|
74
74
|
query_params_dict = query_params.model_dump() if query_params else {}
|
|
75
|
-
|
|
75
|
+
total_result_limit = query_params.total_results_limit if query_params else None
|
|
76
76
|
|
|
77
77
|
async for result in paginate_notion_api_generator(
|
|
78
|
-
self._make_query_request, query_data=query_params_dict or {},
|
|
78
|
+
self._make_query_request, query_data=query_params_dict or {}, total_results_limit=total_result_limit
|
|
79
79
|
):
|
|
80
80
|
yield result
|
|
81
81
|
|
|
@@ -46,6 +46,7 @@ class DataSourceQueryBuilder:
|
|
|
46
46
|
self._negate_next = False
|
|
47
47
|
self._or_group: list[FilterCondition] | None = None
|
|
48
48
|
self._page_size: int | None = None
|
|
49
|
+
self._total_results_limit: int | None = None
|
|
49
50
|
|
|
50
51
|
def where(self, property_name: str) -> Self:
|
|
51
52
|
self._finalize_current_or_group()
|
|
@@ -193,9 +194,15 @@ class DataSourceQueryBuilder:
|
|
|
193
194
|
self._sorts.append(sort)
|
|
194
195
|
return self
|
|
195
196
|
|
|
196
|
-
def
|
|
197
|
-
if
|
|
197
|
+
def total_results_limit(self, total_result_limit: int) -> Self:
|
|
198
|
+
if total_result_limit < 1:
|
|
198
199
|
raise ValueError("Limit must be at least 1")
|
|
200
|
+
self._total_results_limit = total_result_limit
|
|
201
|
+
return self
|
|
202
|
+
|
|
203
|
+
def page_size(self, page_size: int) -> Self:
|
|
204
|
+
if page_size < 1:
|
|
205
|
+
raise ValueError("Page size must be at least 1")
|
|
199
206
|
self._page_size = page_size
|
|
200
207
|
return self
|
|
201
208
|
|
|
@@ -203,7 +210,9 @@ class DataSourceQueryBuilder:
|
|
|
203
210
|
self._finalize_current_or_group()
|
|
204
211
|
notion_filter = self._create_notion_filter_if_needed()
|
|
205
212
|
sorts = self._create_sorts_if_needed()
|
|
206
|
-
return DataSourceQueryParams(
|
|
213
|
+
return DataSourceQueryParams(
|
|
214
|
+
filter=notion_filter, sorts=sorts, page_size=self._page_size, total_results_limit=self._total_results_limit
|
|
215
|
+
)
|
|
207
216
|
|
|
208
217
|
def _select_property_without_negation(self, property_name: str) -> None:
|
|
209
218
|
self._current_property = property_name
|
|
@@ -289,6 +289,8 @@ class DataSourceQueryParams(BaseModel):
|
|
|
289
289
|
sorts: list[NotionSort] | None = None
|
|
290
290
|
page_size: int | None = None
|
|
291
291
|
|
|
292
|
+
total_results_limit: int | None = None
|
|
293
|
+
|
|
292
294
|
@model_serializer
|
|
293
295
|
def to_api_params(self) -> JsonDict:
|
|
294
296
|
result: JsonDict = {}
|
|
@@ -299,4 +301,7 @@ class DataSourceQueryParams(BaseModel):
|
|
|
299
301
|
if self.sorts is not None and len(self.sorts) > 0:
|
|
300
302
|
result["sorts"] = [sort.model_dump() for sort in self.sorts]
|
|
301
303
|
|
|
304
|
+
if self.page_size is not None:
|
|
305
|
+
result["page_size"] = self.page_size
|
|
306
|
+
|
|
302
307
|
return result
|
notionary/data_source/service.py
CHANGED
|
@@ -16,27 +16,18 @@ from notionary.data_source.properties.schemas import (
|
|
|
16
16
|
DataSourceSelectProperty,
|
|
17
17
|
DataSourceStatusProperty,
|
|
18
18
|
)
|
|
19
|
-
from notionary.data_source.query
|
|
20
|
-
from notionary.data_source.query.resolver import QueryResolver
|
|
21
|
-
from notionary.data_source.query.schema import (
|
|
22
|
-
DataSourceQueryParams,
|
|
23
|
-
)
|
|
19
|
+
from notionary.data_source.query import DataSourceQueryBuilder, DataSourceQueryParams, QueryResolver
|
|
24
20
|
from notionary.data_source.schema.service import DataSourcePropertySchemaFormatter
|
|
25
21
|
from notionary.data_source.schemas import DataSourceDto
|
|
26
22
|
from notionary.exceptions.data_source.properties import DataSourcePropertyNotFound, DataSourcePropertyTypeError
|
|
27
23
|
from notionary.page.properties.models import PageTitleProperty
|
|
28
24
|
from notionary.page.schemas import NotionPageDto
|
|
29
25
|
from notionary.shared.entity.dto_parsers import (
|
|
30
|
-
extract_cover_image_url_from_dto,
|
|
31
|
-
extract_database_id,
|
|
32
26
|
extract_description,
|
|
33
|
-
extract_emoji_icon_from_dto,
|
|
34
|
-
extract_external_icon_url_from_dto,
|
|
35
27
|
extract_title,
|
|
36
28
|
)
|
|
37
29
|
from notionary.shared.entity.entity_metadata_update_client import EntityMetadataUpdateClient
|
|
38
30
|
from notionary.shared.entity.service import Entity
|
|
39
|
-
from notionary.user.schemas import PartialUserDto
|
|
40
31
|
from notionary.workspace.query.service import WorkspaceQueryService
|
|
41
32
|
|
|
42
33
|
if TYPE_CHECKING:
|
|
@@ -46,45 +37,21 @@ if TYPE_CHECKING:
|
|
|
46
37
|
class NotionDataSource(Entity):
|
|
47
38
|
def __init__(
|
|
48
39
|
self,
|
|
49
|
-
|
|
40
|
+
dto: DataSourceDto,
|
|
50
41
|
title: str,
|
|
51
|
-
|
|
52
|
-
created_by: PartialUserDto,
|
|
53
|
-
last_edited_time: str,
|
|
54
|
-
last_edited_by: PartialUserDto,
|
|
55
|
-
archived: bool,
|
|
56
|
-
in_trash: bool,
|
|
57
|
-
url: str,
|
|
42
|
+
description: str | None,
|
|
58
43
|
properties: dict[str, DataSourceProperty],
|
|
59
|
-
|
|
60
|
-
emoji_icon: str | None = None,
|
|
61
|
-
external_icon_url: str | None = None,
|
|
62
|
-
cover_image_url: str | None = None,
|
|
63
|
-
description: str | None = None,
|
|
64
|
-
public_url: str | None = None,
|
|
65
|
-
data_source_instance_client: DataSourceInstanceClient | None = None,
|
|
44
|
+
data_source_instance_client: DataSourceInstanceClient,
|
|
66
45
|
query_resolver: QueryResolver | None = None,
|
|
67
46
|
) -> None:
|
|
68
|
-
super().__init__(
|
|
69
|
-
|
|
70
|
-
created_time=created_time,
|
|
71
|
-
created_by=created_by,
|
|
72
|
-
last_edited_time=last_edited_time,
|
|
73
|
-
last_edited_by=last_edited_by,
|
|
74
|
-
in_trash=in_trash,
|
|
75
|
-
emoji_icon=emoji_icon,
|
|
76
|
-
external_icon_url=external_icon_url,
|
|
77
|
-
cover_image_url=cover_image_url,
|
|
78
|
-
)
|
|
79
|
-
self._parent_database_id = parent_database_id
|
|
47
|
+
super().__init__(dto=dto)
|
|
48
|
+
|
|
80
49
|
self._parent_database: NotionDatabase | None = None
|
|
81
50
|
self._title = title
|
|
82
|
-
self._archived = archived
|
|
83
|
-
self._url = url
|
|
84
|
-
self._public_url = public_url
|
|
51
|
+
self._archived = dto.archived
|
|
85
52
|
self._description = description
|
|
86
53
|
self._properties = properties or {}
|
|
87
|
-
self._data_source_client = data_source_instance_client
|
|
54
|
+
self._data_source_client = data_source_instance_client
|
|
88
55
|
self.query_resolver = query_resolver or QueryResolver()
|
|
89
56
|
|
|
90
57
|
@classmethod
|
|
@@ -111,73 +78,21 @@ class NotionDataSource(Entity):
|
|
|
111
78
|
@classmethod
|
|
112
79
|
async def _create_from_dto(
|
|
113
80
|
cls,
|
|
114
|
-
|
|
81
|
+
dto: DataSourceDto,
|
|
115
82
|
rich_text_converter: RichTextToMarkdownConverter,
|
|
116
83
|
) -> Self:
|
|
117
84
|
title, description = await asyncio.gather(
|
|
118
|
-
extract_title(
|
|
119
|
-
extract_description(
|
|
85
|
+
extract_title(dto, rich_text_converter),
|
|
86
|
+
extract_description(dto, rich_text_converter),
|
|
120
87
|
)
|
|
121
88
|
|
|
122
|
-
|
|
89
|
+
data_source_instance_client = DataSourceInstanceClient(data_source_id=dto.id)
|
|
123
90
|
|
|
124
|
-
return cls._create(
|
|
125
|
-
id=response.id,
|
|
126
|
-
title=title,
|
|
127
|
-
description=description,
|
|
128
|
-
created_time=response.created_time,
|
|
129
|
-
created_by=response.created_by,
|
|
130
|
-
last_edited_time=response.last_edited_time,
|
|
131
|
-
last_edited_by=response.last_edited_by,
|
|
132
|
-
archived=response.archived,
|
|
133
|
-
in_trash=response.in_trash,
|
|
134
|
-
url=response.url,
|
|
135
|
-
properties=response.properties,
|
|
136
|
-
parent_database_id=parent_database_id,
|
|
137
|
-
emoji_icon=extract_emoji_icon_from_dto(response),
|
|
138
|
-
external_icon_url=extract_external_icon_url_from_dto(response),
|
|
139
|
-
cover_image_url=extract_cover_image_url_from_dto(response),
|
|
140
|
-
public_url=response.url,
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
@classmethod
|
|
144
|
-
def _create(
|
|
145
|
-
cls,
|
|
146
|
-
id: str,
|
|
147
|
-
title: str,
|
|
148
|
-
created_time: str,
|
|
149
|
-
created_by: PartialUserDto,
|
|
150
|
-
last_edited_time: str,
|
|
151
|
-
last_edited_by: PartialUserDto,
|
|
152
|
-
archived: bool,
|
|
153
|
-
in_trash: bool,
|
|
154
|
-
url: str,
|
|
155
|
-
properties: dict[str, DataSourceProperty],
|
|
156
|
-
parent_database_id: str | None,
|
|
157
|
-
emoji_icon: str | None = None,
|
|
158
|
-
external_icon_url: str | None = None,
|
|
159
|
-
cover_image_url: str | None = None,
|
|
160
|
-
description: str | None = None,
|
|
161
|
-
public_url: str | None = None,
|
|
162
|
-
) -> Self:
|
|
163
|
-
data_source_instance_client = DataSourceInstanceClient(data_source_id=id)
|
|
164
91
|
return cls(
|
|
165
|
-
|
|
92
|
+
dto=dto,
|
|
166
93
|
title=title,
|
|
167
|
-
created_time=created_time,
|
|
168
|
-
created_by=created_by,
|
|
169
|
-
last_edited_time=last_edited_time,
|
|
170
|
-
last_edited_by=last_edited_by,
|
|
171
|
-
archived=archived,
|
|
172
|
-
in_trash=in_trash,
|
|
173
|
-
url=url,
|
|
174
|
-
parent_database_id=parent_database_id,
|
|
175
|
-
emoji_icon=emoji_icon,
|
|
176
|
-
external_icon_url=external_icon_url,
|
|
177
|
-
cover_image_url=cover_image_url,
|
|
178
94
|
description=description,
|
|
179
|
-
|
|
180
|
-
properties=properties,
|
|
95
|
+
properties=dto.properties,
|
|
181
96
|
data_source_instance_client=data_source_instance_client,
|
|
182
97
|
)
|
|
183
98
|
|
|
@@ -201,19 +116,6 @@ class NotionDataSource(Entity):
|
|
|
201
116
|
def properties(self) -> dict[str, DataSourceProperty]:
|
|
202
117
|
return self._properties
|
|
203
118
|
|
|
204
|
-
@property
|
|
205
|
-
def url(self) -> str:
|
|
206
|
-
return self._url
|
|
207
|
-
|
|
208
|
-
@property
|
|
209
|
-
def public_url(self) -> str | None:
|
|
210
|
-
return self._public_url
|
|
211
|
-
|
|
212
|
-
async def get_parent_database(self) -> NotionDatabase | None:
|
|
213
|
-
if self._parent_database is None and self._parent_database_id:
|
|
214
|
-
self._parent_database = await NotionDatabase.from_id(self._parent_database_id)
|
|
215
|
-
return self._parent_database
|
|
216
|
-
|
|
217
119
|
async def create_blank_page(self, title: str | None = None) -> NotionPage:
|
|
218
120
|
return await self._data_source_client.create_blank_page(title=title)
|
|
219
121
|
|
notionary/database/service.py
CHANGED
|
@@ -10,14 +10,10 @@ from notionary.database.client import NotionDatabaseHttpClient
|
|
|
10
10
|
from notionary.database.database_metadata_update_client import DatabaseMetadataUpdateClient
|
|
11
11
|
from notionary.database.schemas import NotionDatabaseDto
|
|
12
12
|
from notionary.shared.entity.dto_parsers import (
|
|
13
|
-
extract_cover_image_url_from_dto,
|
|
14
13
|
extract_description,
|
|
15
|
-
extract_emoji_icon_from_dto,
|
|
16
|
-
extract_external_icon_url_from_dto,
|
|
17
14
|
extract_title,
|
|
18
15
|
)
|
|
19
16
|
from notionary.shared.entity.service import Entity
|
|
20
|
-
from notionary.user.schemas import PartialUserDto
|
|
21
17
|
from notionary.workspace.query.service import WorkspaceQueryService
|
|
22
18
|
|
|
23
19
|
type DataSourceFactory = Callable[[str], Awaitable[NotionDataSource]]
|
|
@@ -26,46 +22,24 @@ type DataSourceFactory = Callable[[str], Awaitable[NotionDataSource]]
|
|
|
26
22
|
class NotionDatabase(Entity):
|
|
27
23
|
def __init__(
|
|
28
24
|
self,
|
|
29
|
-
|
|
25
|
+
dto: NotionDatabaseDto,
|
|
30
26
|
title: str,
|
|
31
|
-
|
|
32
|
-
created_by: PartialUserDto,
|
|
33
|
-
last_edited_time: str,
|
|
34
|
-
last_edited_by: PartialUserDto,
|
|
35
|
-
url: str,
|
|
36
|
-
in_trash: bool,
|
|
37
|
-
is_inline: bool,
|
|
27
|
+
description: str | None,
|
|
38
28
|
data_source_ids: list[str],
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
external_icon_url: str | None = None,
|
|
42
|
-
cover_image_url: str | None = None,
|
|
43
|
-
description: str | None = None,
|
|
44
|
-
client: NotionDatabaseHttpClient | None = None,
|
|
45
|
-
metadata_update_client: DatabaseMetadataUpdateClient | None = None,
|
|
29
|
+
client: NotionDatabaseHttpClient,
|
|
30
|
+
metadata_update_client: DatabaseMetadataUpdateClient,
|
|
46
31
|
) -> None:
|
|
47
|
-
super().__init__(
|
|
48
|
-
|
|
49
|
-
created_time=created_time,
|
|
50
|
-
created_by=created_by,
|
|
51
|
-
last_edited_time=last_edited_time,
|
|
52
|
-
last_edited_by=last_edited_by,
|
|
53
|
-
in_trash=in_trash,
|
|
54
|
-
emoji_icon=emoji_icon,
|
|
55
|
-
external_icon_url=external_icon_url,
|
|
56
|
-
cover_image_url=cover_image_url,
|
|
57
|
-
)
|
|
32
|
+
super().__init__(dto=dto)
|
|
33
|
+
|
|
58
34
|
self._title = title
|
|
59
|
-
self._url = url
|
|
60
|
-
self._public_url = public_url
|
|
61
35
|
self._description = description
|
|
62
|
-
self._is_inline = is_inline
|
|
36
|
+
self._is_inline = dto.is_inline
|
|
63
37
|
|
|
64
38
|
self._data_sources: list[NotionDataSource] | None = None
|
|
65
39
|
self._data_source_ids = data_source_ids
|
|
66
40
|
|
|
67
|
-
self.client = client
|
|
68
|
-
self._metadata_update_client = metadata_update_client
|
|
41
|
+
self.client = client
|
|
42
|
+
self._metadata_update_client = metadata_update_client
|
|
69
43
|
|
|
70
44
|
@classmethod
|
|
71
45
|
async def from_id(
|
|
@@ -94,31 +68,23 @@ class NotionDatabase(Entity):
|
|
|
94
68
|
@classmethod
|
|
95
69
|
async def _create_from_dto(
|
|
96
70
|
cls,
|
|
97
|
-
|
|
71
|
+
dto: NotionDatabaseDto,
|
|
98
72
|
rich_text_converter: RichTextToMarkdownConverter,
|
|
99
73
|
client: NotionDatabaseHttpClient,
|
|
100
74
|
) -> Self:
|
|
101
75
|
title, description = await asyncio.gather(
|
|
102
|
-
extract_title(
|
|
76
|
+
extract_title(dto, rich_text_converter), extract_description(dto, rich_text_converter)
|
|
103
77
|
)
|
|
104
78
|
|
|
79
|
+
metadata_update_client = DatabaseMetadataUpdateClient(database_id=dto.id)
|
|
80
|
+
|
|
105
81
|
return cls(
|
|
106
|
-
|
|
82
|
+
dto=dto,
|
|
107
83
|
title=title,
|
|
108
84
|
description=description,
|
|
109
|
-
|
|
110
|
-
created_by=response.created_by,
|
|
111
|
-
last_edited_time=response.last_edited_time,
|
|
112
|
-
last_edited_by=response.last_edited_by,
|
|
113
|
-
in_trash=response.in_trash,
|
|
114
|
-
is_inline=response.is_inline,
|
|
115
|
-
url=response.url,
|
|
116
|
-
public_url=response.public_url,
|
|
117
|
-
emoji_icon=extract_emoji_icon_from_dto(response),
|
|
118
|
-
external_icon_url=extract_external_icon_url_from_dto(response),
|
|
119
|
-
cover_image_url=extract_cover_image_url_from_dto(response),
|
|
120
|
-
data_source_ids=[ds.id for ds in response.data_sources],
|
|
85
|
+
data_source_ids=[ds.id for ds in dto.data_sources],
|
|
121
86
|
client=client,
|
|
87
|
+
metadata_update_client=metadata_update_client,
|
|
122
88
|
)
|
|
123
89
|
|
|
124
90
|
@property
|
|
@@ -129,14 +95,6 @@ class NotionDatabase(Entity):
|
|
|
129
95
|
def title(self) -> str:
|
|
130
96
|
return self._title
|
|
131
97
|
|
|
132
|
-
@property
|
|
133
|
-
def url(self) -> str:
|
|
134
|
-
return self._url
|
|
135
|
-
|
|
136
|
-
@property
|
|
137
|
-
def public_url(self) -> str | None:
|
|
138
|
-
return self._public_url
|
|
139
|
-
|
|
140
98
|
@property
|
|
141
99
|
def is_inline(self) -> bool:
|
|
142
100
|
return self._is_inline
|
notionary/exceptions/__init__.py
CHANGED
|
@@ -8,6 +8,7 @@ from .api import (
|
|
|
8
8
|
NotionValidationError,
|
|
9
9
|
)
|
|
10
10
|
from .base import NotionaryError
|
|
11
|
+
from .block_parsing import InsufficientColumnsError, InvalidColumnRatioSumError, UnsupportedVideoFormatError
|
|
11
12
|
from .data_source import DataSourcePropertyNotFound, DataSourcePropertyTypeError
|
|
12
13
|
from .properties import AccessPagePropertyWithoutDataSourceError, PagePropertyNotFoundError, PagePropertyTypeError
|
|
13
14
|
from .search import DatabaseNotFound, DataSourceNotFound, EntityNotFound, PageNotFound
|
|
@@ -19,6 +20,8 @@ __all__ = [
|
|
|
19
20
|
"DataSourcePropertyTypeError",
|
|
20
21
|
"DatabaseNotFound",
|
|
21
22
|
"EntityNotFound",
|
|
23
|
+
"InsufficientColumnsError",
|
|
24
|
+
"InvalidColumnRatioSumError",
|
|
22
25
|
"NotionApiError",
|
|
23
26
|
"NotionAuthenticationError",
|
|
24
27
|
"NotionConnectionError",
|
|
@@ -30,4 +33,5 @@ __all__ = [
|
|
|
30
33
|
"PageNotFound",
|
|
31
34
|
"PagePropertyNotFoundError",
|
|
32
35
|
"PagePropertyTypeError",
|
|
36
|
+
"UnsupportedVideoFormatError",
|
|
33
37
|
]
|
|
@@ -14,3 +14,24 @@ class InvalidColumnRatioSumError(NotionaryError):
|
|
|
14
14
|
self.total = total
|
|
15
15
|
self.tolerance = tolerance
|
|
16
16
|
super().__init__(f"Width ratios must sum to 1.0 (±{tolerance}), but sum is {total}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UnsupportedVideoFormatError(ValueError):
|
|
20
|
+
def __init__(self, url: str, supported_formats: list[str]) -> None:
|
|
21
|
+
self.url = url
|
|
22
|
+
self.supported_formats = supported_formats
|
|
23
|
+
super().__init__(self._create_user_friendly_message())
|
|
24
|
+
|
|
25
|
+
def _create_user_friendly_message(self) -> str:
|
|
26
|
+
formats = ", ".join(self.supported_formats[:5])
|
|
27
|
+
remaining = len(self.supported_formats) - 5
|
|
28
|
+
|
|
29
|
+
if remaining > 0:
|
|
30
|
+
formats += f" and {remaining} more"
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
f"The video URL '{self.url}' uses an unsupported format.\n"
|
|
34
|
+
f"Supported formats include: {formats}.\n"
|
|
35
|
+
f"YouTube embed and watch URLs are also supported."
|
|
36
|
+
f"Also see https://developers.notion.com/reference/block#video for more information."
|
|
37
|
+
)
|
notionary/http/client.py
CHANGED
|
@@ -158,7 +158,7 @@ class NotionHttpClient(LoggingMixin):
|
|
|
158
158
|
)
|
|
159
159
|
if status_code == 404:
|
|
160
160
|
raise NotionResourceNotFoundError(
|
|
161
|
-
"The requested resource was not found. Please verify the page/database ID.",
|
|
161
|
+
"The requested resource was not found. Please verify the page/database/datasource ID.",
|
|
162
162
|
status_code=status_code,
|
|
163
163
|
response_text=response_text,
|
|
164
164
|
)
|
|
@@ -5,6 +5,7 @@ from notionary.page.content.parser.post_processing.service import BlockPostProce
|
|
|
5
5
|
from notionary.page.content.parser.pre_processsing.handlers import (
|
|
6
6
|
ColumnSyntaxPreProcessor,
|
|
7
7
|
IndentationNormalizer,
|
|
8
|
+
VideoFormatPreProcessor,
|
|
8
9
|
WhitespacePreProcessor,
|
|
9
10
|
)
|
|
10
11
|
from notionary.page.content.parser.pre_processsing.service import MarkdownPreProcessor
|
|
@@ -60,6 +61,7 @@ class PageContentServiceFactory:
|
|
|
60
61
|
pre_processor.register(ColumnSyntaxPreProcessor())
|
|
61
62
|
pre_processor.register(WhitespacePreProcessor())
|
|
62
63
|
pre_processor.register(IndentationNormalizer())
|
|
64
|
+
pre_processor.register(VideoFormatPreProcessor())
|
|
63
65
|
return pre_processor
|
|
64
66
|
|
|
65
67
|
def _create_post_processor(self) -> BlockPostProcessor:
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from .column_syntax import ColumnSyntaxPreProcessor
|
|
2
2
|
from .indentation import IndentationNormalizer
|
|
3
3
|
from .port import PreProcessor
|
|
4
|
+
from .video_syntax import VideoFormatPreProcessor
|
|
4
5
|
from .whitespace import WhitespacePreProcessor
|
|
5
6
|
|
|
6
7
|
__all__ = [
|
|
7
8
|
"ColumnSyntaxPreProcessor",
|
|
8
9
|
"IndentationNormalizer",
|
|
9
10
|
"PreProcessor",
|
|
11
|
+
"VideoFormatPreProcessor",
|
|
10
12
|
"WhitespacePreProcessor",
|
|
11
13
|
]
|
|
@@ -4,13 +4,14 @@ from typing import override
|
|
|
4
4
|
from notionary.exceptions.block_parsing import InsufficientColumnsError, InvalidColumnRatioSumError
|
|
5
5
|
from notionary.page.content.parser.pre_processsing.handlers.port import PreProcessor
|
|
6
6
|
from notionary.page.content.syntax import MarkdownGrammar, SyntaxRegistry
|
|
7
|
+
from notionary.utils.decorators import time_execution_sync
|
|
7
8
|
from notionary.utils.mixins.logging import LoggingMixin
|
|
8
9
|
|
|
9
|
-
RATIO_TOLERANCE = 0.0001
|
|
10
|
-
MINIMUM_COLUMNS = 2
|
|
11
|
-
|
|
12
10
|
|
|
13
11
|
class ColumnSyntaxPreProcessor(PreProcessor, LoggingMixin):
|
|
12
|
+
_RATIO_TOLERANCE = 0.0001
|
|
13
|
+
_MINIMUM_COLUMNS = 2
|
|
14
|
+
|
|
14
15
|
def __init__(
|
|
15
16
|
self, syntax_registry: SyntaxRegistry | None = None, markdown_grammar: MarkdownGrammar | None = None
|
|
16
17
|
) -> None:
|
|
@@ -24,6 +25,7 @@ class ColumnSyntaxPreProcessor(PreProcessor, LoggingMixin):
|
|
|
24
25
|
self._column_pattern = self._syntax_registry.get_column_syntax().regex_pattern
|
|
25
26
|
|
|
26
27
|
@override
|
|
28
|
+
@time_execution_sync()
|
|
27
29
|
def process(self, markdown_text: str) -> str:
|
|
28
30
|
if not self._contains_column_lists(markdown_text):
|
|
29
31
|
return markdown_text
|
|
@@ -96,8 +98,10 @@ class ColumnSyntaxPreProcessor(PreProcessor, LoggingMixin):
|
|
|
96
98
|
return list(self._column_pattern.finditer(content))
|
|
97
99
|
|
|
98
100
|
def _validate_minimum_column_count(self, column_count: int) -> None:
|
|
99
|
-
if column_count <
|
|
100
|
-
self.logger.error(
|
|
101
|
+
if column_count < self._MINIMUM_COLUMNS:
|
|
102
|
+
self.logger.error(
|
|
103
|
+
f"Column list must contain at least {self._MINIMUM_COLUMNS} columns, found {column_count}"
|
|
104
|
+
)
|
|
101
105
|
raise InsufficientColumnsError(column_count)
|
|
102
106
|
|
|
103
107
|
def _extract_column_ratios(self, column_matches: list[re.Match]) -> list[float]:
|
|
@@ -120,11 +124,11 @@ class ColumnSyntaxPreProcessor(PreProcessor, LoggingMixin):
|
|
|
120
124
|
total_ratio = sum(ratios)
|
|
121
125
|
|
|
122
126
|
if not self._is_ratio_sum_valid(total_ratio):
|
|
123
|
-
self.logger.error(f"Column ratios must sum to 1.0 (±{
|
|
124
|
-
raise InvalidColumnRatioSumError(total_ratio,
|
|
127
|
+
self.logger.error(f"Column ratios must sum to 1.0 (±{self._RATIO_TOLERANCE}), but sum to {total_ratio:.4f}")
|
|
128
|
+
raise InvalidColumnRatioSumError(total_ratio, self._RATIO_TOLERANCE)
|
|
125
129
|
|
|
126
130
|
def _should_validate_ratios(self, ratios: list[float], column_count: int) -> bool:
|
|
127
131
|
return len(ratios) > 0 and len(ratios) == column_count
|
|
128
132
|
|
|
129
133
|
def _is_ratio_sum_valid(self, total: float) -> bool:
|
|
130
|
-
return abs(total - 1.0) <=
|
|
134
|
+
return abs(total - 1.0) <= self._RATIO_TOLERANCE
|
|
@@ -3,6 +3,7 @@ from typing import override
|
|
|
3
3
|
|
|
4
4
|
from notionary.page.content.parser.pre_processsing.handlers.port import PreProcessor
|
|
5
5
|
from notionary.page.content.syntax import MarkdownGrammar, SyntaxRegistry
|
|
6
|
+
from notionary.utils.decorators import time_execution_sync
|
|
6
7
|
from notionary.utils.mixins.logging import LoggingMixin
|
|
7
8
|
|
|
8
9
|
|
|
@@ -18,6 +19,7 @@ class IndentationNormalizer(PreProcessor, LoggingMixin):
|
|
|
18
19
|
self._code_block_start_delimiter = self._syntax_registry.get_code_syntax().start_delimiter
|
|
19
20
|
|
|
20
21
|
@override
|
|
22
|
+
@time_execution_sync()
|
|
21
23
|
def process(self, markdown_text: str) -> str:
|
|
22
24
|
if self._is_empty(markdown_text):
|
|
23
25
|
return ""
|