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.
Files changed (149) hide show
  1. notionary/__init__.py +9 -2
  2. notionary/blocks/__init__.py +5 -0
  3. notionary/blocks/client.py +6 -4
  4. notionary/blocks/enums.py +28 -1
  5. notionary/blocks/rich_text/markdown_rich_text_converter.py +14 -0
  6. notionary/blocks/rich_text/models.py +14 -0
  7. notionary/blocks/rich_text/name_id_resolver/__init__.py +2 -0
  8. notionary/blocks/rich_text/name_id_resolver/data_source.py +32 -0
  9. notionary/blocks/rich_text/rich_text_markdown_converter.py +12 -0
  10. notionary/blocks/rich_text/rich_text_patterns.py +3 -0
  11. notionary/blocks/schemas.py +42 -10
  12. notionary/comments/__init__.py +5 -0
  13. notionary/comments/client.py +7 -10
  14. notionary/comments/factory.py +4 -6
  15. notionary/data_source/http/data_source_instance_client.py +14 -4
  16. notionary/data_source/properties/{models.py → schemas.py} +4 -8
  17. notionary/data_source/query/__init__.py +9 -0
  18. notionary/data_source/query/builder.py +38 -10
  19. notionary/data_source/query/schema.py +13 -10
  20. notionary/data_source/query/validator.py +11 -11
  21. notionary/data_source/schema/registry.py +104 -0
  22. notionary/data_source/schema/service.py +136 -0
  23. notionary/data_source/schemas.py +1 -1
  24. notionary/data_source/service.py +29 -103
  25. notionary/database/service.py +17 -60
  26. notionary/exceptions/__init__.py +5 -1
  27. notionary/exceptions/block_parsing.py +21 -0
  28. notionary/exceptions/search.py +24 -0
  29. notionary/http/client.py +9 -10
  30. notionary/http/models.py +5 -4
  31. notionary/page/content/factory.py +10 -3
  32. notionary/page/content/markdown/builder.py +76 -154
  33. notionary/page/content/markdown/nodes/__init__.py +0 -2
  34. notionary/page/content/markdown/nodes/audio.py +1 -1
  35. notionary/page/content/markdown/nodes/base.py +1 -1
  36. notionary/page/content/markdown/nodes/bookmark.py +1 -1
  37. notionary/page/content/markdown/nodes/breadcrumb.py +1 -1
  38. notionary/page/content/markdown/nodes/bulleted_list.py +31 -8
  39. notionary/page/content/markdown/nodes/callout.py +12 -10
  40. notionary/page/content/markdown/nodes/code.py +3 -5
  41. notionary/page/content/markdown/nodes/columns.py +39 -21
  42. notionary/page/content/markdown/nodes/container.py +64 -0
  43. notionary/page/content/markdown/nodes/divider.py +1 -1
  44. notionary/page/content/markdown/nodes/embed.py +1 -1
  45. notionary/page/content/markdown/nodes/equation.py +1 -1
  46. notionary/page/content/markdown/nodes/file.py +1 -1
  47. notionary/page/content/markdown/nodes/heading.py +26 -6
  48. notionary/page/content/markdown/nodes/image.py +1 -1
  49. notionary/page/content/markdown/nodes/mixins/__init__.py +5 -0
  50. notionary/page/content/markdown/nodes/mixins/caption.py +1 -1
  51. notionary/page/content/markdown/nodes/numbered_list.py +28 -5
  52. notionary/page/content/markdown/nodes/paragraph.py +1 -1
  53. notionary/page/content/markdown/nodes/pdf.py +1 -1
  54. notionary/page/content/markdown/nodes/quote.py +17 -5
  55. notionary/page/content/markdown/nodes/space.py +1 -1
  56. notionary/page/content/markdown/nodes/table.py +1 -1
  57. notionary/page/content/markdown/nodes/table_of_contents.py +1 -1
  58. notionary/page/content/markdown/nodes/todo.py +23 -7
  59. notionary/page/content/markdown/nodes/toggle.py +13 -14
  60. notionary/page/content/markdown/nodes/video.py +1 -1
  61. notionary/page/content/parser/context.py +98 -21
  62. notionary/page/content/parser/factory.py +1 -10
  63. notionary/page/content/parser/parsers/__init__.py +0 -2
  64. notionary/page/content/parser/parsers/audio.py +1 -1
  65. notionary/page/content/parser/parsers/base.py +1 -1
  66. notionary/page/content/parser/parsers/bookmark.py +1 -1
  67. notionary/page/content/parser/parsers/breadcrumb.py +1 -1
  68. notionary/page/content/parser/parsers/bulleted_list.py +52 -8
  69. notionary/page/content/parser/parsers/callout.py +55 -84
  70. notionary/page/content/parser/parsers/caption.py +1 -1
  71. notionary/page/content/parser/parsers/code.py +5 -5
  72. notionary/page/content/parser/parsers/column.py +23 -64
  73. notionary/page/content/parser/parsers/column_list.py +45 -45
  74. notionary/page/content/parser/parsers/divider.py +1 -1
  75. notionary/page/content/parser/parsers/embed.py +1 -1
  76. notionary/page/content/parser/parsers/equation.py +1 -1
  77. notionary/page/content/parser/parsers/file.py +1 -1
  78. notionary/page/content/parser/parsers/heading.py +65 -8
  79. notionary/page/content/parser/parsers/image.py +1 -1
  80. notionary/page/content/parser/parsers/numbered_list.py +52 -8
  81. notionary/page/content/parser/parsers/paragraph.py +3 -2
  82. notionary/page/content/parser/parsers/pdf.py +1 -1
  83. notionary/page/content/parser/parsers/quote.py +75 -15
  84. notionary/page/content/parser/parsers/space.py +14 -8
  85. notionary/page/content/parser/parsers/table.py +1 -1
  86. notionary/page/content/parser/parsers/table_of_contents.py +1 -1
  87. notionary/page/content/parser/parsers/todo.py +57 -19
  88. notionary/page/content/parser/parsers/toggle.py +17 -74
  89. notionary/page/content/parser/parsers/video.py +1 -1
  90. notionary/page/content/parser/post_processing/handlers/rich_text_length.py +6 -4
  91. notionary/page/content/parser/post_processing/handlers/rich_text_length_truncation.py +43 -22
  92. notionary/page/content/parser/pre_processsing/handlers/__init__.py +4 -0
  93. notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +108 -54
  94. notionary/page/content/parser/pre_processsing/handlers/indentation.py +86 -0
  95. notionary/page/content/parser/pre_processsing/handlers/video_syntax.py +66 -0
  96. notionary/page/content/parser/pre_processsing/handlers/whitespace.py +14 -7
  97. notionary/page/content/parser/service.py +9 -0
  98. notionary/page/content/renderer/context.py +5 -2
  99. notionary/page/content/renderer/factory.py +2 -11
  100. notionary/page/content/renderer/post_processing/handlers/__init__.py +2 -2
  101. notionary/page/content/renderer/post_processing/handlers/numbered_list.py +156 -0
  102. notionary/page/content/renderer/renderers/__init__.py +0 -2
  103. notionary/page/content/renderer/renderers/base.py +1 -1
  104. notionary/page/content/renderer/renderers/bulleted_list.py +1 -1
  105. notionary/page/content/renderer/renderers/callout.py +6 -21
  106. notionary/page/content/renderer/renderers/captioned_block.py +1 -1
  107. notionary/page/content/renderer/renderers/column.py +28 -19
  108. notionary/page/content/renderer/renderers/column_list.py +24 -11
  109. notionary/page/content/renderer/renderers/heading.py +53 -27
  110. notionary/page/content/renderer/renderers/numbered_list.py +6 -5
  111. notionary/page/content/renderer/renderers/quote.py +1 -1
  112. notionary/page/content/renderer/renderers/todo.py +1 -1
  113. notionary/page/content/renderer/renderers/toggle.py +6 -7
  114. notionary/page/content/service.py +4 -1
  115. notionary/page/content/syntax/__init__.py +4 -0
  116. notionary/page/content/syntax/grammar.py +10 -0
  117. notionary/page/content/syntax/models.py +0 -2
  118. notionary/page/content/syntax/{service.py → registry.py} +31 -91
  119. notionary/page/properties/client.py +3 -3
  120. notionary/page/properties/models.py +3 -2
  121. notionary/page/properties/service.py +18 -3
  122. notionary/page/service.py +22 -80
  123. notionary/shared/entity/service.py +94 -36
  124. notionary/shared/models/cover.py +1 -1
  125. notionary/shared/typings.py +3 -0
  126. notionary/user/base.py +60 -11
  127. notionary/user/factory.py +0 -0
  128. notionary/utils/decorators.py +122 -0
  129. notionary/utils/fuzzy.py +18 -6
  130. notionary/utils/mixins/logging.py +38 -27
  131. notionary/utils/pagination.py +70 -16
  132. notionary/workspace/__init__.py +2 -1
  133. notionary/workspace/client.py +4 -2
  134. notionary/workspace/query/__init__.py +3 -0
  135. notionary/workspace/query/builder.py +25 -1
  136. notionary/workspace/query/models.py +12 -3
  137. notionary/workspace/query/service.py +57 -32
  138. notionary/workspace/service.py +31 -21
  139. {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/METADATA +35 -105
  140. notionary-0.3.1.dist-info/RECORD +211 -0
  141. notionary/page/content/markdown/nodes/toggleable_heading.py +0 -35
  142. notionary/page/content/parser/parsers/toggleable_heading.py +0 -150
  143. notionary/page/content/renderer/post_processing/handlers/numbered_list_placeholdere.py +0 -62
  144. notionary/page/content/renderer/renderers/toggleable_heading.py +0 -78
  145. notionary/utils/async_retry.py +0 -39
  146. notionary/utils/singleton.py +0 -13
  147. notionary-0.2.28.dist-info/RECORD +0 -200
  148. {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/WHEEL +0 -0
  149. {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.models import DataSourceProperty
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 OperatorValidator
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
- operator_validator: OperatorValidator | None = None,
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._operator_validator = operator_validator or OperatorValidator()
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 order_by_ascending(self, property_name: str) -> Self:
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 order_by_descending(self, property_name: str) -> Self:
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 order_by_created_time(self, direction: SortDirection = SortDirection.DESCENDING) -> Self:
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 order_by_last_edited_time(self, direction: SortDirection = SortDirection.DESCENDING) -> Self:
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(filter=notion_filter, sorts=sorts)
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._operator_validator.validate_operator_for_property(self._current_property, property_obj, operator)
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 Any, Self
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 StatusOperator(StrEnum):
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) -> dict[str, Any]:
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) -> dict[str, Any]:
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 serialize_model(self) -> dict[str, Any]:
296
- result: dict[str, Any] = {}
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.models import DataSourceProperty
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
- StatusOperator,
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 OperatorValidator:
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
@@ -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.models import DiscriminatedDataSourceProperty
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