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,122 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Callable, Coroutine
|
|
6
|
+
from typing import Any, ParamSpec, TypeVar
|
|
7
|
+
|
|
8
|
+
P = ParamSpec("P")
|
|
9
|
+
R = TypeVar("R")
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
type SyncFunc = Callable[P, R]
|
|
13
|
+
type AsyncFunc = Callable[P, Coroutine[Any, Any, R]]
|
|
14
|
+
type SyncDecorator = Callable[[SyncFunc], SyncFunc]
|
|
15
|
+
type AsyncDecorator = Callable[[AsyncFunc], AsyncFunc]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def singleton(cls):
|
|
19
|
+
instance = [None]
|
|
20
|
+
|
|
21
|
+
def wrapper(*args, **kwargs):
|
|
22
|
+
if instance[0] is None:
|
|
23
|
+
instance[0] = cls(*args, **kwargs)
|
|
24
|
+
return instance[0]
|
|
25
|
+
|
|
26
|
+
return wrapper
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def time_execution_sync(additional_text: str = "", min_duration_to_log: float = 0.25) -> SyncDecorator:
|
|
30
|
+
def decorator(func: SyncFunc) -> SyncFunc:
|
|
31
|
+
@functools.wraps(func)
|
|
32
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
33
|
+
start_time = time.perf_counter()
|
|
34
|
+
result = func(*args, **kwargs)
|
|
35
|
+
execution_time = time.perf_counter() - start_time
|
|
36
|
+
|
|
37
|
+
if execution_time > min_duration_to_log:
|
|
38
|
+
logger = _get_logger_from_context(args, func)
|
|
39
|
+
function_name = additional_text.strip("-") or func.__name__
|
|
40
|
+
logger.debug(f"⏳ {function_name}() took {execution_time:.2f}s")
|
|
41
|
+
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
return wrapper
|
|
45
|
+
|
|
46
|
+
return decorator
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def time_execution_async(
|
|
50
|
+
additional_text: str = "",
|
|
51
|
+
min_duration_to_log: float = 0.25,
|
|
52
|
+
) -> AsyncDecorator:
|
|
53
|
+
def decorator(func: AsyncFunc) -> AsyncFunc:
|
|
54
|
+
@functools.wraps(func)
|
|
55
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
56
|
+
start_time = time.perf_counter()
|
|
57
|
+
result = await func(*args, **kwargs)
|
|
58
|
+
execution_time = time.perf_counter() - start_time
|
|
59
|
+
|
|
60
|
+
if execution_time > min_duration_to_log:
|
|
61
|
+
logger = _get_logger_from_context(args, func)
|
|
62
|
+
function_name = additional_text.strip("-") or func.__name__
|
|
63
|
+
logger.debug(f"⏳ {function_name}() took {execution_time:.2f}s")
|
|
64
|
+
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
return wrapper
|
|
68
|
+
|
|
69
|
+
return decorator
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _get_logger_from_context(args: tuple, func: Callable) -> logging.Logger:
|
|
73
|
+
if _has_instance_logger(args):
|
|
74
|
+
return _extract_instance_logger(args)
|
|
75
|
+
|
|
76
|
+
return _get_module_logger(func)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _has_instance_logger(args: tuple) -> bool:
|
|
80
|
+
return bool(args) and hasattr(args[0], "logger")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _extract_instance_logger(args: tuple) -> logging.Logger:
|
|
84
|
+
return args[0].logger
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _get_module_logger(func: Callable) -> logging.Logger:
|
|
88
|
+
return logging.getLogger(func.__module__)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def async_retry(
|
|
92
|
+
max_retries: int = 3,
|
|
93
|
+
initial_delay: float = 1.0,
|
|
94
|
+
backoff_factor: float = 2.0,
|
|
95
|
+
retry_on_exceptions: tuple[type[Exception], ...] | None = None,
|
|
96
|
+
):
|
|
97
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
98
|
+
@functools.wraps(func)
|
|
99
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
100
|
+
delay = initial_delay
|
|
101
|
+
last_exception = None
|
|
102
|
+
|
|
103
|
+
for attempt in range(max_retries + 1):
|
|
104
|
+
try:
|
|
105
|
+
return await func(*args, **kwargs)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
last_exception = e
|
|
108
|
+
|
|
109
|
+
if retry_on_exceptions is not None and not isinstance(e, retry_on_exceptions):
|
|
110
|
+
raise
|
|
111
|
+
|
|
112
|
+
if attempt == max_retries:
|
|
113
|
+
raise
|
|
114
|
+
|
|
115
|
+
await asyncio.sleep(delay)
|
|
116
|
+
delay *= backoff_factor
|
|
117
|
+
|
|
118
|
+
raise last_exception
|
|
119
|
+
|
|
120
|
+
return wrapper
|
|
121
|
+
|
|
122
|
+
return decorator
|
notionary/utils/fuzzy.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import difflib
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class _MatchResult(Generic[T]):
|
|
11
|
+
item: T
|
|
12
|
+
similarity: float
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def find_best_match(
|
|
16
|
+
query: str,
|
|
17
|
+
items: list[T],
|
|
18
|
+
text_extractor: Callable[[T], str],
|
|
19
|
+
min_similarity: float,
|
|
20
|
+
) -> T | None:
|
|
21
|
+
matches = _find_best_matches(query, items, text_extractor, min_similarity, limit=1)
|
|
22
|
+
return matches[0].item if matches else None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def find_all_matches(
|
|
26
|
+
query: str,
|
|
27
|
+
items: list[T],
|
|
28
|
+
text_extractor: Callable[[T], str],
|
|
29
|
+
min_similarity: float,
|
|
30
|
+
) -> list[T]:
|
|
31
|
+
matches = _find_best_matches(query, items, text_extractor, min_similarity, limit=None)
|
|
32
|
+
return [match.item for match in matches]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _find_best_matches(
|
|
36
|
+
query: str,
|
|
37
|
+
items: list[T],
|
|
38
|
+
text_extractor: Callable[[T], str],
|
|
39
|
+
min_similarity: float = 0.0,
|
|
40
|
+
limit: int | None = None,
|
|
41
|
+
) -> list[_MatchResult[T]]:
|
|
42
|
+
results = []
|
|
43
|
+
|
|
44
|
+
for item in items:
|
|
45
|
+
text = text_extractor(item)
|
|
46
|
+
similarity = _calculate_similarity(query, text)
|
|
47
|
+
|
|
48
|
+
if similarity >= min_similarity:
|
|
49
|
+
results.append(_MatchResult(item=item, similarity=similarity))
|
|
50
|
+
|
|
51
|
+
results = _sort_by_highest_similarity_first(results)
|
|
52
|
+
|
|
53
|
+
if limit:
|
|
54
|
+
return results[:limit]
|
|
55
|
+
|
|
56
|
+
return results
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _sort_by_highest_similarity_first(results: list[_MatchResult]) -> list[_MatchResult]:
|
|
60
|
+
return sorted(results, key=lambda x: x.similarity, reverse=True)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _calculate_similarity(query: str, target: str) -> float:
|
|
64
|
+
return difflib.SequenceMatcher(
|
|
65
|
+
isjunk=None,
|
|
66
|
+
a=query.lower().strip(),
|
|
67
|
+
b=target.lower().strip(),
|
|
68
|
+
).ratio()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
|
|
7
|
+
load_dotenv(override=True)
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("notionary")
|
|
10
|
+
logger.addHandler(logging.NullHandler())
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def configure_library_logging(level: str = "WARNING") -> None:
|
|
14
|
+
log_level = getattr(logging, level.upper(), logging.WARNING)
|
|
15
|
+
|
|
16
|
+
library_logger = logging.getLogger("notionary")
|
|
17
|
+
|
|
18
|
+
if library_logger.handlers:
|
|
19
|
+
library_logger.handlers.clear()
|
|
20
|
+
|
|
21
|
+
handler = logging.StreamHandler()
|
|
22
|
+
handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
|
|
23
|
+
|
|
24
|
+
library_logger.setLevel(log_level)
|
|
25
|
+
library_logger.addHandler(handler)
|
|
26
|
+
|
|
27
|
+
_suppress_noisy_third_party_loggers()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _suppress_noisy_third_party_loggers() -> None:
|
|
31
|
+
noisy_loggers = ["httpx", "httpcore", "httpcore.connection", "httpcore.http11"]
|
|
32
|
+
|
|
33
|
+
for logger_name in noisy_loggers:
|
|
34
|
+
logging.getLogger(logger_name).setLevel(logging.WARNING)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _auto_configure_from_environment() -> None:
|
|
38
|
+
env_log_level = os.getenv("NOTIONARY_LOG_LEVEL")
|
|
39
|
+
|
|
40
|
+
if env_log_level:
|
|
41
|
+
configure_library_logging(env_log_level)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_auto_configure_from_environment()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class LoggingMixin:
|
|
48
|
+
logger: ClassVar[logging.Logger] = None
|
|
49
|
+
|
|
50
|
+
def __init_subclass__(cls, **kwargs):
|
|
51
|
+
super().__init_subclass__(**kwargs)
|
|
52
|
+
cls.logger = logging.getLogger(f"notionary.{cls.__name__}")
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def instance_logger(self) -> logging.Logger:
|
|
56
|
+
if not hasattr(self, "_logger"):
|
|
57
|
+
self._logger = logging.getLogger(f"notionary.{self.__class__.__name__}")
|
|
58
|
+
return self._logger
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator, Callable, Coroutine
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PaginatedResponse(BaseModel):
|
|
8
|
+
results: list[Any]
|
|
9
|
+
has_more: bool
|
|
10
|
+
next_cursor: str | None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def _fetch_data(
|
|
14
|
+
api_call: Callable[..., Coroutine[Any, Any, PaginatedResponse]],
|
|
15
|
+
page_size: int | None = None,
|
|
16
|
+
**kwargs,
|
|
17
|
+
) -> AsyncGenerator[PaginatedResponse]:
|
|
18
|
+
next_cursor = None
|
|
19
|
+
has_more = True
|
|
20
|
+
total_fetched = 0
|
|
21
|
+
|
|
22
|
+
while has_more and _should_continue_fetching(page_size, total_fetched):
|
|
23
|
+
request_params = _build_request_params(kwargs, next_cursor, page_size)
|
|
24
|
+
response = await api_call(**request_params)
|
|
25
|
+
|
|
26
|
+
limited_results = _apply_result_limit(response.results, page_size, total_fetched)
|
|
27
|
+
total_fetched += len(limited_results)
|
|
28
|
+
|
|
29
|
+
yield _create_limited_response(response, limited_results)
|
|
30
|
+
|
|
31
|
+
if _has_reached_limit(page_size, total_fetched):
|
|
32
|
+
break
|
|
33
|
+
|
|
34
|
+
has_more = response.has_more
|
|
35
|
+
next_cursor = response.next_cursor
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _should_continue_fetching(page_size: int | None, total_fetched: int) -> bool:
|
|
39
|
+
if page_size is None:
|
|
40
|
+
return True
|
|
41
|
+
return total_fetched < page_size
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _build_request_params(
|
|
45
|
+
base_kwargs: dict[str, Any],
|
|
46
|
+
cursor: str | None,
|
|
47
|
+
page_size: int | None,
|
|
48
|
+
) -> dict[str, Any]:
|
|
49
|
+
params = base_kwargs.copy()
|
|
50
|
+
|
|
51
|
+
if cursor:
|
|
52
|
+
params["start_cursor"] = cursor
|
|
53
|
+
|
|
54
|
+
if page_size:
|
|
55
|
+
params["page_size"] = page_size
|
|
56
|
+
|
|
57
|
+
return params
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _apply_result_limit(results: list[Any], page_size: int | None, total_fetched: int) -> list[Any]:
|
|
61
|
+
if page_size is None:
|
|
62
|
+
return results
|
|
63
|
+
|
|
64
|
+
remaining = page_size - total_fetched
|
|
65
|
+
return results[:remaining]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _has_reached_limit(page_size: int | None, total_fetched: int) -> bool:
|
|
69
|
+
if page_size is None:
|
|
70
|
+
return False
|
|
71
|
+
return total_fetched >= page_size
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _create_limited_response(original: PaginatedResponse, limited_results: list[Any]) -> PaginatedResponse:
|
|
75
|
+
return PaginatedResponse(
|
|
76
|
+
results=limited_results,
|
|
77
|
+
has_more=original.has_more and len(limited_results) == len(original.results),
|
|
78
|
+
next_cursor=original.next_cursor,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def paginate_notion_api(
|
|
83
|
+
api_call: Callable[..., Coroutine[Any, Any, PaginatedResponse]],
|
|
84
|
+
page_size: int | None = None,
|
|
85
|
+
**kwargs,
|
|
86
|
+
) -> list[Any]:
|
|
87
|
+
all_results = []
|
|
88
|
+
async for page in _fetch_data(api_call, page_size=page_size, **kwargs):
|
|
89
|
+
all_results.extend(page.results)
|
|
90
|
+
return all_results
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def paginate_notion_api_generator(
|
|
94
|
+
api_call: Callable[..., Coroutine[Any, Any, PaginatedResponse]],
|
|
95
|
+
page_size: int | None = None,
|
|
96
|
+
**kwargs,
|
|
97
|
+
) -> AsyncGenerator[Any]:
|
|
98
|
+
async for page in _fetch_data(api_call, page_size=page_size, **kwargs):
|
|
99
|
+
for item in page.results:
|
|
100
|
+
yield item
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def extract_uuid(source: str) -> str | None:
|
|
5
|
+
UUID_RAW_PATTERN = r"([a-f0-9]{32})"
|
|
6
|
+
|
|
7
|
+
if _is_valid_uuid(source):
|
|
8
|
+
return source
|
|
9
|
+
|
|
10
|
+
match = re.search(UUID_RAW_PATTERN, source.lower())
|
|
11
|
+
if not match:
|
|
12
|
+
return None
|
|
13
|
+
|
|
14
|
+
uuid_raw = match.group(1)
|
|
15
|
+
return f"{uuid_raw[0:8]}-{uuid_raw[8:12]}-{uuid_raw[12:16]}-{uuid_raw[16:20]}-{uuid_raw[20:32]}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _is_valid_uuid(uuid: str) -> bool:
|
|
19
|
+
UUID_PATTERN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$"
|
|
20
|
+
return bool(re.match(UUID_PATTERN, uuid.lower()))
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator
|
|
2
|
+
|
|
3
|
+
from notionary.data_source.schemas import DataSourceDto
|
|
4
|
+
from notionary.http.client import NotionHttpClient
|
|
5
|
+
from notionary.page.schemas import NotionPageDto
|
|
6
|
+
from notionary.shared.typings import JsonDict
|
|
7
|
+
from notionary.utils.pagination import paginate_notion_api_generator
|
|
8
|
+
from notionary.workspace.query.models import WorkspaceQueryConfig
|
|
9
|
+
from notionary.workspace.schemas import DataSourceSearchResponse, PageSearchResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WorkspaceClient:
|
|
13
|
+
DEFAULT_PAGE_SIZE = 100
|
|
14
|
+
|
|
15
|
+
def __init__(self, http_client: NotionHttpClient | None = None) -> None:
|
|
16
|
+
self._http_client = http_client or NotionHttpClient()
|
|
17
|
+
|
|
18
|
+
async def query_pages_stream(
|
|
19
|
+
self,
|
|
20
|
+
search_config: WorkspaceQueryConfig,
|
|
21
|
+
) -> AsyncGenerator[NotionPageDto]:
|
|
22
|
+
async for page in paginate_notion_api_generator(
|
|
23
|
+
self._query_pages,
|
|
24
|
+
search_config=search_config,
|
|
25
|
+
):
|
|
26
|
+
yield page
|
|
27
|
+
|
|
28
|
+
async def query_data_sources_stream(
|
|
29
|
+
self,
|
|
30
|
+
search_config: WorkspaceQueryConfig,
|
|
31
|
+
) -> AsyncGenerator[DataSourceDto]:
|
|
32
|
+
async for data_source in paginate_notion_api_generator(
|
|
33
|
+
self._query_data_sources,
|
|
34
|
+
search_config=search_config,
|
|
35
|
+
):
|
|
36
|
+
yield data_source
|
|
37
|
+
|
|
38
|
+
async def _query_pages(
|
|
39
|
+
self,
|
|
40
|
+
search_config: WorkspaceQueryConfig,
|
|
41
|
+
start_cursor: str | None = None,
|
|
42
|
+
) -> PageSearchResponse:
|
|
43
|
+
if start_cursor:
|
|
44
|
+
search_config.start_cursor = start_cursor
|
|
45
|
+
|
|
46
|
+
response = await self._execute_search(search_config)
|
|
47
|
+
return PageSearchResponse.model_validate(response)
|
|
48
|
+
|
|
49
|
+
async def _query_data_sources(
|
|
50
|
+
self,
|
|
51
|
+
search_config: WorkspaceQueryConfig,
|
|
52
|
+
start_cursor: str | None = None,
|
|
53
|
+
) -> DataSourceSearchResponse:
|
|
54
|
+
if start_cursor:
|
|
55
|
+
search_config.start_cursor = start_cursor
|
|
56
|
+
|
|
57
|
+
response = await self._execute_search(search_config)
|
|
58
|
+
return DataSourceSearchResponse.model_validate(response)
|
|
59
|
+
|
|
60
|
+
async def _execute_search(self, config: WorkspaceQueryConfig) -> JsonDict:
|
|
61
|
+
serialized_config = config.model_dump(exclude_none=True, by_alias=True)
|
|
62
|
+
return await self._http_client.post("search", serialized_config)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
from notionary.workspace.query.models import (
|
|
4
|
+
SortDirection,
|
|
5
|
+
SortTimestamp,
|
|
6
|
+
WorkspaceQueryConfig,
|
|
7
|
+
WorkspaceQueryObjectType,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WorkspaceQueryConfigBuilder:
|
|
12
|
+
def __init__(self, config: WorkspaceQueryConfig = None) -> None:
|
|
13
|
+
self.config = config or WorkspaceQueryConfig()
|
|
14
|
+
|
|
15
|
+
def with_query(self, query: str) -> Self:
|
|
16
|
+
self.config.query = query
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
def with_pages_only(self) -> Self:
|
|
20
|
+
self.config.object_type = WorkspaceQueryObjectType.PAGE
|
|
21
|
+
return self
|
|
22
|
+
|
|
23
|
+
def with_data_sources_only(self) -> Self:
|
|
24
|
+
self.config.object_type = WorkspaceQueryObjectType.DATA_SOURCE
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
def with_sort_direction(self, direction: SortDirection) -> Self:
|
|
28
|
+
self.config.sort_direction = direction
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def with_sort_ascending(self) -> Self:
|
|
32
|
+
return self.with_sort_direction(SortDirection.ASCENDING)
|
|
33
|
+
|
|
34
|
+
def with_sort_descending(self) -> Self:
|
|
35
|
+
return self.with_sort_direction(SortDirection.DESCENDING)
|
|
36
|
+
|
|
37
|
+
def with_sort_timestamp(self, timestamp: SortTimestamp) -> Self:
|
|
38
|
+
self.config.sort_timestamp = timestamp
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
def with_sort_by_created_time(self) -> Self:
|
|
42
|
+
return self.with_sort_timestamp(SortTimestamp.CREATED_TIME)
|
|
43
|
+
|
|
44
|
+
def with_sort_by_last_edited(self) -> Self:
|
|
45
|
+
return self.with_sort_timestamp(SortTimestamp.LAST_EDITED_TIME)
|
|
46
|
+
|
|
47
|
+
def with_page_size(self, size: int) -> Self:
|
|
48
|
+
self.config.page_size = min(size, 100)
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def with_start_cursor(self, cursor: str | None) -> Self:
|
|
52
|
+
self.config.start_cursor = cursor
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def without_cursor(self) -> Self:
|
|
56
|
+
self.config.start_cursor = None
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def build(self) -> WorkspaceQueryConfig:
|
|
60
|
+
return self.config
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, field_validator, model_serializer
|
|
4
|
+
|
|
5
|
+
from notionary.shared.typings import JsonDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SortDirection(StrEnum):
|
|
9
|
+
ASCENDING = "ascending"
|
|
10
|
+
DESCENDING = "descending"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SortTimestamp(StrEnum):
|
|
14
|
+
LAST_EDITED_TIME = "last_edited_time"
|
|
15
|
+
CREATED_TIME = "created_time"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WorkspaceQueryObjectType(StrEnum):
|
|
19
|
+
PAGE = "page"
|
|
20
|
+
DATA_SOURCE = "data_source"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class WorkspaceQueryConfig(BaseModel):
|
|
24
|
+
query: str | None = None
|
|
25
|
+
object_type: WorkspaceQueryObjectType | None = None
|
|
26
|
+
sort_direction: SortDirection = SortDirection.DESCENDING
|
|
27
|
+
sort_timestamp: SortTimestamp = SortTimestamp.LAST_EDITED_TIME
|
|
28
|
+
page_size: int = Field(default=100, ge=1, le=100)
|
|
29
|
+
start_cursor: str | None = None
|
|
30
|
+
|
|
31
|
+
@field_validator("query")
|
|
32
|
+
@classmethod
|
|
33
|
+
def replace_empty_query_with_none(cls, value: str | None) -> str | None:
|
|
34
|
+
if value is not None and not value.strip():
|
|
35
|
+
return None
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
@model_serializer
|
|
39
|
+
def serialize_model(self) -> JsonDict:
|
|
40
|
+
search_dict: JsonDict = {}
|
|
41
|
+
|
|
42
|
+
if self.query:
|
|
43
|
+
search_dict["query"] = self.query
|
|
44
|
+
|
|
45
|
+
if self.object_type:
|
|
46
|
+
search_dict["filter"] = {
|
|
47
|
+
"property": "object",
|
|
48
|
+
"value": self.object_type.value,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
search_dict["sort"] = {
|
|
52
|
+
"direction": self.sort_direction.value,
|
|
53
|
+
"timestamp": self.sort_timestamp.value,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
search_dict["page_size"] = self.page_size
|
|
57
|
+
|
|
58
|
+
if self.start_cursor:
|
|
59
|
+
search_dict["start_cursor"] = self.start_cursor
|
|
60
|
+
|
|
61
|
+
return search_dict
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol
|
|
6
|
+
|
|
7
|
+
from notionary.exceptions.search import DatabaseNotFound, DataSourceNotFound, PageNotFound
|
|
8
|
+
from notionary.utils.fuzzy import find_all_matches
|
|
9
|
+
from notionary.workspace.client import WorkspaceClient
|
|
10
|
+
from notionary.workspace.query.builder import WorkspaceQueryConfigBuilder
|
|
11
|
+
from notionary.workspace.query.models import WorkspaceQueryConfig
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from notionary import NotionDatabase, NotionDataSource, NotionPage
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SearchableEntity(Protocol):
|
|
18
|
+
title: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WorkspaceQueryService:
|
|
22
|
+
def __init__(self, client: WorkspaceClient | None = None) -> None:
|
|
23
|
+
self._client = client or WorkspaceClient()
|
|
24
|
+
|
|
25
|
+
async def get_pages_stream(self, search_config: WorkspaceQueryConfig) -> AsyncIterator[NotionPage]:
|
|
26
|
+
from notionary import NotionPage
|
|
27
|
+
|
|
28
|
+
async for page_dto in self._client.query_pages_stream(search_config):
|
|
29
|
+
yield await NotionPage.from_id(page_dto.id)
|
|
30
|
+
|
|
31
|
+
async def get_pages(self, search_config: WorkspaceQueryConfig) -> list[NotionPage]:
|
|
32
|
+
from notionary import NotionPage
|
|
33
|
+
|
|
34
|
+
page_dtos = [dto async for dto in self._client.query_pages_stream(search_config)]
|
|
35
|
+
page_tasks = [NotionPage.from_id(dto.id) for dto in page_dtos]
|
|
36
|
+
return await asyncio.gather(*page_tasks)
|
|
37
|
+
|
|
38
|
+
async def get_data_sources_stream(self, search_config: WorkspaceQueryConfig) -> AsyncIterator[NotionDataSource]:
|
|
39
|
+
from notionary import NotionDataSource
|
|
40
|
+
|
|
41
|
+
async for data_source_dto in self._client.query_data_sources_stream(search_config):
|
|
42
|
+
yield await NotionDataSource.from_id(data_source_dto.id)
|
|
43
|
+
|
|
44
|
+
async def get_data_sources(self, search_config: WorkspaceQueryConfig) -> list[NotionDataSource]:
|
|
45
|
+
from notionary import NotionDataSource
|
|
46
|
+
|
|
47
|
+
data_source_dtos = [dto async for dto in self._client.query_data_sources_stream(search_config)]
|
|
48
|
+
data_source_tasks = [NotionDataSource.from_id(dto.id) for dto in data_source_dtos]
|
|
49
|
+
return await asyncio.gather(*data_source_tasks)
|
|
50
|
+
|
|
51
|
+
async def find_data_source(self, query: str) -> NotionDataSource:
|
|
52
|
+
config = WorkspaceQueryConfigBuilder().with_query(query).with_data_sources_only().with_page_size(100).build()
|
|
53
|
+
data_sources = await self.get_data_sources(config)
|
|
54
|
+
return self._find_exact_match(data_sources, query, DataSourceNotFound)
|
|
55
|
+
|
|
56
|
+
async def find_page(self, query: str) -> NotionPage:
|
|
57
|
+
config = WorkspaceQueryConfigBuilder().with_query(query).with_pages_only().with_page_size(100).build()
|
|
58
|
+
pages = await self.get_pages(config)
|
|
59
|
+
return self._find_exact_match(pages, query, PageNotFound)
|
|
60
|
+
|
|
61
|
+
async def find_database(self, query: str) -> NotionDatabase:
|
|
62
|
+
config = WorkspaceQueryConfigBuilder().with_query(query).with_data_sources_only().with_page_size(100).build()
|
|
63
|
+
data_sources = await self.get_data_sources(config)
|
|
64
|
+
|
|
65
|
+
parent_database_tasks = [data_source.get_parent_database() for data_source in data_sources]
|
|
66
|
+
parent_databases = await asyncio.gather(*parent_database_tasks)
|
|
67
|
+
potential_databases = [database for database in parent_databases if database is not None]
|
|
68
|
+
|
|
69
|
+
return self._find_exact_match(potential_databases, query, DatabaseNotFound)
|
|
70
|
+
|
|
71
|
+
def _find_exact_match(
|
|
72
|
+
self,
|
|
73
|
+
search_results: list[SearchableEntity],
|
|
74
|
+
query: str,
|
|
75
|
+
exception_class: type[Exception],
|
|
76
|
+
) -> SearchableEntity:
|
|
77
|
+
if not search_results:
|
|
78
|
+
raise exception_class(query, [])
|
|
79
|
+
|
|
80
|
+
query_lower = query.lower()
|
|
81
|
+
exact_matches = [result for result in search_results if result.title.lower() == query_lower]
|
|
82
|
+
|
|
83
|
+
if exact_matches:
|
|
84
|
+
return exact_matches[0]
|
|
85
|
+
|
|
86
|
+
suggestions = self._get_fuzzy_suggestions(search_results, query)
|
|
87
|
+
raise exception_class(query, suggestions)
|
|
88
|
+
|
|
89
|
+
def _get_fuzzy_suggestions(self, search_results: list[SearchableEntity], query: str) -> list[str]:
|
|
90
|
+
sorted_by_similarity = find_all_matches(
|
|
91
|
+
query=query,
|
|
92
|
+
items=search_results,
|
|
93
|
+
text_extractor=lambda entity: entity.title,
|
|
94
|
+
min_similarity=0.6,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if sorted_by_similarity:
|
|
98
|
+
return [result.title for result in sorted_by_similarity[:5]]
|
|
99
|
+
|
|
100
|
+
return [result.title for result in search_results[:5]]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Generic, Literal, TypeVar
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from notionary.data_source.schemas import DataSourceDto
|
|
6
|
+
from notionary.page.schemas import NotionPageDto
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T", bound=BaseModel)
|
|
9
|
+
|
|
10
|
+
PageOrDataSource = DataSourceDto | NotionPageDto
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SearchResponse(BaseModel, Generic[T]):
|
|
14
|
+
results: list[T]
|
|
15
|
+
next_cursor: str | None = None
|
|
16
|
+
has_more: bool
|
|
17
|
+
type: Literal["page_or_data_source"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
PageSearchResponse = SearchResponse[NotionPageDto]
|
|
21
|
+
DataSourceSearchResponse = SearchResponse[DataSourceDto]
|