notionary 0.2.26__py3-none-any.whl → 0.2.28__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/client.py +87 -215
- notionary/blocks/enums.py +167 -0
- notionary/blocks/rich_text/markdown_rich_text_converter.py +266 -0
- notionary/blocks/rich_text/models.py +164 -0
- notionary/blocks/rich_text/name_id_resolver/__init__.py +11 -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 +132 -0
- notionary/blocks/rich_text/rich_text_patterns.py +39 -0
- notionary/blocks/schemas.py +746 -0
- notionary/comments/client.py +52 -187
- notionary/comments/factory.py +40 -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 +94 -0
- notionary/data_source/properties/models.py +406 -0
- notionary/data_source/query/builder.py +429 -0
- notionary/data_source/query/resolver.py +114 -0
- notionary/data_source/query/schema.py +304 -0
- notionary/data_source/query/validator.py +73 -0
- notionary/data_source/schemas.py +27 -0
- notionary/data_source/service.py +353 -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 +169 -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 +33 -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 +205 -0
- notionary/http/models.py +49 -0
- notionary/page/blocks/client.py +1 -0
- notionary/page/content/factory.py +68 -0
- notionary/page/content/markdown/__init__.py +5 -0
- notionary/page/content/markdown/builder.py +304 -0
- notionary/page/content/markdown/nodes/__init__.py +54 -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 +18 -0
- notionary/page/content/markdown/nodes/callout.py +32 -0
- notionary/page/content/markdown/nodes/code.py +30 -0
- notionary/page/content/markdown/nodes/columns.py +51 -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 +16 -0
- notionary/page/content/markdown/nodes/image.py +23 -0
- notionary/page/content/markdown/nodes/mixins/caption.py +12 -0
- notionary/page/content/markdown/nodes/numbered_list.py +15 -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 +15 -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 +22 -0
- notionary/page/content/markdown/nodes/toggle.py +28 -0
- notionary/page/content/markdown/nodes/toggleable_heading.py +35 -0
- notionary/page/content/markdown/nodes/video.py +23 -0
- notionary/page/content/parser/context.py +49 -0
- notionary/page/content/parser/factory.py +219 -0
- notionary/page/content/parser/parsers/__init__.py +60 -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 +41 -0
- notionary/page/content/parser/parsers/callout.py +129 -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 +117 -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 +58 -0
- notionary/page/content/parser/parsers/image.py +42 -0
- notionary/page/content/parser/parsers/numbered_list.py +45 -0
- notionary/page/content/parser/parsers/paragraph.py +36 -0
- notionary/page/content/parser/parsers/pdf.py +42 -0
- notionary/page/content/parser/parsers/quote.py +65 -0
- notionary/page/content/parser/parsers/space.py +35 -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 +58 -0
- notionary/page/content/parser/parsers/toggle.py +127 -0
- notionary/page/content/parser/parsers/toggleable_heading.py +150 -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 +93 -0
- notionary/page/content/parser/post_processing/handlers/rich_text_length_truncation.py +93 -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 +9 -0
- notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +80 -0
- notionary/page/content/parser/pre_processsing/handlers/port.py +7 -0
- notionary/page/content/parser/pre_processsing/handlers/whitespace.py +68 -0
- notionary/page/content/parser/pre_processsing/service.py +15 -0
- notionary/page/content/parser/service.py +69 -0
- notionary/page/content/renderer/context.py +48 -0
- notionary/page/content/renderer/factory.py +240 -0
- notionary/page/content/renderer/post_processing/handlers/__init__.py +5 -0
- notionary/page/content/renderer/post_processing/handlers/numbered_list_placeholdere.py +62 -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 +57 -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 +65 -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 +44 -0
- notionary/page/content/renderer/renderers/column_list.py +31 -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 +69 -0
- notionary/page/content/renderer/renderers/image.py +31 -0
- notionary/page/content/renderer/renderers/numbered_list.py +41 -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 +53 -0
- notionary/page/content/renderer/renderers/toggleable_heading.py +78 -0
- notionary/page/content/renderer/renderers/video.py +31 -0
- notionary/page/content/renderer/service.py +50 -0
- notionary/page/content/service.py +65 -0
- notionary/page/content/syntax/models.py +68 -0
- notionary/page/content/syntax/service.py +453 -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 +307 -0
- notionary/page/properties/service.py +257 -0
- notionary/page/schemas.py +13 -0
- notionary/page/service.py +222 -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/user/__init__.py +4 -8
- notionary/user/base.py +89 -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/async_retry.py +39 -0
- notionary/utils/date.py +51 -0
- notionary/utils/fuzzy.py +56 -0
- notionary/{util/logging_mixin.py → utils/mixins/logging.py} +4 -16
- notionary/utils/pagination.py +50 -0
- notionary/utils/singleton.py +13 -0
- notionary/utils/uuid_utils.py +20 -0
- notionary/workspace/__init__.py +3 -0
- notionary/workspace/client.py +62 -0
- notionary/workspace/query/builder.py +60 -0
- notionary/workspace/query/models.py +60 -0
- notionary/workspace/query/service.py +93 -0
- notionary/workspace/schemas.py +21 -0
- notionary/workspace/service.py +116 -0
- {notionary-0.2.26.dist-info → notionary-0.2.28.dist-info}/METADATA +54 -49
- notionary-0.2.28.dist-info/RECORD +200 -0
- {notionary-0.2.26.dist-info → notionary-0.2.28.dist-info}/WHEEL +1 -1
- {notionary-0.2.26.dist-info → notionary-0.2.28.dist-info/licenses}/LICENSE +9 -9
- notionary/base_notion_client.py +0 -219
- notionary/blocks/__init__.py +0 -5
- 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/comments/__init__.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/factory.py +0 -0
- 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 -674
- 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/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.26.dist-info/RECORD +0 -202
notionary/file_upload/client.py
CHANGED
@@ -1,11 +1,11 @@
|
|
1
|
+
import traceback
|
1
2
|
from io import BytesIO
|
2
3
|
from pathlib import Path
|
3
|
-
from typing import BinaryIO
|
4
|
+
from typing import BinaryIO
|
4
5
|
|
5
6
|
import aiofiles
|
6
7
|
import httpx
|
7
8
|
|
8
|
-
from notionary.base_notion_client import BaseNotionClient
|
9
9
|
from notionary.file_upload.models import (
|
10
10
|
FileUploadCompleteRequest,
|
11
11
|
FileUploadCreateRequest,
|
@@ -13,21 +13,22 @@ from notionary.file_upload.models import (
|
|
13
13
|
FileUploadResponse,
|
14
14
|
UploadMode,
|
15
15
|
)
|
16
|
+
from notionary.http.client import NotionHttpClient
|
16
17
|
|
17
18
|
|
18
|
-
class
|
19
|
+
class FileUploadHttpClient(NotionHttpClient):
|
19
20
|
"""
|
20
21
|
Client for Notion file upload operations.
|
21
|
-
Inherits base HTTP functionality from
|
22
|
+
Inherits base HTTP functionality from NotionHttpClient.
|
22
23
|
"""
|
23
24
|
|
24
25
|
async def create_file_upload(
|
25
26
|
self,
|
26
27
|
filename: str,
|
27
|
-
content_type:
|
28
|
-
content_length:
|
28
|
+
content_type: str | None = None,
|
29
|
+
content_length: int | None = None,
|
29
30
|
mode: UploadMode = UploadMode.SINGLE_PART,
|
30
|
-
) ->
|
31
|
+
) -> FileUploadResponse | None:
|
31
32
|
"""
|
32
33
|
Create a new file upload.
|
33
34
|
|
@@ -61,8 +62,8 @@ class NotionFileUploadClient(BaseNotionClient):
|
|
61
62
|
self,
|
62
63
|
file_upload_id: str,
|
63
64
|
file_content: BinaryIO,
|
64
|
-
filename:
|
65
|
-
part_number:
|
65
|
+
filename: str | None = None,
|
66
|
+
part_number: int | None = None,
|
66
67
|
) -> bool:
|
67
68
|
"""
|
68
69
|
Send file content to Notion.
|
@@ -76,8 +77,6 @@ class NotionFileUploadClient(BaseNotionClient):
|
|
76
77
|
Returns:
|
77
78
|
True if successful, False otherwise
|
78
79
|
"""
|
79
|
-
await self.ensure_initialized()
|
80
|
-
|
81
80
|
if not self.client:
|
82
81
|
self.logger.error("HTTP client not initialized")
|
83
82
|
return False
|
@@ -110,9 +109,7 @@ class NotionFileUploadClient(BaseNotionClient):
|
|
110
109
|
# Explicitly do NOT set Content-Type - let httpx handle multipart
|
111
110
|
}
|
112
111
|
|
113
|
-
self.logger.debug(
|
114
|
-
"Sending file upload to %s with filename %s", url, filename
|
115
|
-
)
|
112
|
+
self.logger.debug("Sending file upload to %s with filename %s", url, filename)
|
116
113
|
|
117
114
|
# Use a temporary client for the multipart upload
|
118
115
|
async with httpx.AsyncClient(timeout=self.timeout) as upload_client:
|
@@ -130,7 +127,7 @@ class NotionFileUploadClient(BaseNotionClient):
|
|
130
127
|
except httpx.HTTPStatusError as e:
|
131
128
|
try:
|
132
129
|
error_text = e.response.text
|
133
|
-
except:
|
130
|
+
except Exception:
|
134
131
|
error_text = "Unable to read error response"
|
135
132
|
error_msg = f"HTTP {e.response.status_code}: {error_text}"
|
136
133
|
self.logger.error("Send file upload failed (%s): %s", url, error_msg)
|
@@ -142,14 +139,11 @@ class NotionFileUploadClient(BaseNotionClient):
|
|
142
139
|
|
143
140
|
except Exception as e:
|
144
141
|
self.logger.error("Unexpected error in send_file_upload: %s", str(e))
|
145
|
-
import traceback
|
146
142
|
|
147
143
|
self.logger.debug("Full traceback: %s", traceback.format_exc())
|
148
144
|
return False
|
149
145
|
|
150
|
-
async def complete_file_upload(
|
151
|
-
self, file_upload_id: str
|
152
|
-
) -> Optional[FileUploadResponse]:
|
146
|
+
async def complete_file_upload(self, file_upload_id: str) -> FileUploadResponse | None:
|
153
147
|
"""
|
154
148
|
Complete a multi-part file upload.
|
155
149
|
|
@@ -161,9 +155,7 @@ class NotionFileUploadClient(BaseNotionClient):
|
|
161
155
|
"""
|
162
156
|
request_data = FileUploadCompleteRequest()
|
163
157
|
|
164
|
-
response = await self.post(
|
165
|
-
f"file_uploads/{file_upload_id}/complete", data=request_data.model_dump()
|
166
|
-
)
|
158
|
+
response = await self.post(f"file_uploads/{file_upload_id}/complete", data=request_data.model_dump())
|
167
159
|
if response is None:
|
168
160
|
return None
|
169
161
|
|
@@ -173,9 +165,7 @@ class NotionFileUploadClient(BaseNotionClient):
|
|
173
165
|
self.logger.error("Failed to validate complete file upload response: %s", e)
|
174
166
|
return None
|
175
167
|
|
176
|
-
async def retrieve_file_upload(
|
177
|
-
self, file_upload_id: str
|
178
|
-
) -> Optional[FileUploadResponse]:
|
168
|
+
async def retrieve_file_upload(self, file_upload_id: str) -> FileUploadResponse | None:
|
179
169
|
"""
|
180
170
|
Retrieve details of a file upload.
|
181
171
|
|
@@ -196,8 +186,8 @@ class NotionFileUploadClient(BaseNotionClient):
|
|
196
186
|
return None
|
197
187
|
|
198
188
|
async def list_file_uploads(
|
199
|
-
self, page_size: int = 100, start_cursor:
|
200
|
-
) ->
|
189
|
+
self, page_size: int = 100, start_cursor: str | None = None
|
190
|
+
) -> FileUploadListResponse | None:
|
201
191
|
"""
|
202
192
|
List file uploads for the current bot integration.
|
203
193
|
|
@@ -222,9 +212,7 @@ class NotionFileUploadClient(BaseNotionClient):
|
|
222
212
|
self.logger.error("Failed to validate list file uploads response: %s", e)
|
223
213
|
return None
|
224
214
|
|
225
|
-
async def send_file_from_path(
|
226
|
-
self, file_upload_id: str, file_path: Path, part_number: Optional[int] = None
|
227
|
-
) -> bool:
|
215
|
+
async def send_file_from_path(self, file_upload_id: str, file_path: Path, part_number: int | None = None) -> bool:
|
228
216
|
"""
|
229
217
|
Convenience method to send file from file path.
|
230
218
|
|
notionary/file_upload/models.py
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
from enum import Enum
|
2
|
-
|
3
|
-
from typing import Literal, Optional
|
2
|
+
from typing import Literal
|
4
3
|
|
5
4
|
from pydantic import BaseModel
|
6
5
|
|
@@ -25,9 +24,9 @@ class FileUploadResponse(BaseModel):
|
|
25
24
|
upload_url: str
|
26
25
|
archived: bool
|
27
26
|
status: str # "pending", "uploaded", "failed", etc.
|
28
|
-
filename:
|
29
|
-
content_type:
|
30
|
-
content_length:
|
27
|
+
filename: str | None = None
|
28
|
+
content_type: str | None = None
|
29
|
+
content_length: int | None = None
|
31
30
|
request_id: str
|
32
31
|
|
33
32
|
|
@@ -38,7 +37,7 @@ class FileUploadListResponse(BaseModel):
|
|
38
37
|
|
39
38
|
object: Literal["list"]
|
40
39
|
results: list[FileUploadResponse]
|
41
|
-
next_cursor:
|
40
|
+
next_cursor: str | None = None
|
42
41
|
has_more: bool
|
43
42
|
type: Literal["file_upload"]
|
44
43
|
file_upload: dict = {}
|
@@ -51,8 +50,8 @@ class FileUploadCreateRequest(BaseModel):
|
|
51
50
|
"""
|
52
51
|
|
53
52
|
filename: str
|
54
|
-
content_type:
|
55
|
-
content_length:
|
53
|
+
content_type: str | None = None
|
54
|
+
content_length: int | None = None
|
56
55
|
mode: UploadMode = UploadMode.SINGLE_PART
|
57
56
|
|
58
57
|
def model_dump(self, **kwargs):
|
@@ -3,10 +3,9 @@ import mimetypes
|
|
3
3
|
from datetime import datetime, timedelta
|
4
4
|
from io import BytesIO
|
5
5
|
from pathlib import Path
|
6
|
-
from typing import Optional
|
7
6
|
|
8
7
|
from notionary.file_upload.models import FileUploadResponse, UploadMode
|
9
|
-
from notionary.
|
8
|
+
from notionary.utils.mixins.logging import LoggingMixin
|
10
9
|
|
11
10
|
|
12
11
|
class NotionFileUpload(LoggingMixin):
|
@@ -20,15 +19,13 @@ class NotionFileUpload(LoggingMixin):
|
|
20
19
|
MULTI_PART_CHUNK_SIZE = 10 * 1024 * 1024 # 10MB per part
|
21
20
|
MAX_FILENAME_BYTES = 900
|
22
21
|
|
23
|
-
def __init__(self, token:
|
22
|
+
def __init__(self, token: str | None = None):
|
24
23
|
"""Initialize the file upload service."""
|
25
|
-
from notionary.file_upload import
|
24
|
+
from notionary.file_upload import FileUploadHttpClient
|
26
25
|
|
27
|
-
self.client =
|
26
|
+
self.client = FileUploadHttpClient(token=token)
|
28
27
|
|
29
|
-
async def upload_file(
|
30
|
-
self, file_path: Path, filename: Optional[str] = None
|
31
|
-
) -> Optional[FileUploadResponse]:
|
28
|
+
async def upload_file(self, file_path: Path, filename: str | None = None) -> FileUploadResponse | None:
|
32
29
|
"""
|
33
30
|
Upload a file to Notion, automatically choosing single-part or multi-part based on size.
|
34
31
|
|
@@ -62,8 +59,8 @@ class NotionFileUpload(LoggingMixin):
|
|
62
59
|
return await self._upload_large_file(file_path, filename, file_size)
|
63
60
|
|
64
61
|
async def upload_from_bytes(
|
65
|
-
self, file_content: bytes, filename: str, content_type:
|
66
|
-
) ->
|
62
|
+
self, file_content: bytes, filename: str, content_type: str | None = None
|
63
|
+
) -> FileUploadResponse | None:
|
67
64
|
"""
|
68
65
|
Upload file content from bytes.
|
69
66
|
|
@@ -92,15 +89,11 @@ class NotionFileUpload(LoggingMixin):
|
|
92
89
|
|
93
90
|
# Choose upload method based on size
|
94
91
|
if file_size <= self.SINGLE_PART_MAX_SIZE:
|
95
|
-
return await self._upload_small_file_from_bytes(
|
96
|
-
file_content, filename, content_type, file_size
|
97
|
-
)
|
92
|
+
return await self._upload_small_file_from_bytes(file_content, filename, content_type, file_size)
|
98
93
|
else:
|
99
|
-
return await self._upload_large_file_from_bytes(
|
100
|
-
file_content, filename, content_type, file_size
|
101
|
-
)
|
94
|
+
return await self._upload_large_file_from_bytes(file_content, filename, content_type, file_size)
|
102
95
|
|
103
|
-
async def get_upload_status(self, file_upload_id: str) ->
|
96
|
+
async def get_upload_status(self, file_upload_id: str) -> str | None:
|
104
97
|
"""
|
105
98
|
Get the current status of a file upload.
|
106
99
|
|
@@ -115,7 +108,7 @@ class NotionFileUpload(LoggingMixin):
|
|
115
108
|
|
116
109
|
async def wait_for_upload_completion(
|
117
110
|
self, file_upload_id: str, timeout_seconds: int = 300, poll_interval: int = 2
|
118
|
-
) ->
|
111
|
+
) -> FileUploadResponse | None:
|
119
112
|
"""
|
120
113
|
Wait for a file upload to complete.
|
121
114
|
|
@@ -134,9 +127,7 @@ class NotionFileUpload(LoggingMixin):
|
|
134
127
|
upload_info = await self.client.retrieve_file_upload(file_upload_id)
|
135
128
|
|
136
129
|
if not upload_info:
|
137
|
-
self.logger.error(
|
138
|
-
"Failed to retrieve upload info for %s", file_upload_id
|
139
|
-
)
|
130
|
+
self.logger.error("Failed to retrieve upload info for %s", file_upload_id)
|
140
131
|
return None
|
141
132
|
|
142
133
|
if upload_info.status == "uploaded":
|
@@ -168,9 +159,7 @@ class NotionFileUpload(LoggingMixin):
|
|
168
159
|
while remaining > 0:
|
169
160
|
page_size = min(remaining, 100) # API max per request
|
170
161
|
|
171
|
-
response = await self.client.list_file_uploads(
|
172
|
-
page_size=page_size, start_cursor=start_cursor
|
173
|
-
)
|
162
|
+
response = await self.client.list_file_uploads(page_size=page_size, start_cursor=start_cursor)
|
174
163
|
|
175
164
|
if not response or not response.results:
|
176
165
|
break
|
@@ -185,9 +174,7 @@ class NotionFileUpload(LoggingMixin):
|
|
185
174
|
|
186
175
|
return uploads[:limit]
|
187
176
|
|
188
|
-
async def _upload_small_file(
|
189
|
-
self, file_path: Path, filename: str, file_size: int
|
190
|
-
) -> Optional[FileUploadResponse]:
|
177
|
+
async def _upload_small_file(self, file_path: Path, filename: str, file_size: int) -> FileUploadResponse | None:
|
191
178
|
"""Upload a small file using single-part upload."""
|
192
179
|
content_type, _ = mimetypes.guess_type(str(file_path))
|
193
180
|
|
@@ -204,22 +191,16 @@ class NotionFileUpload(LoggingMixin):
|
|
204
191
|
return None
|
205
192
|
|
206
193
|
# Send file content
|
207
|
-
success = await self.client.send_file_from_path(
|
208
|
-
file_upload_id=file_upload.id, file_path=file_path
|
209
|
-
)
|
194
|
+
success = await self.client.send_file_from_path(file_upload_id=file_upload.id, file_path=file_path)
|
210
195
|
|
211
196
|
if not success:
|
212
197
|
self.logger.error("Failed to send file content for %s", filename)
|
213
198
|
return None
|
214
199
|
|
215
|
-
self.logger.info(
|
216
|
-
"Successfully uploaded file: %s (ID: %s)", filename, file_upload.id
|
217
|
-
)
|
200
|
+
self.logger.info("Successfully uploaded file: %s (ID: %s)", filename, file_upload.id)
|
218
201
|
return file_upload
|
219
202
|
|
220
|
-
async def _upload_large_file(
|
221
|
-
self, file_path: Path, filename: str, file_size: int
|
222
|
-
) -> Optional[FileUploadResponse]:
|
203
|
+
async def _upload_large_file(self, file_path: Path, filename: str, file_size: int) -> FileUploadResponse | None:
|
223
204
|
"""Upload a large file using multi-part upload."""
|
224
205
|
content_type, _ = mimetypes.guess_type(str(file_path))
|
225
206
|
|
@@ -232,9 +213,7 @@ class NotionFileUpload(LoggingMixin):
|
|
232
213
|
)
|
233
214
|
|
234
215
|
if not file_upload:
|
235
|
-
self.logger.error(
|
236
|
-
"Failed to create multi-part file upload for %s", filename
|
237
|
-
)
|
216
|
+
self.logger.error("Failed to create multi-part file upload for %s", filename)
|
238
217
|
return None
|
239
218
|
|
240
219
|
# Upload file in parts
|
@@ -251,18 +230,16 @@ class NotionFileUpload(LoggingMixin):
|
|
251
230
|
self.logger.error("Failed to complete file upload for %s", filename)
|
252
231
|
return None
|
253
232
|
|
254
|
-
self.logger.info(
|
255
|
-
"Successfully uploaded large file: %s (ID: %s)", filename, file_upload.id
|
256
|
-
)
|
233
|
+
self.logger.info("Successfully uploaded large file: %s (ID: %s)", filename, file_upload.id)
|
257
234
|
return completed_upload
|
258
235
|
|
259
236
|
async def _upload_small_file_from_bytes(
|
260
237
|
self,
|
261
238
|
file_content: bytes,
|
262
239
|
filename: str,
|
263
|
-
content_type:
|
240
|
+
content_type: str | None,
|
264
241
|
file_size: int,
|
265
|
-
) ->
|
242
|
+
) -> FileUploadResponse | None:
|
266
243
|
"""Upload small file from bytes."""
|
267
244
|
# Create file upload
|
268
245
|
file_upload = await self.client.create_file_upload(
|
@@ -290,9 +267,9 @@ class NotionFileUpload(LoggingMixin):
|
|
290
267
|
self,
|
291
268
|
file_content: bytes,
|
292
269
|
filename: str,
|
293
|
-
content_type:
|
270
|
+
content_type: str | None,
|
294
271
|
file_size: int,
|
295
|
-
) ->
|
272
|
+
) -> FileUploadResponse | None:
|
296
273
|
"""Upload large file from bytes using multi-part."""
|
297
274
|
# Create file upload
|
298
275
|
file_upload = await self.client.create_file_upload(
|
@@ -314,14 +291,10 @@ class NotionFileUpload(LoggingMixin):
|
|
314
291
|
# Complete the upload
|
315
292
|
return await self.client.complete_file_upload(file_upload.id)
|
316
293
|
|
317
|
-
async def _upload_file_parts(
|
318
|
-
self, file_upload_id: str, file_path: Path, file_size: int
|
319
|
-
) -> bool:
|
294
|
+
async def _upload_file_parts(self, file_upload_id: str, file_path: Path, file_size: int) -> bool:
|
320
295
|
"""Upload file in parts for multi-part upload."""
|
321
296
|
part_number = 1
|
322
|
-
total_parts = (
|
323
|
-
file_size + self.MULTI_PART_CHUNK_SIZE - 1
|
324
|
-
) // self.MULTI_PART_CHUNK_SIZE
|
297
|
+
total_parts = (file_size + self.MULTI_PART_CHUNK_SIZE - 1) // self.MULTI_PART_CHUNK_SIZE
|
325
298
|
|
326
299
|
try:
|
327
300
|
import aiofiles
|
@@ -340,9 +313,7 @@ class NotionFileUpload(LoggingMixin):
|
|
340
313
|
)
|
341
314
|
|
342
315
|
if not success:
|
343
|
-
self.logger.error(
|
344
|
-
"Failed to upload part %d/%d", part_number, total_parts
|
345
|
-
)
|
316
|
+
self.logger.error("Failed to upload part %d/%d", part_number, total_parts)
|
346
317
|
return False
|
347
318
|
|
348
319
|
self.logger.debug("Uploaded part %d/%d", part_number, total_parts)
|
@@ -355,14 +326,10 @@ class NotionFileUpload(LoggingMixin):
|
|
355
326
|
self.logger.error("Error uploading file parts: %s", e)
|
356
327
|
return False
|
357
328
|
|
358
|
-
async def _upload_bytes_parts(
|
359
|
-
self, file_upload_id: str, file_content: bytes
|
360
|
-
) -> bool:
|
329
|
+
async def _upload_bytes_parts(self, file_upload_id: str, file_content: bytes) -> bool:
|
361
330
|
"""Upload bytes in parts for multi-part upload."""
|
362
331
|
part_number = 1
|
363
|
-
total_parts = (
|
364
|
-
len(file_content) + self.MULTI_PART_CHUNK_SIZE - 1
|
365
|
-
) // self.MULTI_PART_CHUNK_SIZE
|
332
|
+
total_parts = (len(file_content) + self.MULTI_PART_CHUNK_SIZE - 1) // self.MULTI_PART_CHUNK_SIZE
|
366
333
|
|
367
334
|
for i in range(0, len(file_content), self.MULTI_PART_CHUNK_SIZE):
|
368
335
|
chunk = file_content[i : i + self.MULTI_PART_CHUNK_SIZE]
|
@@ -374,9 +341,7 @@ class NotionFileUpload(LoggingMixin):
|
|
374
341
|
)
|
375
342
|
|
376
343
|
if not success:
|
377
|
-
self.logger.error(
|
378
|
-
"Failed to upload part %d/%d", part_number, total_parts
|
379
|
-
)
|
344
|
+
self.logger.error("Failed to upload part %d/%d", part_number, total_parts)
|
380
345
|
return False
|
381
346
|
|
382
347
|
self.logger.debug("Uploaded part %d/%d", part_number, total_parts)
|
notionary/http/client.py
ADDED
@@ -0,0 +1,205 @@
|
|
1
|
+
import asyncio
|
2
|
+
import os
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
import httpx
|
6
|
+
from dotenv import load_dotenv
|
7
|
+
|
8
|
+
from notionary.exceptions.api import (
|
9
|
+
NotionApiError,
|
10
|
+
NotionAuthenticationError,
|
11
|
+
NotionConnectionError,
|
12
|
+
NotionPermissionError,
|
13
|
+
NotionRateLimitError,
|
14
|
+
NotionResourceNotFoundError,
|
15
|
+
NotionServerError,
|
16
|
+
NotionValidationError,
|
17
|
+
)
|
18
|
+
from notionary.http.models import HttpMethod
|
19
|
+
from notionary.utils.mixins.logging import LoggingMixin
|
20
|
+
|
21
|
+
load_dotenv()
|
22
|
+
|
23
|
+
|
24
|
+
class NotionHttpClient(LoggingMixin):
|
25
|
+
BASE_URL = "https://api.notion.com/v1"
|
26
|
+
NOTION_VERSION = "2025-09-03"
|
27
|
+
|
28
|
+
def __init__(self, timeout: int = 30):
|
29
|
+
self.token = self._find_token()
|
30
|
+
if not self.token:
|
31
|
+
raise ValueError(
|
32
|
+
"No Notion API token found in environment variables. Please set one of these environment variables: "
|
33
|
+
"NOTION_SECRET, NOTION_INTEGRATION_KEY, or NOTION_TOKEN"
|
34
|
+
)
|
35
|
+
|
36
|
+
self.headers = {
|
37
|
+
"Authorization": f"Bearer {self.token}",
|
38
|
+
"Content-Type": "application/json",
|
39
|
+
"Notion-Version": self.NOTION_VERSION,
|
40
|
+
}
|
41
|
+
|
42
|
+
self.client: httpx.AsyncClient | None = None
|
43
|
+
self.timeout = timeout
|
44
|
+
self._is_initialized = False
|
45
|
+
|
46
|
+
def __del__(self):
|
47
|
+
"""Auto-cleanup when client is destroyed."""
|
48
|
+
if not hasattr(self, "client") or not self.client:
|
49
|
+
return
|
50
|
+
|
51
|
+
try:
|
52
|
+
loop = asyncio.get_event_loop()
|
53
|
+
if not loop.is_running():
|
54
|
+
self.logger.warning("Event loop not running, could not auto-close NotionHttpClient")
|
55
|
+
return
|
56
|
+
|
57
|
+
loop.create_task(self.close())
|
58
|
+
self.logger.debug("Created cleanup task for NotionHttpClient")
|
59
|
+
except RuntimeError:
|
60
|
+
self.logger.warning("No event loop available for auto-closing NotionHttpClient")
|
61
|
+
|
62
|
+
async def __aenter__(self):
|
63
|
+
"""Async context manager entry."""
|
64
|
+
await self._ensure_initialized()
|
65
|
+
return self
|
66
|
+
|
67
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
68
|
+
"""Async context manager exit."""
|
69
|
+
await self.close()
|
70
|
+
|
71
|
+
async def close(self) -> None:
|
72
|
+
"""
|
73
|
+
Closes the HTTP client and releases resources.
|
74
|
+
"""
|
75
|
+
if not hasattr(self, "client") or not self.client:
|
76
|
+
return
|
77
|
+
|
78
|
+
await self.client.aclose()
|
79
|
+
self.client = None
|
80
|
+
self._is_initialized = False
|
81
|
+
self.logger.debug("NotionHttpClient closed")
|
82
|
+
|
83
|
+
async def get(self, endpoint: str, params: dict[str, Any] | None = None) -> dict[str, Any] | None:
|
84
|
+
return await self.make_request(HttpMethod.GET, endpoint, params=params)
|
85
|
+
|
86
|
+
async def post(self, endpoint: str, data: dict[str, Any] | None = None) -> dict[str, Any] | None:
|
87
|
+
return await self.make_request(HttpMethod.POST, endpoint, data)
|
88
|
+
|
89
|
+
async def patch(self, endpoint: str, data: dict[str, Any] | None = None) -> dict[str, Any] | None:
|
90
|
+
return await self.make_request(HttpMethod.PATCH, endpoint, data)
|
91
|
+
|
92
|
+
async def delete(self, endpoint: str) -> dict[str, Any] | None:
|
93
|
+
return await self.make_request(HttpMethod.DELETE, endpoint)
|
94
|
+
|
95
|
+
async def make_request(
|
96
|
+
self,
|
97
|
+
method: HttpMethod,
|
98
|
+
endpoint: str,
|
99
|
+
data: dict[str, Any] | None = None,
|
100
|
+
params: dict[str, Any] | None = None,
|
101
|
+
) -> dict[str, Any] | None:
|
102
|
+
"""
|
103
|
+
Executes an HTTP request and returns the data or None on error.
|
104
|
+
|
105
|
+
Args:
|
106
|
+
method: HTTP method to use
|
107
|
+
endpoint: API endpoint
|
108
|
+
data: Request body data (for POST/PATCH)
|
109
|
+
params: Query parameters (for GET requests)
|
110
|
+
"""
|
111
|
+
await self._ensure_initialized()
|
112
|
+
|
113
|
+
url = f"{self.BASE_URL}/{endpoint.lstrip('/')}"
|
114
|
+
try:
|
115
|
+
self.logger.debug("Sending %s request to %s", method.value.upper(), url)
|
116
|
+
|
117
|
+
request_kwargs = {}
|
118
|
+
|
119
|
+
# Add query parameters for GET requests
|
120
|
+
if params:
|
121
|
+
request_kwargs["params"] = params
|
122
|
+
|
123
|
+
if method.value in [HttpMethod.POST.value, HttpMethod.PATCH.value] and data is not None:
|
124
|
+
request_kwargs["json"] = data
|
125
|
+
|
126
|
+
response: httpx.Response = await getattr(self.client, method.value)(url, **request_kwargs)
|
127
|
+
|
128
|
+
response.raise_for_status()
|
129
|
+
result_data = response.json()
|
130
|
+
self.logger.debug("Request successful: %s", url)
|
131
|
+
return result_data
|
132
|
+
|
133
|
+
except httpx.HTTPStatusError as e:
|
134
|
+
self._handle_http_status_error(e)
|
135
|
+
except httpx.RequestError as e:
|
136
|
+
raise NotionConnectionError(
|
137
|
+
f"Failed to connect to Notion API: {e!s}. Please check your internet connection and try again."
|
138
|
+
) from e
|
139
|
+
|
140
|
+
def _handle_http_status_error(self, e: httpx.HTTPStatusError) -> None:
|
141
|
+
status_code = e.response.status_code
|
142
|
+
response_text = e.response.text
|
143
|
+
|
144
|
+
# Map HTTP status codes to specific business exceptions
|
145
|
+
if status_code == 401:
|
146
|
+
raise NotionAuthenticationError(
|
147
|
+
"Invalid or missing API key. Please check your Notion integration token.",
|
148
|
+
status_code=status_code,
|
149
|
+
response_text=response_text,
|
150
|
+
)
|
151
|
+
if status_code == 403:
|
152
|
+
raise NotionPermissionError(
|
153
|
+
"Insufficient permissions. Please check your integration settings at "
|
154
|
+
"https://www.notion.so/profile/integrations and ensure the integration "
|
155
|
+
"has access to the required pages/databases.",
|
156
|
+
status_code=status_code,
|
157
|
+
response_text=response_text,
|
158
|
+
)
|
159
|
+
if status_code == 404:
|
160
|
+
raise NotionResourceNotFoundError(
|
161
|
+
"The requested resource was not found. Please verify the page/database ID.",
|
162
|
+
status_code=status_code,
|
163
|
+
response_text=response_text,
|
164
|
+
)
|
165
|
+
if status_code == 400:
|
166
|
+
raise NotionValidationError(
|
167
|
+
f"Invalid request data. Please check your input parameters: {response_text}",
|
168
|
+
status_code=status_code,
|
169
|
+
response_text=response_text,
|
170
|
+
)
|
171
|
+
if status_code == 429:
|
172
|
+
raise NotionRateLimitError(
|
173
|
+
"Rate limit exceeded. Please wait before making more requests.",
|
174
|
+
status_code=status_code,
|
175
|
+
response_text=response_text,
|
176
|
+
)
|
177
|
+
if 500 <= status_code < 600:
|
178
|
+
raise NotionServerError(
|
179
|
+
"Notion API server error. Please try again later.",
|
180
|
+
status_code=status_code,
|
181
|
+
response_text=response_text,
|
182
|
+
)
|
183
|
+
|
184
|
+
raise NotionApiError(
|
185
|
+
f"API request failed with status {status_code}: {response_text}",
|
186
|
+
status_code=status_code,
|
187
|
+
response_text=response_text,
|
188
|
+
)
|
189
|
+
|
190
|
+
def _find_token(self) -> str | None:
|
191
|
+
token = next(
|
192
|
+
(os.getenv(var) for var in ("NOTION_SECRET", "NOTION_INTEGRATION_KEY", "NOTION_TOKEN") if os.getenv(var)),
|
193
|
+
None,
|
194
|
+
)
|
195
|
+
if token:
|
196
|
+
self.logger.debug("Found token in environment variable.")
|
197
|
+
return token
|
198
|
+
self.logger.warning("No Notion API token found in environment variables")
|
199
|
+
return None
|
200
|
+
|
201
|
+
async def _ensure_initialized(self) -> None:
|
202
|
+
if not self._is_initialized or not self.client:
|
203
|
+
self.client = httpx.AsyncClient(headers=self.headers, timeout=self.timeout)
|
204
|
+
self._is_initialized = True
|
205
|
+
self.logger.debug("NotionHttpClient initialized")
|
notionary/http/models.py
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import time
|
4
|
+
from dataclasses import dataclass, field
|
5
|
+
from enum import StrEnum
|
6
|
+
from typing import Any
|
7
|
+
|
8
|
+
|
9
|
+
class HttpMethod(StrEnum):
|
10
|
+
GET = "get"
|
11
|
+
POST = "post"
|
12
|
+
PATCH = "patch"
|
13
|
+
DELETE = "delete"
|
14
|
+
|
15
|
+
|
16
|
+
@dataclass
|
17
|
+
class HttpRequest:
|
18
|
+
method: HttpMethod
|
19
|
+
endpoint: str
|
20
|
+
data: dict[str, Any] | None = None
|
21
|
+
params: dict[str, Any] | None = None
|
22
|
+
timestamp: float = field(default_factory=time.time)
|
23
|
+
cached_response: HttpResponse | None = None
|
24
|
+
|
25
|
+
@property
|
26
|
+
def cache_key(self) -> str:
|
27
|
+
key_parts = [self.method.value, self.endpoint]
|
28
|
+
|
29
|
+
if self.params:
|
30
|
+
sorted_params = sorted(self.params.items())
|
31
|
+
key_parts.extend(f"{k}={v}" for k, v in sorted_params)
|
32
|
+
|
33
|
+
return "|".join(key_parts)
|
34
|
+
|
35
|
+
def __repr__(self) -> str:
|
36
|
+
return f"HttpRequest(method={self.method.value}, endpoint={self.endpoint})"
|
37
|
+
|
38
|
+
|
39
|
+
@dataclass
|
40
|
+
class HttpResponse:
|
41
|
+
data: dict[str, Any] | None
|
42
|
+
status_code: int = 200
|
43
|
+
headers: dict[str, str] = field(default_factory=dict)
|
44
|
+
timestamp: float = field(default_factory=time.time)
|
45
|
+
from_cache: bool = False
|
46
|
+
error: Exception | None = None
|
47
|
+
|
48
|
+
def __repr__(self) -> str:
|
49
|
+
return f"HttpResponse(status_code={self.status_code}, from_cache={self.from_cache})"
|
@@ -0,0 +1 @@
|
|
1
|
+
# PageBlockClient
|