notionary 0.2.27__py3-none-any.whl → 0.3.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.
- notionary/__init__.py +5 -20
- notionary/blocks/__init__.py +4 -4
- notionary/blocks/client.py +90 -216
- notionary/blocks/enums.py +167 -0
- notionary/blocks/rich_text/markdown_rich_text_converter.py +280 -0
- notionary/blocks/rich_text/models.py +178 -0
- notionary/blocks/rich_text/name_id_resolver/__init__.py +13 -0
- notionary/blocks/rich_text/name_id_resolver/data_source.py +32 -0
- notionary/blocks/rich_text/name_id_resolver/database.py +31 -0
- notionary/blocks/rich_text/name_id_resolver/page.py +34 -0
- notionary/blocks/rich_text/name_id_resolver/person.py +37 -0
- notionary/blocks/rich_text/name_id_resolver/port.py +11 -0
- notionary/blocks/rich_text/rich_text_markdown_converter.py +144 -0
- notionary/blocks/rich_text/rich_text_patterns.py +42 -0
- notionary/blocks/schemas.py +778 -0
- notionary/comments/__init__.py +1 -22
- notionary/comments/client.py +52 -187
- notionary/comments/factory.py +38 -0
- notionary/comments/models.py +5 -127
- notionary/comments/schemas.py +240 -0
- notionary/comments/service.py +34 -0
- notionary/data_source/http/client.py +11 -0
- notionary/data_source/http/data_source_instance_client.py +104 -0
- notionary/data_source/properties/schemas.py +402 -0
- notionary/data_source/query/builder.py +448 -0
- notionary/data_source/query/resolver.py +114 -0
- notionary/data_source/query/schema.py +302 -0
- notionary/data_source/query/validator.py +73 -0
- notionary/data_source/schema/registry.py +104 -0
- notionary/data_source/schema/service.py +136 -0
- notionary/data_source/schemas.py +27 -0
- notionary/data_source/service.py +377 -0
- notionary/database/client.py +30 -135
- notionary/database/database_metadata_update_client.py +19 -0
- notionary/database/schemas.py +29 -0
- notionary/database/service.py +168 -0
- notionary/exceptions/__init__.py +33 -0
- notionary/exceptions/api.py +41 -0
- notionary/exceptions/base.py +2 -0
- notionary/exceptions/block_parsing.py +16 -0
- notionary/exceptions/data_source/__init__.py +6 -0
- notionary/exceptions/data_source/builder.py +182 -0
- notionary/exceptions/data_source/properties.py +34 -0
- notionary/exceptions/properties.py +58 -0
- notionary/exceptions/search.py +57 -0
- notionary/file_upload/client.py +18 -30
- notionary/file_upload/models.py +7 -8
- notionary/file_upload/{notion_file_upload.py → service.py} +29 -64
- notionary/http/client.py +204 -0
- notionary/http/models.py +50 -0
- notionary/page/blocks/client.py +1 -0
- notionary/page/content/factory.py +73 -0
- notionary/page/content/markdown/__init__.py +5 -0
- notionary/page/content/markdown/builder.py +226 -0
- notionary/page/content/markdown/nodes/__init__.py +52 -0
- notionary/page/content/markdown/nodes/audio.py +23 -0
- notionary/page/content/markdown/nodes/base.py +12 -0
- notionary/page/content/markdown/nodes/bookmark.py +25 -0
- notionary/page/content/markdown/nodes/breadcrumb.py +14 -0
- notionary/page/content/markdown/nodes/bulleted_list.py +41 -0
- notionary/page/content/markdown/nodes/callout.py +34 -0
- notionary/page/content/markdown/nodes/code.py +28 -0
- notionary/page/content/markdown/nodes/columns.py +69 -0
- notionary/page/content/markdown/nodes/container.py +64 -0
- notionary/page/content/markdown/nodes/divider.py +14 -0
- notionary/page/content/markdown/nodes/embed.py +23 -0
- notionary/page/content/markdown/nodes/equation.py +19 -0
- notionary/page/content/markdown/nodes/file.py +23 -0
- notionary/page/content/markdown/nodes/heading.py +36 -0
- notionary/page/content/markdown/nodes/image.py +23 -0
- notionary/page/content/markdown/nodes/mixins/__init__.py +5 -0
- notionary/page/content/markdown/nodes/mixins/caption.py +12 -0
- notionary/page/content/markdown/nodes/numbered_list.py +38 -0
- notionary/page/content/markdown/nodes/paragraph.py +14 -0
- notionary/page/content/markdown/nodes/pdf.py +23 -0
- notionary/page/content/markdown/nodes/quote.py +27 -0
- notionary/page/content/markdown/nodes/space.py +14 -0
- notionary/page/content/markdown/nodes/table.py +45 -0
- notionary/page/content/markdown/nodes/table_of_contents.py +14 -0
- notionary/page/content/markdown/nodes/todo.py +38 -0
- notionary/page/content/markdown/nodes/toggle.py +27 -0
- notionary/page/content/markdown/nodes/video.py +23 -0
- notionary/page/content/parser/context.py +126 -0
- notionary/page/content/parser/factory.py +210 -0
- notionary/page/content/parser/parsers/__init__.py +58 -0
- notionary/page/content/parser/parsers/audio.py +40 -0
- notionary/page/content/parser/parsers/base.py +30 -0
- notionary/page/content/parser/parsers/bookmark.py +33 -0
- notionary/page/content/parser/parsers/breadcrumb.py +33 -0
- notionary/page/content/parser/parsers/bulleted_list.py +85 -0
- notionary/page/content/parser/parsers/callout.py +100 -0
- notionary/page/content/parser/parsers/caption.py +55 -0
- notionary/page/content/parser/parsers/code.py +81 -0
- notionary/page/content/parser/parsers/column.py +76 -0
- notionary/page/content/parser/parsers/column_list.py +81 -0
- notionary/page/content/parser/parsers/divider.py +33 -0
- notionary/page/content/parser/parsers/embed.py +33 -0
- notionary/page/content/parser/parsers/equation.py +65 -0
- notionary/page/content/parser/parsers/file.py +42 -0
- notionary/page/content/parser/parsers/heading.py +115 -0
- notionary/page/content/parser/parsers/image.py +42 -0
- notionary/page/content/parser/parsers/numbered_list.py +89 -0
- notionary/page/content/parser/parsers/paragraph.py +37 -0
- notionary/page/content/parser/parsers/pdf.py +42 -0
- notionary/page/content/parser/parsers/quote.py +125 -0
- notionary/page/content/parser/parsers/space.py +41 -0
- notionary/page/content/parser/parsers/table.py +144 -0
- notionary/page/content/parser/parsers/table_of_contents.py +32 -0
- notionary/page/content/parser/parsers/todo.py +96 -0
- notionary/page/content/parser/parsers/toggle.py +70 -0
- notionary/page/content/parser/parsers/video.py +42 -0
- notionary/page/content/parser/post_processing/handlers/__init__.py +5 -0
- notionary/page/content/parser/post_processing/handlers/rich_text_length.py +95 -0
- notionary/page/content/parser/post_processing/handlers/rich_text_length_truncation.py +114 -0
- notionary/page/content/parser/post_processing/port.py +9 -0
- notionary/page/content/parser/post_processing/service.py +16 -0
- notionary/page/content/parser/pre_processsing/handlers/__init__.py +11 -0
- notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +130 -0
- notionary/page/content/parser/pre_processsing/handlers/indentation.py +84 -0
- notionary/page/content/parser/pre_processsing/handlers/port.py +7 -0
- notionary/page/content/parser/pre_processsing/handlers/whitespace.py +73 -0
- notionary/page/content/parser/pre_processsing/service.py +15 -0
- notionary/page/content/parser/service.py +78 -0
- notionary/page/content/renderer/context.py +51 -0
- notionary/page/content/renderer/factory.py +231 -0
- notionary/page/content/renderer/post_processing/handlers/__init__.py +5 -0
- notionary/page/content/renderer/post_processing/handlers/numbered_list.py +156 -0
- notionary/page/content/renderer/post_processing/port.py +7 -0
- notionary/page/content/renderer/post_processing/service.py +15 -0
- notionary/page/content/renderer/renderers/__init__.py +55 -0
- notionary/page/content/renderer/renderers/audio.py +31 -0
- notionary/page/content/renderer/renderers/base.py +31 -0
- notionary/page/content/renderer/renderers/bookmark.py +25 -0
- notionary/page/content/renderer/renderers/breadcrumb.py +21 -0
- notionary/page/content/renderer/renderers/bulleted_list.py +48 -0
- notionary/page/content/renderer/renderers/callout.py +50 -0
- notionary/page/content/renderer/renderers/captioned_block.py +58 -0
- notionary/page/content/renderer/renderers/code.py +34 -0
- notionary/page/content/renderer/renderers/column.py +53 -0
- notionary/page/content/renderer/renderers/column_list.py +44 -0
- notionary/page/content/renderer/renderers/divider.py +22 -0
- notionary/page/content/renderer/renderers/embed.py +25 -0
- notionary/page/content/renderer/renderers/equation.py +37 -0
- notionary/page/content/renderer/renderers/fallback.py +24 -0
- notionary/page/content/renderer/renderers/file.py +40 -0
- notionary/page/content/renderer/renderers/heading.py +95 -0
- notionary/page/content/renderer/renderers/image.py +31 -0
- notionary/page/content/renderer/renderers/numbered_list.py +42 -0
- notionary/page/content/renderer/renderers/paragraph.py +40 -0
- notionary/page/content/renderer/renderers/pdf.py +31 -0
- notionary/page/content/renderer/renderers/quote.py +49 -0
- notionary/page/content/renderer/renderers/table.py +115 -0
- notionary/page/content/renderer/renderers/table_of_contents.py +26 -0
- notionary/page/content/renderer/renderers/table_row.py +17 -0
- notionary/page/content/renderer/renderers/todo.py +56 -0
- notionary/page/content/renderer/renderers/toggle.py +52 -0
- notionary/page/content/renderer/renderers/video.py +31 -0
- notionary/page/content/renderer/service.py +50 -0
- notionary/page/content/service.py +68 -0
- notionary/page/content/syntax/__init__.py +4 -0
- notionary/page/content/syntax/grammar.py +10 -0
- notionary/page/content/syntax/models.py +66 -0
- notionary/page/content/syntax/registry.py +393 -0
- notionary/page/page_context.py +7 -16
- notionary/page/page_http_client.py +15 -0
- notionary/page/page_metadata_update_client.py +19 -0
- notionary/page/properties/client.py +144 -0
- notionary/page/properties/factory.py +26 -0
- notionary/page/properties/models.py +308 -0
- notionary/page/properties/service.py +261 -0
- notionary/page/schemas.py +13 -0
- notionary/page/service.py +225 -0
- notionary/shared/entity/client.py +29 -0
- notionary/shared/entity/dto_parsers.py +53 -0
- notionary/shared/entity/entity_metadata_update_client.py +41 -0
- notionary/shared/entity/schemas.py +45 -0
- notionary/shared/entity/service.py +171 -0
- notionary/shared/models/cover.py +20 -0
- notionary/shared/models/file.py +21 -0
- notionary/shared/models/icon.py +28 -0
- notionary/shared/models/parent.py +41 -0
- notionary/shared/properties/type.py +30 -0
- notionary/shared/typings.py +3 -0
- notionary/user/__init__.py +4 -8
- notionary/user/base.py +138 -0
- notionary/user/bot.py +70 -0
- notionary/user/client.py +22 -111
- notionary/user/person.py +41 -0
- notionary/user/schemas.py +67 -0
- notionary/user/service.py +65 -0
- notionary/utils/date.py +51 -0
- notionary/utils/decorators.py +122 -0
- notionary/utils/fuzzy.py +68 -0
- notionary/utils/mixins/logging.py +58 -0
- notionary/utils/pagination.py +100 -0
- notionary/utils/uuid_utils.py +20 -0
- notionary/workspace/__init__.py +4 -0
- notionary/workspace/client.py +62 -0
- notionary/workspace/query/__init__.py +3 -0
- notionary/workspace/query/builder.py +60 -0
- notionary/workspace/query/models.py +61 -0
- notionary/workspace/query/service.py +100 -0
- notionary/workspace/schemas.py +21 -0
- notionary/workspace/service.py +116 -0
- notionary-0.3.0.dist-info/METADATA +201 -0
- notionary-0.3.0.dist-info/RECORD +209 -0
- {notionary-0.2.27.dist-info → notionary-0.3.0.dist-info}/WHEEL +1 -1
- {notionary-0.2.27.dist-info → notionary-0.3.0.dist-info/licenses}/LICENSE +9 -9
- notionary/base_notion_client.py +0 -219
- notionary/blocks/_bootstrap.py +0 -271
- notionary/blocks/audio/__init__.py +0 -11
- notionary/blocks/audio/audio_element.py +0 -158
- notionary/blocks/audio/audio_markdown_node.py +0 -24
- notionary/blocks/audio/audio_models.py +0 -10
- notionary/blocks/base_block_element.py +0 -42
- notionary/blocks/bookmark/__init__.py +0 -12
- notionary/blocks/bookmark/bookmark_element.py +0 -83
- notionary/blocks/bookmark/bookmark_markdown_node.py +0 -28
- notionary/blocks/bookmark/bookmark_models.py +0 -15
- notionary/blocks/breadcrumbs/__init__.py +0 -15
- notionary/blocks/breadcrumbs/breadcrumb_element.py +0 -39
- notionary/blocks/breadcrumbs/breadcrumb_markdown_node.py +0 -13
- notionary/blocks/breadcrumbs/breadcrumb_models.py +0 -12
- notionary/blocks/bulleted_list/__init__.py +0 -15
- notionary/blocks/bulleted_list/bulleted_list_element.py +0 -74
- notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +0 -20
- notionary/blocks/bulleted_list/bulleted_list_models.py +0 -17
- notionary/blocks/callout/__init__.py +0 -12
- notionary/blocks/callout/callout_element.py +0 -99
- notionary/blocks/callout/callout_markdown_node.py +0 -19
- notionary/blocks/callout/callout_models.py +0 -33
- notionary/blocks/child_database/__init__.py +0 -14
- notionary/blocks/child_database/child_database_element.py +0 -59
- notionary/blocks/child_database/child_database_models.py +0 -12
- notionary/blocks/child_page/__init__.py +0 -9
- notionary/blocks/child_page/child_page_element.py +0 -94
- notionary/blocks/child_page/child_page_models.py +0 -12
- notionary/blocks/code/__init__.py +0 -11
- notionary/blocks/code/code_element.py +0 -149
- notionary/blocks/code/code_markdown_node.py +0 -80
- notionary/blocks/code/code_models.py +0 -94
- notionary/blocks/column/__init__.py +0 -25
- notionary/blocks/column/column_element.py +0 -65
- notionary/blocks/column/column_list_element.py +0 -52
- notionary/blocks/column/column_list_markdown_node.py +0 -34
- notionary/blocks/column/column_markdown_node.py +0 -42
- notionary/blocks/column/column_models.py +0 -26
- notionary/blocks/divider/__init__.py +0 -12
- notionary/blocks/divider/divider_element.py +0 -41
- notionary/blocks/divider/divider_markdown_node.py +0 -11
- notionary/blocks/divider/divider_models.py +0 -12
- notionary/blocks/embed/__init__.py +0 -12
- notionary/blocks/embed/embed_element.py +0 -98
- notionary/blocks/embed/embed_markdown_node.py +0 -19
- notionary/blocks/embed/embed_models.py +0 -14
- notionary/blocks/equation/__init__.py +0 -13
- notionary/blocks/equation/equation_element.py +0 -133
- notionary/blocks/equation/equation_element_markdown_node.py +0 -23
- notionary/blocks/equation/equation_models.py +0 -11
- notionary/blocks/file/__init__.py +0 -23
- notionary/blocks/file/file_element.py +0 -133
- notionary/blocks/file/file_element_markdown_node.py +0 -24
- notionary/blocks/file/file_element_models.py +0 -39
- notionary/blocks/heading/__init__.py +0 -19
- notionary/blocks/heading/heading_element.py +0 -112
- notionary/blocks/heading/heading_markdown_node.py +0 -16
- notionary/blocks/heading/heading_models.py +0 -29
- notionary/blocks/image_block/__init__.py +0 -11
- notionary/blocks/image_block/image_element.py +0 -130
- notionary/blocks/image_block/image_markdown_node.py +0 -25
- notionary/blocks/image_block/image_models.py +0 -10
- notionary/blocks/markdown/markdown_builder.py +0 -525
- notionary/blocks/markdown/markdown_document_model.py +0 -0
- notionary/blocks/markdown/markdown_node.py +0 -25
- notionary/blocks/mixins/captions/__init__.py +0 -4
- notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +0 -31
- notionary/blocks/mixins/captions/caption_mixin.py +0 -92
- notionary/blocks/mixins/file_upload/__init__.py +0 -3
- notionary/blocks/mixins/file_upload/file_upload_mixin.py +0 -320
- notionary/blocks/models.py +0 -174
- notionary/blocks/numbered_list/__init__.py +0 -16
- notionary/blocks/numbered_list/numbered_list_element.py +0 -65
- notionary/blocks/numbered_list/numbered_list_markdown_node.py +0 -17
- notionary/blocks/numbered_list/numbered_list_models.py +0 -17
- notionary/blocks/paragraph/__init__.py +0 -15
- notionary/blocks/paragraph/paragraph_element.py +0 -58
- notionary/blocks/paragraph/paragraph_markdown_node.py +0 -16
- notionary/blocks/paragraph/paragraph_models.py +0 -16
- notionary/blocks/pdf/__init__.py +0 -11
- notionary/blocks/pdf/pdf_element.py +0 -146
- notionary/blocks/pdf/pdf_markdown_node.py +0 -24
- notionary/blocks/pdf/pdf_models.py +0 -11
- notionary/blocks/quote/__init__.py +0 -14
- notionary/blocks/quote/quote_element.py +0 -75
- notionary/blocks/quote/quote_markdown_node.py +0 -16
- notionary/blocks/quote/quote_models.py +0 -18
- notionary/blocks/registry/__init__.py +0 -3
- notionary/blocks/registry/block_registry.py +0 -150
- notionary/blocks/rich_text/__init__.py +0 -33
- notionary/blocks/rich_text/rich_text_models.py +0 -221
- notionary/blocks/rich_text/text_inline_formatter.py +0 -456
- notionary/blocks/syntax_prompt_builder.py +0 -137
- notionary/blocks/table/__init__.py +0 -19
- notionary/blocks/table/table_element.py +0 -225
- notionary/blocks/table/table_markdown_node.py +0 -42
- notionary/blocks/table/table_models.py +0 -28
- notionary/blocks/table_of_contents/__init__.py +0 -17
- notionary/blocks/table_of_contents/table_of_contents_element.py +0 -80
- notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +0 -21
- notionary/blocks/table_of_contents/table_of_contents_models.py +0 -18
- notionary/blocks/todo/__init__.py +0 -12
- notionary/blocks/todo/todo_element.py +0 -81
- notionary/blocks/todo/todo_markdown_node.py +0 -21
- notionary/blocks/todo/todo_models.py +0 -18
- notionary/blocks/toggle/__init__.py +0 -12
- notionary/blocks/toggle/toggle_element.py +0 -112
- notionary/blocks/toggle/toggle_markdown_node.py +0 -31
- notionary/blocks/toggle/toggle_models.py +0 -17
- notionary/blocks/toggleable_heading/__init__.py +0 -11
- notionary/blocks/toggleable_heading/toggleable_heading_element.py +0 -115
- notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +0 -34
- notionary/blocks/types.py +0 -130
- notionary/blocks/video/__init__.py +0 -11
- notionary/blocks/video/video_element.py +0 -187
- notionary/blocks/video/video_element_models.py +0 -10
- notionary/blocks/video/video_markdown_node.py +0 -26
- notionary/database/__init__.py +0 -4
- notionary/database/database.py +0 -480
- notionary/database/database_filter_builder.py +0 -173
- notionary/database/database_provider.py +0 -227
- notionary/database/exceptions.py +0 -13
- notionary/database/models.py +0 -337
- notionary/database/notion_database.py +0 -487
- notionary/file_upload/__init__.py +0 -7
- notionary/page/client.py +0 -124
- notionary/page/markdown_whitespace_processor.py +0 -129
- notionary/page/models.py +0 -322
- notionary/page/notion_page.py +0 -712
- notionary/page/page_content_deleting_service.py +0 -117
- notionary/page/page_content_writer.py +0 -80
- notionary/page/property_formatter.py +0 -99
- notionary/page/reader/handler/__init__.py +0 -19
- notionary/page/reader/handler/base_block_renderer.py +0 -44
- notionary/page/reader/handler/block_processing_context.py +0 -35
- notionary/page/reader/handler/block_rendering_context.py +0 -48
- notionary/page/reader/handler/column_list_renderer.py +0 -51
- notionary/page/reader/handler/column_renderer.py +0 -60
- notionary/page/reader/handler/equation_renderer.py +0 -0
- notionary/page/reader/handler/line_renderer.py +0 -73
- notionary/page/reader/handler/numbered_list_renderer.py +0 -85
- notionary/page/reader/handler/toggle_renderer.py +0 -69
- notionary/page/reader/handler/toggleable_heading_renderer.py +0 -89
- notionary/page/reader/page_content_retriever.py +0 -81
- notionary/page/search_filter_builder.py +0 -132
- notionary/page/utils.py +0 -60
- notionary/page/writer/handler/__init__.py +0 -24
- notionary/page/writer/handler/code_handler.py +0 -72
- notionary/page/writer/handler/column_handler.py +0 -141
- notionary/page/writer/handler/column_list_handler.py +0 -139
- notionary/page/writer/handler/equation_handler.py +0 -74
- notionary/page/writer/handler/line_handler.py +0 -35
- notionary/page/writer/handler/line_processing_context.py +0 -54
- notionary/page/writer/handler/regular_line_handler.py +0 -86
- notionary/page/writer/handler/table_handler.py +0 -66
- notionary/page/writer/handler/toggle_handler.py +0 -159
- notionary/page/writer/handler/toggleable_heading_handler.py +0 -174
- notionary/page/writer/markdown_to_notion_converter.py +0 -139
- notionary/page/writer/markdown_to_notion_converter_context.py +0 -30
- notionary/page/writer/markdown_to_notion_text_length_post_processor.py +0 -0
- notionary/page/writer/notion_text_length_processor.py +0 -150
- notionary/schemas/__init__.py +0 -3
- notionary/schemas/base.py +0 -73
- notionary/shared/__init__.py +0 -3
- notionary/shared/name_to_id_resolver.py +0 -203
- notionary/telemetry/__init__.py +0 -19
- notionary/telemetry/service.py +0 -136
- notionary/telemetry/views.py +0 -73
- notionary/user/base_notion_user.py +0 -53
- notionary/user/models.py +0 -84
- notionary/user/notion_bot_user.py +0 -226
- notionary/user/notion_user.py +0 -255
- notionary/user/notion_user_manager.py +0 -101
- notionary/util/__init__.py +0 -15
- notionary/util/concurrency_limiter.py +0 -0
- notionary/util/factory_decorator.py +0 -0
- notionary/util/factory_only.py +0 -37
- notionary/util/fuzzy.py +0 -75
- notionary/util/logging_mixin.py +0 -59
- notionary/util/page_id_utils.py +0 -27
- notionary/util/singleton.py +0 -18
- notionary/util/singleton_metaclass.py +0 -22
- notionary/workspace.py +0 -105
- notionary-0.2.27.dist-info/METADATA +0 -270
- notionary-0.2.27.dist-info/RECORD +0 -202
- /notionary/{database → user}/factory.py +0 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from typing import Self
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ValidationInfo, field_validator, model_serializer, model_validator
|
|
7
|
+
|
|
8
|
+
from notionary.shared.properties.type import PropertyType
|
|
9
|
+
from notionary.shared.typings import JsonDict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FieldType(StrEnum):
|
|
13
|
+
STRING = "string"
|
|
14
|
+
NUMBER = "number"
|
|
15
|
+
BOOLEAN = "boolean"
|
|
16
|
+
DATE = "date"
|
|
17
|
+
DATETIME = "datetime"
|
|
18
|
+
ARRAY = "array"
|
|
19
|
+
RELATION = "relation"
|
|
20
|
+
PEOPLE = "people"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class StringOperator(StrEnum):
|
|
24
|
+
EQUALS = "equals"
|
|
25
|
+
DOES_NOT_EQUAL = "does_not_equal"
|
|
26
|
+
CONTAINS = "contains"
|
|
27
|
+
DOES_NOT_CONTAIN = "does_not_contain"
|
|
28
|
+
STARTS_WITH = "starts_with"
|
|
29
|
+
ENDS_WITH = "ends_with"
|
|
30
|
+
IS_EMPTY = "is_empty"
|
|
31
|
+
IS_NOT_EMPTY = "is_not_empty"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class NumberOperator(StrEnum):
|
|
35
|
+
EQUALS = "equals"
|
|
36
|
+
DOES_NOT_EQUAL = "does_not_equal"
|
|
37
|
+
GREATER_THAN = "greater_than"
|
|
38
|
+
GREATER_THAN_OR_EQUAL_TO = "greater_than_or_equal_to"
|
|
39
|
+
LESS_THAN = "less_than"
|
|
40
|
+
LESS_THAN_OR_EQUAL_TO = "less_than_or_equal_to"
|
|
41
|
+
IS_EMPTY = "is_empty"
|
|
42
|
+
IS_NOT_EMPTY = "is_not_empty"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BooleanOperator(StrEnum):
|
|
46
|
+
IS_TRUE = "is_true"
|
|
47
|
+
IS_FALSE = "is_false"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SelectOperator(StrEnum):
|
|
51
|
+
EQUALS = "equals"
|
|
52
|
+
DOES_NOT_EQUAL = "does_not_equal"
|
|
53
|
+
IS_EMPTY = "is_empty"
|
|
54
|
+
IS_NOT_EMPTY = "is_not_empty"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class DateOperator(StrEnum):
|
|
58
|
+
EQUALS = "equals"
|
|
59
|
+
BEFORE = "before"
|
|
60
|
+
AFTER = "after"
|
|
61
|
+
ON_OR_BEFORE = "on_or_before"
|
|
62
|
+
ON_OR_AFTER = "on_or_after"
|
|
63
|
+
IS_EMPTY = "is_empty"
|
|
64
|
+
IS_NOT_EMPTY = "is_not_empty"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ArrayOperator(StrEnum):
|
|
68
|
+
CONTAINS = "contains"
|
|
69
|
+
DOES_NOT_CONTAIN = "does_not_contain"
|
|
70
|
+
IS_EMPTY = "is_empty"
|
|
71
|
+
IS_NOT_EMPTY = "is_not_empty"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class LogicalOperator(StrEnum):
|
|
75
|
+
AND = "and"
|
|
76
|
+
OR = "or"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class SortDirection(StrEnum):
|
|
80
|
+
ASCENDING = "ascending"
|
|
81
|
+
DESCENDING = "descending"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TimestampType(StrEnum):
|
|
85
|
+
CREATED_TIME = "created_time"
|
|
86
|
+
LAST_EDITED_TIME = "last_edited_time"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TimeUnit(StrEnum):
|
|
90
|
+
DAYS = "days"
|
|
91
|
+
WEEKS = "weeks"
|
|
92
|
+
MONTHS = "months"
|
|
93
|
+
YEARS = "years"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
type Operator = StringOperator | NumberOperator | BooleanOperator | DateOperator | ArrayOperator
|
|
97
|
+
type FilterValue = str | int | float | bool | list[str | int | float]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class FilterCondition(BaseModel):
|
|
101
|
+
field: str
|
|
102
|
+
field_type: FieldType
|
|
103
|
+
operator: Operator
|
|
104
|
+
value: FilterValue | None = None
|
|
105
|
+
time_value: int | None = None
|
|
106
|
+
time_unit: TimeUnit | None = None
|
|
107
|
+
|
|
108
|
+
@model_validator(mode="after")
|
|
109
|
+
def validate_operator_and_value(self) -> Self:
|
|
110
|
+
self._validate_no_value_operators()
|
|
111
|
+
self._validate_value_required_operators()
|
|
112
|
+
self._validate_value_type_matches_field_type()
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
def _validate_no_value_operators(self) -> None:
|
|
116
|
+
no_value_ops = {
|
|
117
|
+
StringOperator.IS_EMPTY,
|
|
118
|
+
StringOperator.IS_NOT_EMPTY,
|
|
119
|
+
NumberOperator.IS_EMPTY,
|
|
120
|
+
NumberOperator.IS_NOT_EMPTY,
|
|
121
|
+
BooleanOperator.IS_TRUE,
|
|
122
|
+
BooleanOperator.IS_FALSE,
|
|
123
|
+
DateOperator.IS_EMPTY,
|
|
124
|
+
DateOperator.IS_NOT_EMPTY,
|
|
125
|
+
ArrayOperator.IS_EMPTY,
|
|
126
|
+
ArrayOperator.IS_NOT_EMPTY,
|
|
127
|
+
}
|
|
128
|
+
if self.operator in no_value_ops and self.value is not None:
|
|
129
|
+
raise ValueError(f"Operator '{self.operator}' does not expect a value")
|
|
130
|
+
|
|
131
|
+
def _validate_value_required_operators(self) -> None:
|
|
132
|
+
operators_to_skip = {
|
|
133
|
+
StringOperator.IS_EMPTY,
|
|
134
|
+
StringOperator.IS_NOT_EMPTY,
|
|
135
|
+
NumberOperator.IS_EMPTY,
|
|
136
|
+
NumberOperator.IS_NOT_EMPTY,
|
|
137
|
+
BooleanOperator.IS_TRUE,
|
|
138
|
+
BooleanOperator.IS_FALSE,
|
|
139
|
+
DateOperator.IS_EMPTY,
|
|
140
|
+
DateOperator.IS_NOT_EMPTY,
|
|
141
|
+
ArrayOperator.IS_EMPTY,
|
|
142
|
+
ArrayOperator.IS_NOT_EMPTY,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
is_skipped_operator = self.operator in operators_to_skip
|
|
146
|
+
if not is_skipped_operator and self.value is None:
|
|
147
|
+
raise ValueError(f"Operator '{self.operator}' requires a value")
|
|
148
|
+
|
|
149
|
+
def _validate_value_type_matches_field_type(self) -> None:
|
|
150
|
+
if self.value is None:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
if self.field_type == FieldType.STRING:
|
|
154
|
+
self._ensure_value_is_string()
|
|
155
|
+
elif self.field_type == FieldType.NUMBER:
|
|
156
|
+
self._ensure_value_is_number()
|
|
157
|
+
elif self.field_type == FieldType.BOOLEAN:
|
|
158
|
+
self._ensure_value_is_boolean()
|
|
159
|
+
elif self.field_type in (FieldType.DATE, FieldType.DATETIME) or self.field_type in (
|
|
160
|
+
FieldType.ARRAY,
|
|
161
|
+
FieldType.RELATION,
|
|
162
|
+
FieldType.PEOPLE,
|
|
163
|
+
):
|
|
164
|
+
self._ensure_value_is_string()
|
|
165
|
+
|
|
166
|
+
def _ensure_value_is_string(self) -> None:
|
|
167
|
+
if not isinstance(self.value, str):
|
|
168
|
+
raise ValueError(
|
|
169
|
+
f"Value for field type '{self.field_type}' must be a string, got {type(self.value).__name__}"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _ensure_value_is_number(self) -> None:
|
|
173
|
+
if not isinstance(self.value, (int, float)):
|
|
174
|
+
raise ValueError(
|
|
175
|
+
f"Value for field type '{self.field_type}' must be a number (int or float), "
|
|
176
|
+
f"got {type(self.value).__name__}"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def _ensure_value_is_boolean(self) -> None:
|
|
180
|
+
if not isinstance(self.value, bool):
|
|
181
|
+
raise ValueError(
|
|
182
|
+
f"Value for field type '{self.field_type}' must be a boolean, got {type(self.value).__name__}"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@field_validator("operator")
|
|
186
|
+
@classmethod
|
|
187
|
+
def validate_operator_for_field_type(
|
|
188
|
+
cls,
|
|
189
|
+
value: Operator,
|
|
190
|
+
info: ValidationInfo,
|
|
191
|
+
) -> Operator:
|
|
192
|
+
if "field_type" not in info.data:
|
|
193
|
+
return value
|
|
194
|
+
|
|
195
|
+
field_type: FieldType = info.data["field_type"]
|
|
196
|
+
operator_value = value if isinstance(value, str) else value.value
|
|
197
|
+
|
|
198
|
+
if not cls._is_operator_valid_for_field_type(operator_value, field_type):
|
|
199
|
+
raise ValueError(f"Operator '{operator_value}' is not valid for field type '{field_type}'")
|
|
200
|
+
|
|
201
|
+
return value
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def _is_operator_valid_for_field_type(operator: str, field_type: FieldType) -> bool:
|
|
205
|
+
valid_operators: dict[FieldType, list[str]] = {
|
|
206
|
+
FieldType.STRING: [op.value for op in StringOperator],
|
|
207
|
+
FieldType.NUMBER: [op.value for op in NumberOperator],
|
|
208
|
+
FieldType.BOOLEAN: [op.value for op in BooleanOperator],
|
|
209
|
+
FieldType.DATE: [op.value for op in DateOperator],
|
|
210
|
+
FieldType.DATETIME: [op.value for op in DateOperator],
|
|
211
|
+
FieldType.ARRAY: [op.value for op in ArrayOperator],
|
|
212
|
+
FieldType.RELATION: [op.value for op in ArrayOperator],
|
|
213
|
+
FieldType.PEOPLE: [op.value for op in ArrayOperator],
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return operator in valid_operators.get(field_type, [])
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class OrGroupMarker(BaseModel):
|
|
220
|
+
conditions: list[FilterCondition]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
type InternalFilterCondition = FilterCondition | OrGroupMarker
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class PropertyFilter(BaseModel):
|
|
227
|
+
property: str
|
|
228
|
+
property_type: PropertyType
|
|
229
|
+
operator: Operator
|
|
230
|
+
value: FilterValue | None = None
|
|
231
|
+
|
|
232
|
+
@model_validator(mode="after")
|
|
233
|
+
def validate_value_type(self) -> Self:
|
|
234
|
+
if self.value is None:
|
|
235
|
+
return self
|
|
236
|
+
|
|
237
|
+
if self.property_type in (PropertyType.PEOPLE, PropertyType.RELATION) and not isinstance(self.value, str):
|
|
238
|
+
raise ValueError(
|
|
239
|
+
f"Value for property type '{self.property_type.value}' must be a string, "
|
|
240
|
+
f"got {type(self.value).__name__}"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return self
|
|
244
|
+
|
|
245
|
+
@model_serializer
|
|
246
|
+
def serialize_model(self) -> JsonDict:
|
|
247
|
+
property_type_str = self.property_type.value
|
|
248
|
+
operator_str = self.operator.value
|
|
249
|
+
filter_value = self.value
|
|
250
|
+
|
|
251
|
+
if isinstance(self.operator, BooleanOperator):
|
|
252
|
+
operator_str = "equals"
|
|
253
|
+
filter_value = self.operator == BooleanOperator.IS_TRUE
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
"property": self.property,
|
|
257
|
+
property_type_str: {operator_str: filter_value if filter_value is not None else True},
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class CompoundFilter(BaseModel):
|
|
262
|
+
operator: LogicalOperator
|
|
263
|
+
filters: list[PropertyFilter | CompoundFilter]
|
|
264
|
+
|
|
265
|
+
@model_serializer
|
|
266
|
+
def serialize_model(self) -> JsonDict:
|
|
267
|
+
operator_str = self.operator.value
|
|
268
|
+
return {operator_str: [f.model_dump() for f in self.filters]}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
type NotionFilter = PropertyFilter | CompoundFilter
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class PropertySort(BaseModel):
|
|
275
|
+
property: str
|
|
276
|
+
direction: SortDirection
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class TimestampSort(BaseModel):
|
|
280
|
+
timestamp: TimestampType
|
|
281
|
+
direction: SortDirection
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
type NotionSort = PropertySort | TimestampSort
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class DataSourceQueryParams(BaseModel):
|
|
288
|
+
filter: NotionFilter | None = None
|
|
289
|
+
sorts: list[NotionSort] | None = None
|
|
290
|
+
page_size: int | None = None
|
|
291
|
+
|
|
292
|
+
@model_serializer
|
|
293
|
+
def to_api_params(self) -> JsonDict:
|
|
294
|
+
result: JsonDict = {}
|
|
295
|
+
|
|
296
|
+
if self.filter is not None:
|
|
297
|
+
result["filter"] = self.filter.model_dump()
|
|
298
|
+
|
|
299
|
+
if self.sorts is not None and len(self.sorts) > 0:
|
|
300
|
+
result["sorts"] = [sort.model_dump() for sort in self.sorts]
|
|
301
|
+
|
|
302
|
+
return result
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import ClassVar
|
|
2
|
+
|
|
3
|
+
from notionary.data_source.properties.schemas import DataSourceProperty
|
|
4
|
+
from notionary.data_source.query.schema import (
|
|
5
|
+
ArrayOperator,
|
|
6
|
+
BooleanOperator,
|
|
7
|
+
DateOperator,
|
|
8
|
+
NumberOperator,
|
|
9
|
+
Operator,
|
|
10
|
+
SelectOperator,
|
|
11
|
+
StringOperator,
|
|
12
|
+
)
|
|
13
|
+
from notionary.exceptions.data_source.builder import InvalidOperatorForPropertyType
|
|
14
|
+
from notionary.shared.properties.type import PropertyType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class QueryValidator:
|
|
18
|
+
_PROPERTY_TYPE_OPERATORS: ClassVar[dict[PropertyType, list[type[Operator]]]] = {
|
|
19
|
+
PropertyType.TITLE: [StringOperator],
|
|
20
|
+
PropertyType.RICH_TEXT: [StringOperator],
|
|
21
|
+
PropertyType.URL: [StringOperator],
|
|
22
|
+
PropertyType.EMAIL: [StringOperator],
|
|
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],
|
|
29
|
+
PropertyType.CREATED_TIME: [DateOperator],
|
|
30
|
+
PropertyType.LAST_EDITED_TIME: [DateOperator],
|
|
31
|
+
PropertyType.PEOPLE: [ArrayOperator],
|
|
32
|
+
PropertyType.CREATED_BY: [ArrayOperator],
|
|
33
|
+
PropertyType.LAST_EDITED_BY: [ArrayOperator],
|
|
34
|
+
PropertyType.RELATION: [ArrayOperator],
|
|
35
|
+
PropertyType.CHECKBOX: [BooleanOperator],
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def validate_operator_for_property(
|
|
39
|
+
self, property_name: str, property_obj: DataSourceProperty, operator: Operator
|
|
40
|
+
) -> None:
|
|
41
|
+
if not self._is_operator_valid_for_property_type(property_obj.type, operator):
|
|
42
|
+
valid_operators = self._get_valid_operators_for_property_type(property_obj.type)
|
|
43
|
+
raise InvalidOperatorForPropertyType(
|
|
44
|
+
property_name=property_name,
|
|
45
|
+
property_type=property_obj.type,
|
|
46
|
+
operator=operator,
|
|
47
|
+
valid_operators=valid_operators,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def _is_operator_valid_for_property_type(self, property_type: PropertyType, operator: Operator) -> bool:
|
|
51
|
+
allowed_operator_types = self._PROPERTY_TYPE_OPERATORS.get(property_type, [])
|
|
52
|
+
valid_operator_values = self._get_operator_values_from_types(allowed_operator_types)
|
|
53
|
+
return operator.value in valid_operator_values
|
|
54
|
+
|
|
55
|
+
def _get_operator_values_from_types(self, operator_types: list[type[Operator]]) -> set[str]:
|
|
56
|
+
values: set[str] = set()
|
|
57
|
+
for operator_type in operator_types:
|
|
58
|
+
for operator in operator_type:
|
|
59
|
+
values.add(operator.value)
|
|
60
|
+
return values
|
|
61
|
+
|
|
62
|
+
def _get_valid_operators_for_property_type(self, property_type: PropertyType) -> list[Operator]:
|
|
63
|
+
allowed_operator_types = self._PROPERTY_TYPE_OPERATORS.get(property_type, [])
|
|
64
|
+
return self._collect_all_operators_from_types(allowed_operator_types)
|
|
65
|
+
|
|
66
|
+
def _collect_all_operators_from_types(self, operator_types: list[type[Operator]]) -> list[Operator]:
|
|
67
|
+
operators: list[Operator] = []
|
|
68
|
+
for operator_type in operator_types:
|
|
69
|
+
operators.extend(self._get_all_enum_values(operator_type))
|
|
70
|
+
return operators
|
|
71
|
+
|
|
72
|
+
def _get_all_enum_values(self, operator_type: type[Operator]) -> list[Operator]:
|
|
73
|
+
return list(operator_type)
|
|
@@ -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
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
from notionary.blocks.rich_text.models import RichText
|
|
4
|
+
from notionary.data_source.properties.schemas import DiscriminatedDataSourceProperty
|
|
5
|
+
from notionary.page.schemas import NotionPageDto
|
|
6
|
+
from notionary.shared.entity.schemas import EntityResponseDto, NotionEntityUpdateDto
|
|
7
|
+
from notionary.shared.models.parent import Parent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UpdateDataSourceDto(NotionEntityUpdateDto):
|
|
11
|
+
title: list[RichText]
|
|
12
|
+
description: list[RichText]
|
|
13
|
+
archived: bool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class QueryDataSourceResponse(BaseModel):
|
|
17
|
+
results: list[NotionPageDto]
|
|
18
|
+
next_cursor: str | None = None
|
|
19
|
+
has_more: bool
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DataSourceDto(EntityResponseDto):
|
|
23
|
+
database_parent: Parent
|
|
24
|
+
title: list[RichText]
|
|
25
|
+
description: list[RichText]
|
|
26
|
+
archived: bool
|
|
27
|
+
properties: dict[str, DiscriminatedDataSourceProperty]
|