notionary 0.2.28__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/__init__.py +5 -0
- notionary/blocks/client.py +6 -4
- notionary/blocks/enums.py +28 -1
- notionary/blocks/rich_text/markdown_rich_text_converter.py +14 -0
- notionary/blocks/rich_text/models.py +14 -0
- notionary/blocks/rich_text/name_id_resolver/__init__.py +2 -0
- notionary/blocks/rich_text/name_id_resolver/data_source.py +32 -0
- notionary/blocks/rich_text/rich_text_markdown_converter.py +12 -0
- notionary/blocks/rich_text/rich_text_patterns.py +3 -0
- notionary/blocks/schemas.py +42 -10
- notionary/comments/__init__.py +5 -0
- notionary/comments/client.py +7 -10
- notionary/comments/factory.py +4 -6
- notionary/data_source/http/data_source_instance_client.py +14 -4
- notionary/data_source/properties/{models.py → schemas.py} +4 -8
- notionary/data_source/query/__init__.py +9 -0
- notionary/data_source/query/builder.py +38 -10
- notionary/data_source/query/schema.py +13 -10
- notionary/data_source/query/validator.py +11 -11
- notionary/data_source/schema/registry.py +104 -0
- notionary/data_source/schema/service.py +136 -0
- notionary/data_source/schemas.py +1 -1
- notionary/data_source/service.py +29 -103
- notionary/database/service.py +17 -60
- notionary/exceptions/__init__.py +5 -1
- notionary/exceptions/block_parsing.py +21 -0
- notionary/exceptions/search.py +24 -0
- notionary/http/client.py +9 -10
- notionary/http/models.py +5 -4
- notionary/page/content/factory.py +10 -3
- notionary/page/content/markdown/builder.py +76 -154
- notionary/page/content/markdown/nodes/__init__.py +0 -2
- notionary/page/content/markdown/nodes/audio.py +1 -1
- notionary/page/content/markdown/nodes/base.py +1 -1
- notionary/page/content/markdown/nodes/bookmark.py +1 -1
- notionary/page/content/markdown/nodes/breadcrumb.py +1 -1
- notionary/page/content/markdown/nodes/bulleted_list.py +31 -8
- notionary/page/content/markdown/nodes/callout.py +12 -10
- notionary/page/content/markdown/nodes/code.py +3 -5
- notionary/page/content/markdown/nodes/columns.py +39 -21
- notionary/page/content/markdown/nodes/container.py +64 -0
- notionary/page/content/markdown/nodes/divider.py +1 -1
- notionary/page/content/markdown/nodes/embed.py +1 -1
- notionary/page/content/markdown/nodes/equation.py +1 -1
- notionary/page/content/markdown/nodes/file.py +1 -1
- notionary/page/content/markdown/nodes/heading.py +26 -6
- notionary/page/content/markdown/nodes/image.py +1 -1
- notionary/page/content/markdown/nodes/mixins/__init__.py +5 -0
- notionary/page/content/markdown/nodes/mixins/caption.py +1 -1
- notionary/page/content/markdown/nodes/numbered_list.py +28 -5
- notionary/page/content/markdown/nodes/paragraph.py +1 -1
- notionary/page/content/markdown/nodes/pdf.py +1 -1
- notionary/page/content/markdown/nodes/quote.py +17 -5
- notionary/page/content/markdown/nodes/space.py +1 -1
- notionary/page/content/markdown/nodes/table.py +1 -1
- notionary/page/content/markdown/nodes/table_of_contents.py +1 -1
- notionary/page/content/markdown/nodes/todo.py +23 -7
- notionary/page/content/markdown/nodes/toggle.py +13 -14
- notionary/page/content/markdown/nodes/video.py +1 -1
- notionary/page/content/parser/context.py +98 -21
- notionary/page/content/parser/factory.py +1 -10
- notionary/page/content/parser/parsers/__init__.py +0 -2
- notionary/page/content/parser/parsers/audio.py +1 -1
- notionary/page/content/parser/parsers/base.py +1 -1
- notionary/page/content/parser/parsers/bookmark.py +1 -1
- notionary/page/content/parser/parsers/breadcrumb.py +1 -1
- notionary/page/content/parser/parsers/bulleted_list.py +52 -8
- notionary/page/content/parser/parsers/callout.py +55 -84
- notionary/page/content/parser/parsers/caption.py +1 -1
- notionary/page/content/parser/parsers/code.py +5 -5
- notionary/page/content/parser/parsers/column.py +23 -64
- notionary/page/content/parser/parsers/column_list.py +45 -45
- notionary/page/content/parser/parsers/divider.py +1 -1
- notionary/page/content/parser/parsers/embed.py +1 -1
- notionary/page/content/parser/parsers/equation.py +1 -1
- notionary/page/content/parser/parsers/file.py +1 -1
- notionary/page/content/parser/parsers/heading.py +65 -8
- notionary/page/content/parser/parsers/image.py +1 -1
- notionary/page/content/parser/parsers/numbered_list.py +52 -8
- notionary/page/content/parser/parsers/paragraph.py +3 -2
- notionary/page/content/parser/parsers/pdf.py +1 -1
- notionary/page/content/parser/parsers/quote.py +75 -15
- notionary/page/content/parser/parsers/space.py +14 -8
- notionary/page/content/parser/parsers/table.py +1 -1
- notionary/page/content/parser/parsers/table_of_contents.py +1 -1
- notionary/page/content/parser/parsers/todo.py +57 -19
- notionary/page/content/parser/parsers/toggle.py +17 -74
- notionary/page/content/parser/parsers/video.py +1 -1
- notionary/page/content/parser/post_processing/handlers/rich_text_length.py +6 -4
- notionary/page/content/parser/post_processing/handlers/rich_text_length_truncation.py +43 -22
- notionary/page/content/parser/pre_processsing/handlers/__init__.py +4 -0
- notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +108 -54
- notionary/page/content/parser/pre_processsing/handlers/indentation.py +86 -0
- notionary/page/content/parser/pre_processsing/handlers/video_syntax.py +66 -0
- notionary/page/content/parser/pre_processsing/handlers/whitespace.py +14 -7
- notionary/page/content/parser/service.py +9 -0
- notionary/page/content/renderer/context.py +5 -2
- notionary/page/content/renderer/factory.py +2 -11
- notionary/page/content/renderer/post_processing/handlers/__init__.py +2 -2
- notionary/page/content/renderer/post_processing/handlers/numbered_list.py +156 -0
- notionary/page/content/renderer/renderers/__init__.py +0 -2
- notionary/page/content/renderer/renderers/base.py +1 -1
- notionary/page/content/renderer/renderers/bulleted_list.py +1 -1
- notionary/page/content/renderer/renderers/callout.py +6 -21
- notionary/page/content/renderer/renderers/captioned_block.py +1 -1
- notionary/page/content/renderer/renderers/column.py +28 -19
- notionary/page/content/renderer/renderers/column_list.py +24 -11
- notionary/page/content/renderer/renderers/heading.py +53 -27
- notionary/page/content/renderer/renderers/numbered_list.py +6 -5
- notionary/page/content/renderer/renderers/quote.py +1 -1
- notionary/page/content/renderer/renderers/todo.py +1 -1
- notionary/page/content/renderer/renderers/toggle.py +6 -7
- notionary/page/content/service.py +4 -1
- notionary/page/content/syntax/__init__.py +4 -0
- notionary/page/content/syntax/grammar.py +10 -0
- notionary/page/content/syntax/models.py +0 -2
- notionary/page/content/syntax/{service.py → registry.py} +31 -91
- notionary/page/properties/client.py +3 -3
- notionary/page/properties/models.py +3 -2
- notionary/page/properties/service.py +18 -3
- notionary/page/service.py +22 -80
- notionary/shared/entity/service.py +94 -36
- notionary/shared/models/cover.py +1 -1
- notionary/shared/typings.py +3 -0
- notionary/user/base.py +60 -11
- notionary/user/factory.py +0 -0
- notionary/utils/decorators.py +122 -0
- notionary/utils/fuzzy.py +18 -6
- notionary/utils/mixins/logging.py +38 -27
- notionary/utils/pagination.py +70 -16
- notionary/workspace/__init__.py +2 -1
- notionary/workspace/client.py +4 -2
- notionary/workspace/query/__init__.py +3 -0
- notionary/workspace/query/builder.py +25 -1
- notionary/workspace/query/models.py +12 -3
- notionary/workspace/query/service.py +57 -32
- notionary/workspace/service.py +31 -21
- {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/METADATA +35 -105
- notionary-0.3.1.dist-info/RECORD +211 -0
- notionary/page/content/markdown/nodes/toggleable_heading.py +0 -35
- notionary/page/content/parser/parsers/toggleable_heading.py +0 -150
- notionary/page/content/renderer/post_processing/handlers/numbered_list_placeholdere.py +0 -62
- notionary/page/content/renderer/renderers/toggleable_heading.py +0 -78
- notionary/utils/async_retry.py +0 -39
- notionary/utils/singleton.py +0 -13
- notionary-0.2.28.dist-info/RECORD +0 -200
- {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/WHEEL +0 -0
- {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
2
|
from typing import Self
|
|
3
3
|
|
|
4
|
-
from notionary.data_source.properties.
|
|
4
|
+
from notionary.data_source.properties.schemas import DataSourceProperty
|
|
5
5
|
from notionary.data_source.query.schema import (
|
|
6
6
|
ArrayOperator,
|
|
7
7
|
BooleanOperator,
|
|
@@ -24,7 +24,7 @@ from notionary.data_source.query.schema import (
|
|
|
24
24
|
TimestampSort,
|
|
25
25
|
TimestampType,
|
|
26
26
|
)
|
|
27
|
-
from notionary.data_source.query.validator import
|
|
27
|
+
from notionary.data_source.query.validator import QueryValidator
|
|
28
28
|
from notionary.exceptions.data_source.properties import DataSourcePropertyNotFound
|
|
29
29
|
from notionary.utils.date import parse_date
|
|
30
30
|
|
|
@@ -33,11 +33,11 @@ class DataSourceQueryBuilder:
|
|
|
33
33
|
def __init__(
|
|
34
34
|
self,
|
|
35
35
|
properties: dict[str, DataSourceProperty],
|
|
36
|
-
|
|
36
|
+
query_validator: QueryValidator | None = None,
|
|
37
37
|
date_parser: Callable[[str], str] = parse_date,
|
|
38
38
|
) -> None:
|
|
39
39
|
self._properties = properties
|
|
40
|
-
self.
|
|
40
|
+
self._query_validator = query_validator or QueryValidator()
|
|
41
41
|
self._date_parser = date_parser
|
|
42
42
|
|
|
43
43
|
self._filters: list[InternalFilterCondition] = []
|
|
@@ -45,6 +45,8 @@ class DataSourceQueryBuilder:
|
|
|
45
45
|
self._current_property: str | None = None
|
|
46
46
|
self._negate_next = False
|
|
47
47
|
self._or_group: list[FilterCondition] | None = None
|
|
48
|
+
self._page_size: int | None = None
|
|
49
|
+
self._total_results_limit: int | None = None
|
|
48
50
|
|
|
49
51
|
def where(self, property_name: str) -> Self:
|
|
50
52
|
self._finalize_current_or_group()
|
|
@@ -164,27 +166,53 @@ class DataSourceQueryBuilder:
|
|
|
164
166
|
self._sorts.append(sort)
|
|
165
167
|
return self
|
|
166
168
|
|
|
167
|
-
def
|
|
169
|
+
def order_by_property_name_ascending(self, property_name: str) -> Self:
|
|
168
170
|
return self.order_by(property_name, SortDirection.ASCENDING)
|
|
169
171
|
|
|
170
|
-
def
|
|
172
|
+
def order_by_property_name_descending(self, property_name: str) -> Self:
|
|
171
173
|
return self.order_by(property_name, SortDirection.DESCENDING)
|
|
172
174
|
|
|
173
|
-
def
|
|
175
|
+
def order_by_created_time_ascending(self) -> Self:
|
|
176
|
+
return self._order_by_created_time(SortDirection.ASCENDING)
|
|
177
|
+
|
|
178
|
+
def order_by_created_time_descending(self) -> Self:
|
|
179
|
+
return self._order_by_created_time(SortDirection.DESCENDING)
|
|
180
|
+
|
|
181
|
+
def _order_by_created_time(self, direction: SortDirection = SortDirection.DESCENDING) -> Self:
|
|
174
182
|
sort = TimestampSort(timestamp=TimestampType.CREATED_TIME, direction=direction)
|
|
175
183
|
self._sorts.append(sort)
|
|
176
184
|
return self
|
|
177
185
|
|
|
178
|
-
def
|
|
186
|
+
def order_by_last_edited_time_ascending(self) -> Self:
|
|
187
|
+
return self._order_by_last_edited_time(SortDirection.ASCENDING)
|
|
188
|
+
|
|
189
|
+
def order_by_last_edited_time_descending(self) -> Self:
|
|
190
|
+
return self._order_by_last_edited_time(SortDirection.DESCENDING)
|
|
191
|
+
|
|
192
|
+
def _order_by_last_edited_time(self, direction: SortDirection = SortDirection.DESCENDING) -> Self:
|
|
179
193
|
sort = TimestampSort(timestamp=TimestampType.LAST_EDITED_TIME, direction=direction)
|
|
180
194
|
self._sorts.append(sort)
|
|
181
195
|
return self
|
|
182
196
|
|
|
197
|
+
def total_results_limit(self, total_result_limit: int) -> Self:
|
|
198
|
+
if total_result_limit < 1:
|
|
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")
|
|
206
|
+
self._page_size = page_size
|
|
207
|
+
return self
|
|
208
|
+
|
|
183
209
|
def build(self) -> DataSourceQueryParams:
|
|
184
210
|
self._finalize_current_or_group()
|
|
185
211
|
notion_filter = self._create_notion_filter_if_needed()
|
|
186
212
|
sorts = self._create_sorts_if_needed()
|
|
187
|
-
return DataSourceQueryParams(
|
|
213
|
+
return DataSourceQueryParams(
|
|
214
|
+
filter=notion_filter, sorts=sorts, page_size=self._page_size, total_results_limit=self._total_results_limit
|
|
215
|
+
)
|
|
188
216
|
|
|
189
217
|
def _select_property_without_negation(self, property_name: str) -> None:
|
|
190
218
|
self._current_property = property_name
|
|
@@ -273,7 +301,7 @@ class DataSourceQueryBuilder:
|
|
|
273
301
|
|
|
274
302
|
property_obj = self._properties.get(self._current_property)
|
|
275
303
|
if property_obj:
|
|
276
|
-
self.
|
|
304
|
+
self._query_validator.validate_operator_for_property(self._current_property, property_obj, operator)
|
|
277
305
|
return self
|
|
278
306
|
|
|
279
307
|
def _ensure_property_is_selected(self) -> None:
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from enum import StrEnum
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Self
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, ValidationInfo, field_validator, model_serializer, model_validator
|
|
7
7
|
|
|
8
8
|
from notionary.shared.properties.type import PropertyType
|
|
9
|
+
from notionary.shared.typings import JsonDict
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class FieldType(StrEnum):
|
|
@@ -46,7 +47,7 @@ class BooleanOperator(StrEnum):
|
|
|
46
47
|
IS_FALSE = "is_false"
|
|
47
48
|
|
|
48
49
|
|
|
49
|
-
class
|
|
50
|
+
class SelectOperator(StrEnum):
|
|
50
51
|
EQUALS = "equals"
|
|
51
52
|
DOES_NOT_EQUAL = "does_not_equal"
|
|
52
53
|
IS_EMPTY = "is_empty"
|
|
@@ -70,10 +71,6 @@ class ArrayOperator(StrEnum):
|
|
|
70
71
|
IS_NOT_EMPTY = "is_not_empty"
|
|
71
72
|
|
|
72
73
|
|
|
73
|
-
RelationOperator = ArrayOperator
|
|
74
|
-
PeopleOperator = ArrayOperator
|
|
75
|
-
|
|
76
|
-
|
|
77
74
|
class LogicalOperator(StrEnum):
|
|
78
75
|
AND = "and"
|
|
79
76
|
OR = "or"
|
|
@@ -246,7 +243,7 @@ class PropertyFilter(BaseModel):
|
|
|
246
243
|
return self
|
|
247
244
|
|
|
248
245
|
@model_serializer
|
|
249
|
-
def serialize_model(self) ->
|
|
246
|
+
def serialize_model(self) -> JsonDict:
|
|
250
247
|
property_type_str = self.property_type.value
|
|
251
248
|
operator_str = self.operator.value
|
|
252
249
|
filter_value = self.value
|
|
@@ -266,7 +263,7 @@ class CompoundFilter(BaseModel):
|
|
|
266
263
|
filters: list[PropertyFilter | CompoundFilter]
|
|
267
264
|
|
|
268
265
|
@model_serializer
|
|
269
|
-
def serialize_model(self) ->
|
|
266
|
+
def serialize_model(self) -> JsonDict:
|
|
270
267
|
operator_str = self.operator.value
|
|
271
268
|
return {operator_str: [f.model_dump() for f in self.filters]}
|
|
272
269
|
|
|
@@ -290,10 +287,13 @@ type NotionSort = PropertySort | TimestampSort
|
|
|
290
287
|
class DataSourceQueryParams(BaseModel):
|
|
291
288
|
filter: NotionFilter | None = None
|
|
292
289
|
sorts: list[NotionSort] | None = None
|
|
290
|
+
page_size: int | None = None
|
|
291
|
+
|
|
292
|
+
total_results_limit: int | None = None
|
|
293
293
|
|
|
294
294
|
@model_serializer
|
|
295
|
-
def
|
|
296
|
-
result:
|
|
295
|
+
def to_api_params(self) -> JsonDict:
|
|
296
|
+
result: JsonDict = {}
|
|
297
297
|
|
|
298
298
|
if self.filter is not None:
|
|
299
299
|
result["filter"] = self.filter.model_dump()
|
|
@@ -301,4 +301,7 @@ class DataSourceQueryParams(BaseModel):
|
|
|
301
301
|
if self.sorts is not None and len(self.sorts) > 0:
|
|
302
302
|
result["sorts"] = [sort.model_dump() for sort in self.sorts]
|
|
303
303
|
|
|
304
|
+
if self.page_size is not None:
|
|
305
|
+
result["page_size"] = self.page_size
|
|
306
|
+
|
|
304
307
|
return result
|
|
@@ -1,38 +1,38 @@
|
|
|
1
1
|
from typing import ClassVar
|
|
2
2
|
|
|
3
|
-
from notionary.data_source.properties.
|
|
3
|
+
from notionary.data_source.properties.schemas import DataSourceProperty
|
|
4
4
|
from notionary.data_source.query.schema import (
|
|
5
5
|
ArrayOperator,
|
|
6
6
|
BooleanOperator,
|
|
7
7
|
DateOperator,
|
|
8
8
|
NumberOperator,
|
|
9
9
|
Operator,
|
|
10
|
-
|
|
10
|
+
SelectOperator,
|
|
11
11
|
StringOperator,
|
|
12
12
|
)
|
|
13
13
|
from notionary.exceptions.data_source.builder import InvalidOperatorForPropertyType
|
|
14
14
|
from notionary.shared.properties.type import PropertyType
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
class
|
|
17
|
+
class QueryValidator:
|
|
18
18
|
_PROPERTY_TYPE_OPERATORS: ClassVar[dict[PropertyType, list[type[Operator]]]] = {
|
|
19
19
|
PropertyType.TITLE: [StringOperator],
|
|
20
20
|
PropertyType.RICH_TEXT: [StringOperator],
|
|
21
|
-
PropertyType.NUMBER: [NumberOperator],
|
|
22
|
-
PropertyType.SELECT: [StringOperator],
|
|
23
|
-
PropertyType.MULTI_SELECT: [ArrayOperator],
|
|
24
|
-
PropertyType.STATUS: [StatusOperator],
|
|
25
|
-
PropertyType.DATE: [DateOperator],
|
|
26
|
-
PropertyType.PEOPLE: [ArrayOperator],
|
|
27
|
-
PropertyType.CHECKBOX: [BooleanOperator],
|
|
28
21
|
PropertyType.URL: [StringOperator],
|
|
29
22
|
PropertyType.EMAIL: [StringOperator],
|
|
30
23
|
PropertyType.PHONE_NUMBER: [StringOperator],
|
|
24
|
+
PropertyType.SELECT: [SelectOperator],
|
|
25
|
+
PropertyType.STATUS: [SelectOperator],
|
|
26
|
+
PropertyType.MULTI_SELECT: [ArrayOperator],
|
|
27
|
+
PropertyType.NUMBER: [NumberOperator],
|
|
28
|
+
PropertyType.DATE: [DateOperator],
|
|
31
29
|
PropertyType.CREATED_TIME: [DateOperator],
|
|
32
|
-
PropertyType.CREATED_BY: [ArrayOperator],
|
|
33
30
|
PropertyType.LAST_EDITED_TIME: [DateOperator],
|
|
31
|
+
PropertyType.PEOPLE: [ArrayOperator],
|
|
32
|
+
PropertyType.CREATED_BY: [ArrayOperator],
|
|
34
33
|
PropertyType.LAST_EDITED_BY: [ArrayOperator],
|
|
35
34
|
PropertyType.RELATION: [ArrayOperator],
|
|
35
|
+
PropertyType.CHECKBOX: [BooleanOperator],
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
def validate_operator_for_property(
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from notionary.shared.properties.type import PropertyType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class PropertyTypeDescriptor:
|
|
8
|
+
display_name: str
|
|
9
|
+
description: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DatabasePropertyTypeDescriptorRegistry:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self._DESCRIPTORS = {
|
|
15
|
+
PropertyType.TITLE: PropertyTypeDescriptor(
|
|
16
|
+
display_name="Title", description="Required field for the main heading of the entry"
|
|
17
|
+
),
|
|
18
|
+
PropertyType.RICH_TEXT: PropertyTypeDescriptor(
|
|
19
|
+
display_name="Rich Text", description="Free-form text field for additional information"
|
|
20
|
+
),
|
|
21
|
+
PropertyType.NUMBER: PropertyTypeDescriptor(display_name="Number", description="Numeric value field"),
|
|
22
|
+
PropertyType.CHECKBOX: PropertyTypeDescriptor(
|
|
23
|
+
display_name="Checkbox", description="Boolean value (true/false)"
|
|
24
|
+
),
|
|
25
|
+
PropertyType.DATE: PropertyTypeDescriptor(display_name="Date", description="Date or date range field"),
|
|
26
|
+
PropertyType.URL: PropertyTypeDescriptor(display_name="URL", description="Web address field"),
|
|
27
|
+
PropertyType.EMAIL: PropertyTypeDescriptor(display_name="Email", description="Email address field"),
|
|
28
|
+
PropertyType.PHONE_NUMBER: PropertyTypeDescriptor(
|
|
29
|
+
display_name="Phone Number", description="Phone number field"
|
|
30
|
+
),
|
|
31
|
+
PropertyType.FILES: PropertyTypeDescriptor(
|
|
32
|
+
display_name="Files & Media", description="Upload or link to files"
|
|
33
|
+
),
|
|
34
|
+
PropertyType.PEOPLE: PropertyTypeDescriptor(display_name="People", description="Reference to Notion users"),
|
|
35
|
+
PropertyType.SELECT: PropertyTypeDescriptor(
|
|
36
|
+
display_name="Single Select", description="Choose one option from available choices"
|
|
37
|
+
),
|
|
38
|
+
PropertyType.MULTI_SELECT: PropertyTypeDescriptor(
|
|
39
|
+
display_name="Multi Select", description="Choose multiple options from available choices"
|
|
40
|
+
),
|
|
41
|
+
PropertyType.STATUS: PropertyTypeDescriptor(
|
|
42
|
+
display_name="Status", description="Track status with predefined options"
|
|
43
|
+
),
|
|
44
|
+
PropertyType.RELATION: PropertyTypeDescriptor(
|
|
45
|
+
display_name="Relation", description="Link to entries in another database"
|
|
46
|
+
),
|
|
47
|
+
PropertyType.CREATED_TIME: PropertyTypeDescriptor(
|
|
48
|
+
display_name="Created Time",
|
|
49
|
+
description="Automatically set when the page is created",
|
|
50
|
+
),
|
|
51
|
+
PropertyType.CREATED_BY: PropertyTypeDescriptor(
|
|
52
|
+
display_name="Created By",
|
|
53
|
+
description="Automatically set to the user who created the page",
|
|
54
|
+
),
|
|
55
|
+
PropertyType.LAST_EDITED_TIME: PropertyTypeDescriptor(
|
|
56
|
+
display_name="Last Edited Time",
|
|
57
|
+
description="Automatically updated when the page is modified",
|
|
58
|
+
),
|
|
59
|
+
PropertyType.LAST_EDITED_BY: PropertyTypeDescriptor(
|
|
60
|
+
display_name="Last Edited By",
|
|
61
|
+
description="Automatically set to the user who last edited the page",
|
|
62
|
+
),
|
|
63
|
+
PropertyType.LAST_VISITED_TIME: PropertyTypeDescriptor(
|
|
64
|
+
display_name="Last Visited Time",
|
|
65
|
+
description="Automatically updated when the page is visited",
|
|
66
|
+
),
|
|
67
|
+
PropertyType.FORMULA: PropertyTypeDescriptor(
|
|
68
|
+
display_name="Formula",
|
|
69
|
+
description="Computed value based on other properties",
|
|
70
|
+
),
|
|
71
|
+
PropertyType.ROLLUP: PropertyTypeDescriptor(
|
|
72
|
+
display_name="Rollup",
|
|
73
|
+
description="Aggregate values from related database entries",
|
|
74
|
+
),
|
|
75
|
+
PropertyType.BUTTON: PropertyTypeDescriptor(
|
|
76
|
+
display_name="Button",
|
|
77
|
+
description="Interactive button that triggers an action",
|
|
78
|
+
),
|
|
79
|
+
PropertyType.LOCATION: PropertyTypeDescriptor(
|
|
80
|
+
display_name="Location",
|
|
81
|
+
description="Geographic location field",
|
|
82
|
+
),
|
|
83
|
+
PropertyType.PLACE: PropertyTypeDescriptor(
|
|
84
|
+
display_name="Place",
|
|
85
|
+
description="Place or venue information",
|
|
86
|
+
),
|
|
87
|
+
PropertyType.VERIFICATION: PropertyTypeDescriptor(
|
|
88
|
+
display_name="Verification",
|
|
89
|
+
description="Verification status field",
|
|
90
|
+
),
|
|
91
|
+
PropertyType.UNIQUE_ID: PropertyTypeDescriptor(
|
|
92
|
+
display_name="Unique ID",
|
|
93
|
+
description="Auto-generated unique identifier",
|
|
94
|
+
),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
def get_descriptor(self, property_type: PropertyType) -> PropertyTypeDescriptor:
|
|
98
|
+
return self._DESCRIPTORS.get(
|
|
99
|
+
property_type,
|
|
100
|
+
PropertyTypeDescriptor(display_name=self._format_unknown_type_name(property_type), description=""),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def _format_unknown_type_name(self, property_type: PropertyType) -> str:
|
|
104
|
+
return property_type.value.replace("_", " ").title()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from collections.abc import Awaitable, Callable
|
|
2
|
+
|
|
3
|
+
from notionary.blocks.rich_text.name_id_resolver import DataSourceNameIdResolver
|
|
4
|
+
from notionary.data_source.properties.schemas import (
|
|
5
|
+
DataSourceMultiSelectProperty,
|
|
6
|
+
DataSourceProperty,
|
|
7
|
+
DataSourceRelationProperty,
|
|
8
|
+
DataSourceSelectProperty,
|
|
9
|
+
DataSourceStatusProperty,
|
|
10
|
+
)
|
|
11
|
+
from notionary.data_source.schema.registry import DatabasePropertyTypeDescriptorRegistry, PropertyTypeDescriptor
|
|
12
|
+
from notionary.shared.properties.type import PropertyType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PropertyFormatter:
|
|
16
|
+
INDENTATION = " - "
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
relation_options_fetcher: Callable[[DataSourceRelationProperty], Awaitable[list[str]]],
|
|
21
|
+
type_descriptor_registry: DatabasePropertyTypeDescriptorRegistry | None = None,
|
|
22
|
+
data_source_resolver: DataSourceNameIdResolver | None = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
self._relation_options_fetcher = relation_options_fetcher
|
|
25
|
+
self._type_descriptor_registry = type_descriptor_registry or DatabasePropertyTypeDescriptorRegistry()
|
|
26
|
+
self._data_source_resolver = data_source_resolver or DataSourceNameIdResolver()
|
|
27
|
+
|
|
28
|
+
async def format_property(self, prop: DataSourceProperty) -> list[str]:
|
|
29
|
+
specific_details = await self._format_property_specific_details(prop)
|
|
30
|
+
|
|
31
|
+
if specific_details:
|
|
32
|
+
return [*specific_details, *self._format_custom_description(prop)]
|
|
33
|
+
|
|
34
|
+
descriptor = self._type_descriptor_registry.get_descriptor(prop.type)
|
|
35
|
+
return [*self._format_property_description(descriptor), *self._format_custom_description(prop)]
|
|
36
|
+
|
|
37
|
+
def _format_property_description(self, descriptor: PropertyTypeDescriptor) -> list[str]:
|
|
38
|
+
if not descriptor.description:
|
|
39
|
+
return []
|
|
40
|
+
return [f"{self.INDENTATION}{descriptor.description}"]
|
|
41
|
+
|
|
42
|
+
async def _format_property_specific_details(self, prop: DataSourceProperty) -> list[str]:
|
|
43
|
+
if isinstance(prop, DataSourceSelectProperty):
|
|
44
|
+
return self._format_available_options("Choose one option from", prop.option_names)
|
|
45
|
+
|
|
46
|
+
if isinstance(prop, DataSourceMultiSelectProperty):
|
|
47
|
+
return self._format_available_options("Choose multiple options from", prop.option_names)
|
|
48
|
+
|
|
49
|
+
if isinstance(prop, DataSourceStatusProperty):
|
|
50
|
+
return self._format_available_options("Available statuses", prop.option_names)
|
|
51
|
+
|
|
52
|
+
if isinstance(prop, DataSourceRelationProperty):
|
|
53
|
+
return await self._format_relation_details(prop)
|
|
54
|
+
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
def _format_custom_description(self, prop: DataSourceProperty) -> list[str]:
|
|
58
|
+
if not prop.description:
|
|
59
|
+
return []
|
|
60
|
+
return [f"{self.INDENTATION}Description: {prop.description}"]
|
|
61
|
+
|
|
62
|
+
def _format_available_options(self, label: str, options: list[str]) -> list[str]:
|
|
63
|
+
options_text = ", ".join(options)
|
|
64
|
+
return [f"{self.INDENTATION}{label}: {options_text}"]
|
|
65
|
+
|
|
66
|
+
async def _format_relation_details(self, prop: DataSourceRelationProperty) -> list[str]:
|
|
67
|
+
if not prop.related_data_source_id:
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
data_source_name = await self._data_source_resolver.resolve_id_to_name(prop.related_data_source_id)
|
|
71
|
+
data_source_display = data_source_name or prop.related_data_source_id
|
|
72
|
+
lines = [f"{self.INDENTATION}Links to datasource: {data_source_display}"]
|
|
73
|
+
|
|
74
|
+
available_entries = await self._fetch_relation_entries(prop)
|
|
75
|
+
if available_entries:
|
|
76
|
+
entries_text = ", ".join(available_entries)
|
|
77
|
+
lines.append(f"{self.INDENTATION}Available entries: {entries_text}")
|
|
78
|
+
|
|
79
|
+
return lines
|
|
80
|
+
|
|
81
|
+
async def _fetch_relation_entries(self, prop: DataSourceRelationProperty) -> list[str] | None:
|
|
82
|
+
try:
|
|
83
|
+
return await self._relation_options_fetcher(prop)
|
|
84
|
+
except Exception:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class DataSourcePropertySchemaFormatter:
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
relation_options_fetcher: Callable[[DataSourceRelationProperty], Awaitable[list[str]]] | None = None,
|
|
92
|
+
data_source_resolver: DataSourceNameIdResolver | None = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
self._property_formatter = PropertyFormatter(
|
|
95
|
+
relation_options_fetcher, data_source_resolver=data_source_resolver
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
async def format(self, title: str, description: str | None, properties: dict[str, DataSourceProperty]) -> str:
|
|
99
|
+
lines = self._format_header(title, description)
|
|
100
|
+
lines.append("Properties:")
|
|
101
|
+
lines.append("")
|
|
102
|
+
lines.extend(await self._format_properties(properties))
|
|
103
|
+
|
|
104
|
+
return "\n".join(lines)
|
|
105
|
+
|
|
106
|
+
def _format_header(self, title: str, description: str | None) -> list[str]:
|
|
107
|
+
lines = [f"Data Source: {title}", ""]
|
|
108
|
+
|
|
109
|
+
if description:
|
|
110
|
+
lines.append(f"Description: {description}")
|
|
111
|
+
lines.append("")
|
|
112
|
+
|
|
113
|
+
return lines
|
|
114
|
+
|
|
115
|
+
async def _format_properties(self, properties: dict[str, DataSourceProperty]) -> list[str]:
|
|
116
|
+
lines = []
|
|
117
|
+
sorted_properties = self._sort_with_title_first(properties)
|
|
118
|
+
|
|
119
|
+
for index, (name, prop) in enumerate(sorted_properties, start=1):
|
|
120
|
+
lines.extend(await self._format_single_property(index, name, prop))
|
|
121
|
+
|
|
122
|
+
return lines
|
|
123
|
+
|
|
124
|
+
def _sort_with_title_first(self, properties: dict[str, DataSourceProperty]) -> list[tuple[str, DataSourceProperty]]:
|
|
125
|
+
return sorted(properties.items(), key=lambda item: (self._is_not_title_property(item[1]), item[0]))
|
|
126
|
+
|
|
127
|
+
def _is_not_title_property(self, prop: DataSourceProperty) -> bool:
|
|
128
|
+
return prop.type != PropertyType.TITLE
|
|
129
|
+
|
|
130
|
+
async def _format_single_property(self, index: int, name: str, prop: DataSourceProperty) -> list[str]:
|
|
131
|
+
lines = [f"{index}. - Property Name: '{name}'", f" - Property Type: '{prop.type.value}'"]
|
|
132
|
+
|
|
133
|
+
lines.extend(await self._property_formatter.format_property(prop))
|
|
134
|
+
lines.append("")
|
|
135
|
+
|
|
136
|
+
return lines
|
notionary/data_source/schemas.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from pydantic import BaseModel
|
|
2
2
|
|
|
3
3
|
from notionary.blocks.rich_text.models import RichText
|
|
4
|
-
from notionary.data_source.properties.
|
|
4
|
+
from notionary.data_source.properties.schemas import DiscriminatedDataSourceProperty
|
|
5
5
|
from notionary.page.schemas import NotionPageDto
|
|
6
6
|
from notionary.shared.entity.schemas import EntityResponseDto, NotionEntityUpdateDto
|
|
7
7
|
from notionary.shared.models.parent import Parent
|