frameio 0.0.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.
- frameio/__init__.py +1158 -0
- frameio/account_permissions/__init__.py +4 -0
- frameio/account_permissions/client.py +193 -0
- frameio/account_permissions/raw_client.py +333 -0
- frameio/accounts/__init__.py +4 -0
- frameio/accounts/client.py +162 -0
- frameio/accounts/raw_client.py +309 -0
- frameio/client.py +439 -0
- frameio/comments/__init__.py +38 -0
- frameio/comments/client.py +654 -0
- frameio/comments/raw_client.py +1316 -0
- frameio/comments/types/__init__.py +40 -0
- frameio/comments/types/comments_show_request_include.py +5 -0
- frameio/comments/types/create_comment_params_data.py +33 -0
- frameio/comments/types/update_comment_params_data.py +33 -0
- frameio/core/__init__.py +122 -0
- frameio/core/api_error.py +23 -0
- frameio/core/client_wrapper.py +95 -0
- frameio/core/custom_pagination.py +152 -0
- frameio/core/datetime_utils.py +28 -0
- frameio/core/file.py +67 -0
- frameio/core/force_multipart.py +18 -0
- frameio/core/http_client.py +663 -0
- frameio/core/http_response.py +55 -0
- frameio/core/http_sse/__init__.py +42 -0
- frameio/core/http_sse/_api.py +112 -0
- frameio/core/http_sse/_decoders.py +61 -0
- frameio/core/http_sse/_exceptions.py +7 -0
- frameio/core/http_sse/_models.py +17 -0
- frameio/core/jsonable_encoder.py +100 -0
- frameio/core/pagination.py +82 -0
- frameio/core/pydantic_utilities.py +361 -0
- frameio/core/query_encoder.py +58 -0
- frameio/core/remove_none_from_dict.py +11 -0
- frameio/core/request_options.py +35 -0
- frameio/core/serialization.py +276 -0
- frameio/core/unchecked_base_model.py +376 -0
- frameio/environment.py +7 -0
- frameio/errors/__init__.py +53 -0
- frameio/errors/bad_request_error.py +11 -0
- frameio/errors/forbidden_error.py +11 -0
- frameio/errors/not_found_error.py +11 -0
- frameio/errors/too_many_requests_error.py +11 -0
- frameio/errors/unauthorized_error.py +11 -0
- frameio/errors/unprocessable_entity_error.py +10 -0
- frameio/files/__init__.py +58 -0
- frameio/files/client.py +1171 -0
- frameio/files/raw_client.py +2517 -0
- frameio/files/types/__init__.py +56 -0
- frameio/files/types/file_copy_params_data.py +15 -0
- frameio/files/types/file_create_local_upload_params_data.py +20 -0
- frameio/files/types/file_create_params_data.py +31 -0
- frameio/files/types/file_create_remote_upload_params_data.py +20 -0
- frameio/files/types/file_move_params_data.py +15 -0
- frameio/files/types/file_update_params_data.py +15 -0
- frameio/files/types/files_copy_request_copy_comments.py +5 -0
- frameio/folders/__init__.py +39 -0
- frameio/folders/client.py +1004 -0
- frameio/folders/raw_client.py +2074 -0
- frameio/folders/types/__init__.py +42 -0
- frameio/folders/types/folder_copy_params_data.py +15 -0
- frameio/folders/types/folder_create_params_data.py +15 -0
- frameio/folders/types/folder_move_params_data.py +15 -0
- frameio/folders/types/folder_update_params_data.py +15 -0
- frameio/metadata/__init__.py +37 -0
- frameio/metadata/client.py +293 -0
- frameio/metadata/raw_client.py +509 -0
- frameio/metadata/types/__init__.py +38 -0
- frameio/metadata/types/bulk_update_metadata_params_data.py +23 -0
- frameio/metadata/types/bulk_update_metadata_params_data_values_item.py +13 -0
- frameio/metadata_fields/__init__.py +103 -0
- frameio/metadata_fields/client.py +536 -0
- frameio/metadata_fields/raw_client.py +996 -0
- frameio/metadata_fields/types/__init__.py +105 -0
- frameio/metadata_fields/types/create_field_definition_params_data.py +112 -0
- frameio/metadata_fields/types/update_field_definition_params_data.py +118 -0
- frameio/project_permissions/__init__.py +4 -0
- frameio/project_permissions/client.py +426 -0
- frameio/project_permissions/raw_client.py +824 -0
- frameio/projects/__init__.py +38 -0
- frameio/projects/client.py +604 -0
- frameio/projects/raw_client.py +1286 -0
- frameio/projects/types/__init__.py +40 -0
- frameio/projects/types/project_params_data.py +20 -0
- frameio/projects/types/project_update_params_data.py +26 -0
- frameio/projects/types/project_update_params_data_status.py +5 -0
- frameio/py.typed +0 -0
- frameio/shares/__init__.py +64 -0
- frameio/shares/client.py +1217 -0
- frameio/shares/raw_client.py +2511 -0
- frameio/shares/types/__init__.py +61 -0
- frameio/shares/types/add_asset_params_data.py +15 -0
- frameio/shares/types/add_reviewers_to_share_params_data.py +18 -0
- frameio/shares/types/add_reviewers_to_share_params_data_reviewers.py +27 -0
- frameio/shares/types/create_share_params_data.py +26 -0
- frameio/shares/types/remove_reviewer_params_data.py +13 -0
- frameio/shares/types/remove_reviewer_params_data_reviewers.py +27 -0
- frameio/shares/types/update_share_params_data.py +34 -0
- frameio/shares/types/update_share_params_data_access.py +5 -0
- frameio/types/__init__.py +983 -0
- frameio/types/account.py +70 -0
- frameio/types/account_roles_item.py +5 -0
- frameio/types/account_user_role.py +23 -0
- frameio/types/account_user_role_role.py +5 -0
- frameio/types/account_user_roles_response.py +27 -0
- frameio/types/accounts_response.py +23 -0
- frameio/types/add_asset_response.py +13 -0
- frameio/types/add_asset_response_data.py +15 -0
- frameio/types/asset_common.py +48 -0
- frameio/types/asset_common_type.py +5 -0
- frameio/types/asset_common_with_includes.py +58 -0
- frameio/types/asset_common_with_includes_type.py +5 -0
- frameio/types/asset_include.py +5 -0
- frameio/types/asset_share_params.py +39 -0
- frameio/types/asset_share_params_access.py +5 -0
- frameio/types/asset_with_includes.py +76 -0
- frameio/types/assets_with_includes_response.py +23 -0
- frameio/types/bad_request.py +13 -0
- frameio/types/bad_request_errors_item.py +15 -0
- frameio/types/bad_request_errors_item_source.py +12 -0
- frameio/types/boolean_value.py +12 -0
- frameio/types/children_type.py +3 -0
- frameio/types/comment.py +59 -0
- frameio/types/comment_include.py +5 -0
- frameio/types/comment_response.py +13 -0
- frameio/types/comment_with_includes.py +17 -0
- frameio/types/comment_with_includes_response.py +13 -0
- frameio/types/comments_with_includes_response.py +23 -0
- frameio/types/date_definition.py +49 -0
- frameio/types/date_definition_field_configuration.py +21 -0
- frameio/types/date_definition_field_configuration_display_format.py +7 -0
- frameio/types/date_definition_field_configuration_time_format.py +5 -0
- frameio/types/date_definition_field_type.py +10 -0
- frameio/types/date_definition_params.py +17 -0
- frameio/types/date_definition_params_field_configuration.py +23 -0
- frameio/types/date_definition_params_field_configuration_display_format.py +7 -0
- frameio/types/date_definition_params_field_configuration_time_format.py +7 -0
- frameio/types/date_definition_with_includes.py +13 -0
- frameio/types/date_value.py +12 -0
- frameio/types/email.py +3 -0
- frameio/types/empty_json.py +5 -0
- frameio/types/field_definition.py +167 -0
- frameio/types/field_definition_include.py +5 -0
- frameio/types/field_definition_response.py +13 -0
- frameio/types/field_definition_with_includes.py +178 -0
- frameio/types/field_definitions_with_includes_response.py +27 -0
- frameio/types/field_value_common.py +29 -0
- frameio/types/file.py +60 -0
- frameio/types/file_copy_response.py +13 -0
- frameio/types/file_remote_upload_response.py +17 -0
- frameio/types/file_response.py +13 -0
- frameio/types/file_status.py +5 -0
- frameio/types/file_upload_status.py +41 -0
- frameio/types/file_upload_status_response.py +13 -0
- frameio/types/file_with_includes.py +25 -0
- frameio/types/file_with_includes_response.py +13 -0
- frameio/types/file_with_includes_status.py +5 -0
- frameio/types/file_with_media_links_include.py +19 -0
- frameio/types/file_with_upload_urls.py +16 -0
- frameio/types/file_with_upload_urls_response.py +13 -0
- frameio/types/folder.py +15 -0
- frameio/types/folder_copy_response.py +13 -0
- frameio/types/folder_response.py +13 -0
- frameio/types/folder_with_includes.py +63 -0
- frameio/types/folder_with_includes_response.py +13 -0
- frameio/types/folders_with_includes_response.py +23 -0
- frameio/types/forbidden.py +13 -0
- frameio/types/forbidden_errors_item.py +15 -0
- frameio/types/forbidden_errors_item_source.py +12 -0
- frameio/types/include.py +5 -0
- frameio/types/include_total_count.py +3 -0
- frameio/types/integer_value.py +12 -0
- frameio/types/json_error_response.py +13 -0
- frameio/types/json_error_response_errors_item.py +15 -0
- frameio/types/json_error_response_errors_item_source.py +12 -0
- frameio/types/links.py +22 -0
- frameio/types/long_text_definition.py +49 -0
- frameio/types/long_text_definition_field_configuration.py +10 -0
- frameio/types/long_text_definition_field_type.py +10 -0
- frameio/types/long_text_definition_params.py +15 -0
- frameio/types/long_text_definition_with_includes.py +13 -0
- frameio/types/media_link_common.py +17 -0
- frameio/types/media_links_collection.py +24 -0
- frameio/types/metadata_field.py +149 -0
- frameio/types/metadata_response.py +13 -0
- frameio/types/metadata_with_definition.py +21 -0
- frameio/types/multi_select_value.py +14 -0
- frameio/types/multi_user_value.py +20 -0
- frameio/types/multi_user_value_member_options_type.py +5 -0
- frameio/types/no_content.py +3 -0
- frameio/types/not_found.py +13 -0
- frameio/types/not_found_errors_item.py +15 -0
- frameio/types/not_found_errors_item_source.py +12 -0
- frameio/types/number_definition.py +43 -0
- frameio/types/number_definition_field_configuration.py +21 -0
- frameio/types/number_definition_field_configuration_number_format.py +7 -0
- frameio/types/number_definition_params.py +17 -0
- frameio/types/number_definition_params_field_configuration.py +23 -0
- frameio/types/number_definition_params_field_configuration_number_format.py +7 -0
- frameio/types/number_definition_with_includes.py +13 -0
- frameio/types/number_value.py +12 -0
- frameio/types/original_media_link.py +16 -0
- frameio/types/profile.py +34 -0
- frameio/types/profile_response.py +17 -0
- frameio/types/project.py +66 -0
- frameio/types/project_include.py +5 -0
- frameio/types/project_response.py +17 -0
- frameio/types/project_status.py +5 -0
- frameio/types/project_with_includes.py +18 -0
- frameio/types/project_with_includes_response.py +17 -0
- frameio/types/projects_with_includes_response.py +27 -0
- frameio/types/rating_definition.py +43 -0
- frameio/types/rating_definition_field_configuration.py +22 -0
- frameio/types/rating_definition_field_configuration_style.py +5 -0
- frameio/types/rating_definition_params.py +17 -0
- frameio/types/rating_definition_params_field_configuration.py +22 -0
- frameio/types/rating_definition_params_field_configuration_style.py +5 -0
- frameio/types/rating_definition_with_includes.py +13 -0
- frameio/types/remove_asset_response.py +13 -0
- frameio/types/remove_asset_response_data.py +15 -0
- frameio/types/rendition_media_link.py +19 -0
- frameio/types/request_after_opaque_cursor.py +5 -0
- frameio/types/request_page_size.py +3 -0
- frameio/types/select_definition.py +43 -0
- frameio/types/select_definition_field_configuration.py +18 -0
- frameio/types/select_definition_field_configuration_options_item.py +22 -0
- frameio/types/select_definition_params.py +17 -0
- frameio/types/select_definition_params_field_configuration.py +20 -0
- frameio/types/select_definition_params_field_configuration_options_item.py +20 -0
- frameio/types/select_definition_with_includes.py +13 -0
- frameio/types/select_multi_definition.py +49 -0
- frameio/types/select_multi_definition_field_configuration.py +18 -0
- frameio/types/select_multi_definition_field_configuration_options_item.py +22 -0
- frameio/types/select_multi_definition_field_type.py +10 -0
- frameio/types/select_multi_definition_params.py +17 -0
- frameio/types/select_multi_definition_params_field_configuration.py +20 -0
- frameio/types/select_multi_definition_params_field_configuration_options_item.py +20 -0
- frameio/types/select_multi_definition_with_includes.py +13 -0
- frameio/types/select_option.py +20 -0
- frameio/types/select_value.py +14 -0
- frameio/types/share.py +66 -0
- frameio/types/share_access.py +5 -0
- frameio/types/share_response.py +13 -0
- frameio/types/share_reviewers_response.py +27 -0
- frameio/types/shares_response.py +27 -0
- frameio/types/single_user_value.py +20 -0
- frameio/types/single_user_value_member_options_type.py +5 -0
- frameio/types/text_definition.py +49 -0
- frameio/types/text_definition_field_configuration.py +10 -0
- frameio/types/text_definition_field_type.py +10 -0
- frameio/types/text_definition_params.py +15 -0
- frameio/types/text_definition_with_includes.py +13 -0
- frameio/types/text_value.py +12 -0
- frameio/types/time_stamp.py +5 -0
- frameio/types/toggle_definition.py +43 -0
- frameio/types/toggle_definition_field_configuration.py +15 -0
- frameio/types/toggle_definition_params.py +17 -0
- frameio/types/toggle_definition_params_field_configuration.py +15 -0
- frameio/types/toggle_definition_with_includes.py +13 -0
- frameio/types/too_many_requests.py +13 -0
- frameio/types/too_many_requests_errors_item.py +15 -0
- frameio/types/too_many_requests_errors_item_source.py +12 -0
- frameio/types/unauthorized.py +13 -0
- frameio/types/unauthorized_errors_item.py +15 -0
- frameio/types/unauthorized_errors_item_source.py +12 -0
- frameio/types/unprocessable_entity.py +13 -0
- frameio/types/unprocessable_entity_errors_item.py +15 -0
- frameio/types/unprocessable_entity_errors_item_source.py +12 -0
- frameio/types/update_date_definition_params.py +17 -0
- frameio/types/update_date_definition_params_field_configuration.py +25 -0
- frameio/types/update_date_definition_params_field_configuration_display_format.py +7 -0
- frameio/types/update_date_definition_params_field_configuration_time_format.py +7 -0
- frameio/types/update_long_text_definition_params.py +15 -0
- frameio/types/update_number_definition_params.py +17 -0
- frameio/types/update_number_definition_params_field_configuration.py +25 -0
- frameio/types/update_number_definition_params_field_configuration_number_format.py +7 -0
- frameio/types/update_rating_definition_params.py +17 -0
- frameio/types/update_rating_definition_params_field_configuration.py +24 -0
- frameio/types/update_rating_definition_params_field_configuration_style.py +7 -0
- frameio/types/update_select_definition_params.py +17 -0
- frameio/types/update_select_definition_params_field_configuration.py +20 -0
- frameio/types/update_select_definition_params_field_configuration_options_item.py +20 -0
- frameio/types/update_select_multi_definition_params.py +19 -0
- frameio/types/update_select_multi_definition_params_field_configuration.py +20 -0
- frameio/types/update_select_multi_definition_params_field_configuration_options_item.py +20 -0
- frameio/types/update_text_definition_params.py +15 -0
- frameio/types/update_toggle_definition_params.py +17 -0
- frameio/types/update_toggle_definition_params_field_configuration.py +15 -0
- frameio/types/update_user_multi_definition_params.py +17 -0
- frameio/types/update_user_multi_definition_params_field_configuration.py +25 -0
- frameio/types/update_user_multi_definition_params_field_configuration_custom_members_item.py +20 -0
- frameio/types/update_user_multi_definition_params_field_configuration_custom_members_item_type.py +7 -0
- frameio/types/update_user_multi_definition_params_field_configuration_member_options_type.py +7 -0
- frameio/types/update_user_roles_params.py +17 -0
- frameio/types/update_user_roles_params_data.py +13 -0
- frameio/types/update_user_roles_params_data_role.py +7 -0
- frameio/types/update_user_roles_response.py +13 -0
- frameio/types/update_user_roles_response_data.py +13 -0
- frameio/types/update_user_roles_response_data_role.py +7 -0
- frameio/types/update_user_single_definition_params.py +17 -0
- frameio/types/update_user_single_definition_params_field_configuration.py +25 -0
- frameio/types/update_user_single_definition_params_field_configuration_custom_members_item.py +20 -0
- frameio/types/update_user_single_definition_params_field_configuration_custom_members_item_type.py +7 -0
- frameio/types/update_user_single_definition_params_field_configuration_member_options_type.py +7 -0
- frameio/types/upload_url.py +20 -0
- frameio/types/user.py +44 -0
- frameio/types/user_multi_definition.py +49 -0
- frameio/types/user_multi_definition_field_configuration.py +23 -0
- frameio/types/user_multi_definition_field_configuration_custom_members_item.py +20 -0
- frameio/types/user_multi_definition_field_configuration_custom_members_item_type.py +7 -0
- frameio/types/user_multi_definition_field_configuration_member_options_type.py +7 -0
- frameio/types/user_multi_definition_field_type.py +10 -0
- frameio/types/user_multi_definition_params.py +17 -0
- frameio/types/user_multi_definition_params_field_configuration.py +23 -0
- frameio/types/user_multi_definition_params_field_configuration_custom_members_item.py +20 -0
- frameio/types/user_multi_definition_params_field_configuration_custom_members_item_type.py +7 -0
- frameio/types/user_multi_definition_params_field_configuration_member_options_type.py +7 -0
- frameio/types/user_multi_definition_with_includes.py +13 -0
- frameio/types/user_role.py +19 -0
- frameio/types/user_role_role.py +7 -0
- frameio/types/user_roles_response.py +27 -0
- frameio/types/user_single_definition.py +49 -0
- frameio/types/user_single_definition_field_configuration.py +23 -0
- frameio/types/user_single_definition_field_configuration_custom_members_item.py +20 -0
- frameio/types/user_single_definition_field_configuration_custom_members_item_type.py +7 -0
- frameio/types/user_single_definition_field_configuration_member_options_type.py +7 -0
- frameio/types/user_single_definition_field_type.py +10 -0
- frameio/types/user_single_definition_params.py +17 -0
- frameio/types/user_single_definition_params_field_configuration.py +23 -0
- frameio/types/user_single_definition_params_field_configuration_custom_members_item.py +20 -0
- frameio/types/user_single_definition_params_field_configuration_custom_members_item_type.py +7 -0
- frameio/types/user_single_definition_params_field_configuration_member_options_type.py +7 -0
- frameio/types/user_single_definition_with_includes.py +13 -0
- frameio/types/user_value.py +18 -0
- frameio/types/user_value_type.py +5 -0
- frameio/types/uuid_.py +3 -0
- frameio/types/version_stack.py +50 -0
- frameio/types/version_stack_copy_response.py +13 -0
- frameio/types/version_stack_response.py +13 -0
- frameio/types/version_stack_with_includes.py +60 -0
- frameio/types/version_stack_with_includes_response.py +13 -0
- frameio/types/version_stacks_with_includes_response.py +19 -0
- frameio/types/webhook.py +36 -0
- frameio/types/webhook_create_response.py +17 -0
- frameio/types/webhook_create_response_data.py +15 -0
- frameio/types/webhook_events.py +5 -0
- frameio/types/webhook_response.py +13 -0
- frameio/types/webhook_with_includes.py +13 -0
- frameio/types/webhook_with_includes_response.py +13 -0
- frameio/types/webhooks_with_includes_response.py +23 -0
- frameio/types/workspace.py +40 -0
- frameio/types/workspace_include.py +5 -0
- frameio/types/workspace_params.py +17 -0
- frameio/types/workspace_params_data.py +15 -0
- frameio/types/workspace_response.py +17 -0
- frameio/types/workspace_with_includes.py +13 -0
- frameio/types/workspace_with_includes_response.py +17 -0
- frameio/types/workspaces_with_includes_response.py +27 -0
- frameio/users/__init__.py +4 -0
- frameio/users/client.py +100 -0
- frameio/users/raw_client.py +234 -0
- frameio/version.py +3 -0
- frameio/version_stacks/__init__.py +49 -0
- frameio/version_stacks/client.py +818 -0
- frameio/version_stacks/raw_client.py +1614 -0
- frameio/version_stacks/types/__init__.py +47 -0
- frameio/version_stacks/types/version_stack_copy_params_data.py +15 -0
- frameio/version_stacks/types/version_stack_create_params_data.py +19 -0
- frameio/version_stacks/types/version_stack_move_params_data.py +15 -0
- frameio/version_stacks/types/version_stacks_show_request_include.py +19 -0
- frameio/webhooks/__init__.py +34 -0
- frameio/webhooks/client.py +793 -0
- frameio/webhooks/raw_client.py +1347 -0
- frameio/webhooks/types/__init__.py +38 -0
- frameio/webhooks/types/webhook_create_params_data.py +18 -0
- frameio/webhooks/types/webhook_update_params_data.py +23 -0
- frameio/workspace_permissions/__init__.py +4 -0
- frameio/workspace_permissions/client.py +430 -0
- frameio/workspace_permissions/raw_client.py +824 -0
- frameio/workspaces/__init__.py +4 -0
- frameio/workspaces/client.py +563 -0
- frameio/workspaces/raw_client.py +1259 -0
- frameio-0.0.28.dist-info/METADATA +259 -0
- frameio-0.0.28.dist-info/RECORD +385 -0
- frameio-0.0.28.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
# This file was auto-generated by Fern from our API Definition.
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import email.utils
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
import typing
|
|
8
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
9
|
+
from random import random
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
from .file import File, convert_file_dict_to_httpx_tuples
|
|
13
|
+
from .force_multipart import FORCE_MULTIPART
|
|
14
|
+
from .jsonable_encoder import jsonable_encoder
|
|
15
|
+
from .query_encoder import encode_query
|
|
16
|
+
from .remove_none_from_dict import remove_none_from_dict as remove_none_from_dict
|
|
17
|
+
from .request_options import RequestOptions
|
|
18
|
+
from httpx._types import RequestFiles
|
|
19
|
+
|
|
20
|
+
INITIAL_RETRY_DELAY_SECONDS = 1.0
|
|
21
|
+
MAX_RETRY_DELAY_SECONDS = 60.0
|
|
22
|
+
JITTER_FACTOR = 0.2 # 20% random jitter
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]:
|
|
26
|
+
"""
|
|
27
|
+
This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait.
|
|
28
|
+
|
|
29
|
+
Inspired by the urllib3 retry implementation.
|
|
30
|
+
"""
|
|
31
|
+
retry_after_ms = response_headers.get("retry-after-ms")
|
|
32
|
+
if retry_after_ms is not None:
|
|
33
|
+
try:
|
|
34
|
+
return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
retry_after = response_headers.get("retry-after")
|
|
39
|
+
if retry_after is None:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
# Attempt to parse the header as an int.
|
|
43
|
+
if re.match(r"^\s*[0-9]+\s*$", retry_after):
|
|
44
|
+
seconds = float(retry_after)
|
|
45
|
+
# Fallback to parsing it as a date.
|
|
46
|
+
else:
|
|
47
|
+
retry_date_tuple = email.utils.parsedate_tz(retry_after)
|
|
48
|
+
if retry_date_tuple is None:
|
|
49
|
+
return None
|
|
50
|
+
if retry_date_tuple[9] is None: # Python 2
|
|
51
|
+
# Assume UTC if no timezone was specified
|
|
52
|
+
# On Python2.7, parsedate_tz returns None for a timezone offset
|
|
53
|
+
# instead of 0 if no timezone is given, where mktime_tz treats
|
|
54
|
+
# a None timezone offset as local time.
|
|
55
|
+
retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:]
|
|
56
|
+
|
|
57
|
+
retry_date = email.utils.mktime_tz(retry_date_tuple)
|
|
58
|
+
seconds = retry_date - time.time()
|
|
59
|
+
|
|
60
|
+
if seconds < 0:
|
|
61
|
+
seconds = 0
|
|
62
|
+
|
|
63
|
+
return seconds
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _add_positive_jitter(delay: float) -> float:
|
|
67
|
+
"""Add positive jitter (0-20%) to prevent thundering herd."""
|
|
68
|
+
jitter_multiplier = 1 + random() * JITTER_FACTOR
|
|
69
|
+
return delay * jitter_multiplier
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _add_symmetric_jitter(delay: float) -> float:
|
|
73
|
+
"""Add symmetric jitter (±10%) for exponential backoff."""
|
|
74
|
+
jitter_multiplier = 1 + (random() - 0.5) * JITTER_FACTOR
|
|
75
|
+
return delay * jitter_multiplier
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _parse_x_ratelimit_reset(response_headers: httpx.Headers) -> typing.Optional[float]:
|
|
79
|
+
"""
|
|
80
|
+
Parse the X-RateLimit-Reset header (Unix timestamp in seconds).
|
|
81
|
+
Returns seconds to wait, or None if header is missing/invalid.
|
|
82
|
+
"""
|
|
83
|
+
reset_time_str = response_headers.get("x-ratelimit-reset")
|
|
84
|
+
if reset_time_str is None:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
reset_time = int(reset_time_str)
|
|
89
|
+
delay = reset_time - time.time()
|
|
90
|
+
if delay > 0:
|
|
91
|
+
return delay
|
|
92
|
+
except (ValueError, TypeError):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _retry_timeout(response: httpx.Response, retries: int) -> float:
|
|
99
|
+
"""
|
|
100
|
+
Determine the amount of time to wait before retrying a request.
|
|
101
|
+
This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff
|
|
102
|
+
with a jitter to determine the number of seconds to wait.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
# 1. Check Retry-After header first
|
|
106
|
+
retry_after = _parse_retry_after(response.headers)
|
|
107
|
+
if retry_after is not None and retry_after > 0:
|
|
108
|
+
return min(retry_after, MAX_RETRY_DELAY_SECONDS)
|
|
109
|
+
|
|
110
|
+
# 2. Check X-RateLimit-Reset header (with positive jitter)
|
|
111
|
+
ratelimit_reset = _parse_x_ratelimit_reset(response.headers)
|
|
112
|
+
if ratelimit_reset is not None:
|
|
113
|
+
return _add_positive_jitter(min(ratelimit_reset, MAX_RETRY_DELAY_SECONDS))
|
|
114
|
+
|
|
115
|
+
# 3. Fall back to exponential backoff (with symmetric jitter)
|
|
116
|
+
backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS)
|
|
117
|
+
return _add_symmetric_jitter(backoff)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _should_retry(response: httpx.Response) -> bool:
|
|
121
|
+
retryable_400s = [429, 408, 409]
|
|
122
|
+
return response.status_code >= 500 or response.status_code in retryable_400s
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _build_url(base_url: str, path: typing.Optional[str]) -> str:
|
|
126
|
+
"""
|
|
127
|
+
Build a full URL by joining a base URL with a path.
|
|
128
|
+
|
|
129
|
+
This function correctly handles base URLs that contain path prefixes (e.g., tenant-based URLs)
|
|
130
|
+
by using string concatenation instead of urllib.parse.urljoin(), which would incorrectly
|
|
131
|
+
strip path components when the path starts with '/'.
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
>>> _build_url("https://cloud.example.com/org/tenant/api", "/users")
|
|
135
|
+
'https://cloud.example.com/org/tenant/api/users'
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
base_url: The base URL, which may contain path prefixes.
|
|
139
|
+
path: The path to append. Can be None or empty string.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
The full URL with base_url and path properly joined.
|
|
143
|
+
"""
|
|
144
|
+
if not path:
|
|
145
|
+
return base_url
|
|
146
|
+
return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _maybe_filter_none_from_multipart_data(
|
|
150
|
+
data: typing.Optional[typing.Any],
|
|
151
|
+
request_files: typing.Optional[RequestFiles],
|
|
152
|
+
force_multipart: typing.Optional[bool],
|
|
153
|
+
) -> typing.Optional[typing.Any]:
|
|
154
|
+
"""
|
|
155
|
+
Filter None values from data body for multipart/form requests.
|
|
156
|
+
This prevents httpx from converting None to empty strings in multipart encoding.
|
|
157
|
+
Only applies when files are present or force_multipart is True.
|
|
158
|
+
"""
|
|
159
|
+
if data is not None and isinstance(data, typing.Mapping) and (request_files or force_multipart):
|
|
160
|
+
return remove_none_from_dict(data)
|
|
161
|
+
return data
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def remove_omit_from_dict(
|
|
165
|
+
original: typing.Dict[str, typing.Optional[typing.Any]],
|
|
166
|
+
omit: typing.Optional[typing.Any],
|
|
167
|
+
) -> typing.Dict[str, typing.Any]:
|
|
168
|
+
if omit is None:
|
|
169
|
+
return original
|
|
170
|
+
new: typing.Dict[str, typing.Any] = {}
|
|
171
|
+
for key, value in original.items():
|
|
172
|
+
if value is not omit:
|
|
173
|
+
new[key] = value
|
|
174
|
+
return new
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def maybe_filter_request_body(
|
|
178
|
+
data: typing.Optional[typing.Any],
|
|
179
|
+
request_options: typing.Optional[RequestOptions],
|
|
180
|
+
omit: typing.Optional[typing.Any],
|
|
181
|
+
) -> typing.Optional[typing.Any]:
|
|
182
|
+
if data is None:
|
|
183
|
+
return (
|
|
184
|
+
jsonable_encoder(request_options.get("additional_body_parameters", {})) or {}
|
|
185
|
+
if request_options is not None
|
|
186
|
+
else None
|
|
187
|
+
)
|
|
188
|
+
elif not isinstance(data, typing.Mapping):
|
|
189
|
+
data_content = jsonable_encoder(data)
|
|
190
|
+
else:
|
|
191
|
+
data_content = {
|
|
192
|
+
**(jsonable_encoder(remove_omit_from_dict(data, omit))), # type: ignore
|
|
193
|
+
**(
|
|
194
|
+
jsonable_encoder(request_options.get("additional_body_parameters", {})) or {}
|
|
195
|
+
if request_options is not None
|
|
196
|
+
else {}
|
|
197
|
+
),
|
|
198
|
+
}
|
|
199
|
+
return data_content
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# Abstracted out for testing purposes
|
|
203
|
+
def get_request_body(
|
|
204
|
+
*,
|
|
205
|
+
json: typing.Optional[typing.Any],
|
|
206
|
+
data: typing.Optional[typing.Any],
|
|
207
|
+
request_options: typing.Optional[RequestOptions],
|
|
208
|
+
omit: typing.Optional[typing.Any],
|
|
209
|
+
) -> typing.Tuple[typing.Optional[typing.Any], typing.Optional[typing.Any]]:
|
|
210
|
+
json_body = None
|
|
211
|
+
data_body = None
|
|
212
|
+
if data is not None:
|
|
213
|
+
data_body = maybe_filter_request_body(data, request_options, omit)
|
|
214
|
+
else:
|
|
215
|
+
# If both data and json are None, we send json data in the event extra properties are specified
|
|
216
|
+
json_body = maybe_filter_request_body(json, request_options, omit)
|
|
217
|
+
|
|
218
|
+
has_additional_body_parameters = bool(
|
|
219
|
+
request_options is not None and request_options.get("additional_body_parameters")
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Only collapse empty dict to None when the body was not explicitly provided
|
|
223
|
+
# and there are no additional body parameters. This preserves explicit empty
|
|
224
|
+
# bodies (e.g., when an endpoint has a request body type but all fields are optional).
|
|
225
|
+
if json_body == {} and json is None and not has_additional_body_parameters:
|
|
226
|
+
json_body = None
|
|
227
|
+
if data_body == {} and data is None and not has_additional_body_parameters:
|
|
228
|
+
data_body = None
|
|
229
|
+
|
|
230
|
+
return json_body, data_body
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class HttpClient:
|
|
234
|
+
def __init__(
|
|
235
|
+
self,
|
|
236
|
+
*,
|
|
237
|
+
httpx_client: httpx.Client,
|
|
238
|
+
base_timeout: typing.Callable[[], typing.Optional[float]],
|
|
239
|
+
base_headers: typing.Callable[[], typing.Dict[str, str]],
|
|
240
|
+
base_url: typing.Optional[typing.Callable[[], str]] = None,
|
|
241
|
+
):
|
|
242
|
+
self.base_url = base_url
|
|
243
|
+
self.base_timeout = base_timeout
|
|
244
|
+
self.base_headers = base_headers
|
|
245
|
+
self.httpx_client = httpx_client
|
|
246
|
+
|
|
247
|
+
def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str:
|
|
248
|
+
base_url = maybe_base_url
|
|
249
|
+
if self.base_url is not None and base_url is None:
|
|
250
|
+
base_url = self.base_url()
|
|
251
|
+
|
|
252
|
+
if base_url is None:
|
|
253
|
+
raise ValueError("A base_url is required to make this request, please provide one and try again.")
|
|
254
|
+
return base_url
|
|
255
|
+
|
|
256
|
+
def request(
|
|
257
|
+
self,
|
|
258
|
+
path: typing.Optional[str] = None,
|
|
259
|
+
*,
|
|
260
|
+
method: str,
|
|
261
|
+
base_url: typing.Optional[str] = None,
|
|
262
|
+
params: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
263
|
+
json: typing.Optional[typing.Any] = None,
|
|
264
|
+
data: typing.Optional[typing.Any] = None,
|
|
265
|
+
content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None,
|
|
266
|
+
files: typing.Optional[
|
|
267
|
+
typing.Union[
|
|
268
|
+
typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]],
|
|
269
|
+
typing.List[typing.Tuple[str, File]],
|
|
270
|
+
]
|
|
271
|
+
] = None,
|
|
272
|
+
headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
273
|
+
request_options: typing.Optional[RequestOptions] = None,
|
|
274
|
+
retries: int = 0,
|
|
275
|
+
omit: typing.Optional[typing.Any] = None,
|
|
276
|
+
force_multipart: typing.Optional[bool] = None,
|
|
277
|
+
) -> httpx.Response:
|
|
278
|
+
base_url = self.get_base_url(base_url)
|
|
279
|
+
timeout = (
|
|
280
|
+
request_options.get("timeout_in_seconds")
|
|
281
|
+
if request_options is not None and request_options.get("timeout_in_seconds") is not None
|
|
282
|
+
else self.base_timeout()
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
|
|
286
|
+
|
|
287
|
+
request_files: typing.Optional[RequestFiles] = (
|
|
288
|
+
convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
|
|
289
|
+
if (files is not None and files is not omit and isinstance(files, dict))
|
|
290
|
+
else None
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if (request_files is None or len(request_files) == 0) and force_multipart:
|
|
294
|
+
request_files = FORCE_MULTIPART
|
|
295
|
+
|
|
296
|
+
data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
|
|
297
|
+
|
|
298
|
+
# Compute encoded params separately to avoid passing empty list to httpx
|
|
299
|
+
# (httpx strips existing query params from URL when params=[] is passed)
|
|
300
|
+
_encoded_params = encode_query(
|
|
301
|
+
jsonable_encoder(
|
|
302
|
+
remove_none_from_dict(
|
|
303
|
+
remove_omit_from_dict(
|
|
304
|
+
{
|
|
305
|
+
**(params if params is not None else {}),
|
|
306
|
+
**(
|
|
307
|
+
request_options.get("additional_query_parameters", {}) or {}
|
|
308
|
+
if request_options is not None
|
|
309
|
+
else {}
|
|
310
|
+
),
|
|
311
|
+
},
|
|
312
|
+
omit,
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
response = self.httpx_client.request(
|
|
319
|
+
method=method,
|
|
320
|
+
url=_build_url(base_url, path),
|
|
321
|
+
headers=jsonable_encoder(
|
|
322
|
+
remove_none_from_dict(
|
|
323
|
+
{
|
|
324
|
+
**self.base_headers(),
|
|
325
|
+
**(headers if headers is not None else {}),
|
|
326
|
+
**(request_options.get("additional_headers", {}) or {} if request_options is not None else {}),
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
),
|
|
330
|
+
params=_encoded_params if _encoded_params else None,
|
|
331
|
+
json=json_body,
|
|
332
|
+
data=data_body,
|
|
333
|
+
content=content,
|
|
334
|
+
files=request_files,
|
|
335
|
+
timeout=timeout,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
max_retries: int = request_options.get("max_retries", 2) if request_options is not None else 2
|
|
339
|
+
if _should_retry(response=response):
|
|
340
|
+
if retries < max_retries:
|
|
341
|
+
time.sleep(_retry_timeout(response=response, retries=retries))
|
|
342
|
+
return self.request(
|
|
343
|
+
path=path,
|
|
344
|
+
method=method,
|
|
345
|
+
base_url=base_url,
|
|
346
|
+
params=params,
|
|
347
|
+
json=json,
|
|
348
|
+
content=content,
|
|
349
|
+
files=files,
|
|
350
|
+
headers=headers,
|
|
351
|
+
request_options=request_options,
|
|
352
|
+
retries=retries + 1,
|
|
353
|
+
omit=omit,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
return response
|
|
357
|
+
|
|
358
|
+
@contextmanager
|
|
359
|
+
def stream(
|
|
360
|
+
self,
|
|
361
|
+
path: typing.Optional[str] = None,
|
|
362
|
+
*,
|
|
363
|
+
method: str,
|
|
364
|
+
base_url: typing.Optional[str] = None,
|
|
365
|
+
params: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
366
|
+
json: typing.Optional[typing.Any] = None,
|
|
367
|
+
data: typing.Optional[typing.Any] = None,
|
|
368
|
+
content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None,
|
|
369
|
+
files: typing.Optional[
|
|
370
|
+
typing.Union[
|
|
371
|
+
typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]],
|
|
372
|
+
typing.List[typing.Tuple[str, File]],
|
|
373
|
+
]
|
|
374
|
+
] = None,
|
|
375
|
+
headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
376
|
+
request_options: typing.Optional[RequestOptions] = None,
|
|
377
|
+
retries: int = 0,
|
|
378
|
+
omit: typing.Optional[typing.Any] = None,
|
|
379
|
+
force_multipart: typing.Optional[bool] = None,
|
|
380
|
+
) -> typing.Iterator[httpx.Response]:
|
|
381
|
+
base_url = self.get_base_url(base_url)
|
|
382
|
+
timeout = (
|
|
383
|
+
request_options.get("timeout_in_seconds")
|
|
384
|
+
if request_options is not None and request_options.get("timeout_in_seconds") is not None
|
|
385
|
+
else self.base_timeout()
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
request_files: typing.Optional[RequestFiles] = (
|
|
389
|
+
convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
|
|
390
|
+
if (files is not None and files is not omit and isinstance(files, dict))
|
|
391
|
+
else None
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
if (request_files is None or len(request_files) == 0) and force_multipart:
|
|
395
|
+
request_files = FORCE_MULTIPART
|
|
396
|
+
|
|
397
|
+
json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
|
|
398
|
+
|
|
399
|
+
data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
|
|
400
|
+
|
|
401
|
+
# Compute encoded params separately to avoid passing empty list to httpx
|
|
402
|
+
# (httpx strips existing query params from URL when params=[] is passed)
|
|
403
|
+
_encoded_params = encode_query(
|
|
404
|
+
jsonable_encoder(
|
|
405
|
+
remove_none_from_dict(
|
|
406
|
+
remove_omit_from_dict(
|
|
407
|
+
{
|
|
408
|
+
**(params if params is not None else {}),
|
|
409
|
+
**(
|
|
410
|
+
request_options.get("additional_query_parameters", {})
|
|
411
|
+
if request_options is not None
|
|
412
|
+
else {}
|
|
413
|
+
),
|
|
414
|
+
},
|
|
415
|
+
omit,
|
|
416
|
+
)
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
with self.httpx_client.stream(
|
|
422
|
+
method=method,
|
|
423
|
+
url=_build_url(base_url, path),
|
|
424
|
+
headers=jsonable_encoder(
|
|
425
|
+
remove_none_from_dict(
|
|
426
|
+
{
|
|
427
|
+
**self.base_headers(),
|
|
428
|
+
**(headers if headers is not None else {}),
|
|
429
|
+
**(request_options.get("additional_headers", {}) if request_options is not None else {}),
|
|
430
|
+
}
|
|
431
|
+
)
|
|
432
|
+
),
|
|
433
|
+
params=_encoded_params if _encoded_params else None,
|
|
434
|
+
json=json_body,
|
|
435
|
+
data=data_body,
|
|
436
|
+
content=content,
|
|
437
|
+
files=request_files,
|
|
438
|
+
timeout=timeout,
|
|
439
|
+
) as stream:
|
|
440
|
+
yield stream
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
class AsyncHttpClient:
|
|
444
|
+
def __init__(
|
|
445
|
+
self,
|
|
446
|
+
*,
|
|
447
|
+
httpx_client: httpx.AsyncClient,
|
|
448
|
+
base_timeout: typing.Callable[[], typing.Optional[float]],
|
|
449
|
+
base_headers: typing.Callable[[], typing.Dict[str, str]],
|
|
450
|
+
base_url: typing.Optional[typing.Callable[[], str]] = None,
|
|
451
|
+
async_base_headers: typing.Optional[typing.Callable[[], typing.Awaitable[typing.Dict[str, str]]]] = None,
|
|
452
|
+
):
|
|
453
|
+
self.base_url = base_url
|
|
454
|
+
self.base_timeout = base_timeout
|
|
455
|
+
self.base_headers = base_headers
|
|
456
|
+
self.async_base_headers = async_base_headers
|
|
457
|
+
self.httpx_client = httpx_client
|
|
458
|
+
|
|
459
|
+
async def _get_headers(self) -> typing.Dict[str, str]:
|
|
460
|
+
if self.async_base_headers is not None:
|
|
461
|
+
return await self.async_base_headers()
|
|
462
|
+
return self.base_headers()
|
|
463
|
+
|
|
464
|
+
def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str:
|
|
465
|
+
base_url = maybe_base_url
|
|
466
|
+
if self.base_url is not None and base_url is None:
|
|
467
|
+
base_url = self.base_url()
|
|
468
|
+
|
|
469
|
+
if base_url is None:
|
|
470
|
+
raise ValueError("A base_url is required to make this request, please provide one and try again.")
|
|
471
|
+
return base_url
|
|
472
|
+
|
|
473
|
+
async def request(
|
|
474
|
+
self,
|
|
475
|
+
path: typing.Optional[str] = None,
|
|
476
|
+
*,
|
|
477
|
+
method: str,
|
|
478
|
+
base_url: typing.Optional[str] = None,
|
|
479
|
+
params: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
480
|
+
json: typing.Optional[typing.Any] = None,
|
|
481
|
+
data: typing.Optional[typing.Any] = None,
|
|
482
|
+
content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None,
|
|
483
|
+
files: typing.Optional[
|
|
484
|
+
typing.Union[
|
|
485
|
+
typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]],
|
|
486
|
+
typing.List[typing.Tuple[str, File]],
|
|
487
|
+
]
|
|
488
|
+
] = None,
|
|
489
|
+
headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
490
|
+
request_options: typing.Optional[RequestOptions] = None,
|
|
491
|
+
retries: int = 0,
|
|
492
|
+
omit: typing.Optional[typing.Any] = None,
|
|
493
|
+
force_multipart: typing.Optional[bool] = None,
|
|
494
|
+
) -> httpx.Response:
|
|
495
|
+
base_url = self.get_base_url(base_url)
|
|
496
|
+
timeout = (
|
|
497
|
+
request_options.get("timeout_in_seconds")
|
|
498
|
+
if request_options is not None and request_options.get("timeout_in_seconds") is not None
|
|
499
|
+
else self.base_timeout()
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
request_files: typing.Optional[RequestFiles] = (
|
|
503
|
+
convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
|
|
504
|
+
if (files is not None and files is not omit and isinstance(files, dict))
|
|
505
|
+
else None
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
if (request_files is None or len(request_files) == 0) and force_multipart:
|
|
509
|
+
request_files = FORCE_MULTIPART
|
|
510
|
+
|
|
511
|
+
json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
|
|
512
|
+
|
|
513
|
+
data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
|
|
514
|
+
|
|
515
|
+
# Get headers (supports async token providers)
|
|
516
|
+
_headers = await self._get_headers()
|
|
517
|
+
|
|
518
|
+
# Compute encoded params separately to avoid passing empty list to httpx
|
|
519
|
+
# (httpx strips existing query params from URL when params=[] is passed)
|
|
520
|
+
_encoded_params = encode_query(
|
|
521
|
+
jsonable_encoder(
|
|
522
|
+
remove_none_from_dict(
|
|
523
|
+
remove_omit_from_dict(
|
|
524
|
+
{
|
|
525
|
+
**(params if params is not None else {}),
|
|
526
|
+
**(
|
|
527
|
+
request_options.get("additional_query_parameters", {}) or {}
|
|
528
|
+
if request_options is not None
|
|
529
|
+
else {}
|
|
530
|
+
),
|
|
531
|
+
},
|
|
532
|
+
omit,
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
)
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
# Add the input to each of these and do None-safety checks
|
|
539
|
+
response = await self.httpx_client.request(
|
|
540
|
+
method=method,
|
|
541
|
+
url=_build_url(base_url, path),
|
|
542
|
+
headers=jsonable_encoder(
|
|
543
|
+
remove_none_from_dict(
|
|
544
|
+
{
|
|
545
|
+
**_headers,
|
|
546
|
+
**(headers if headers is not None else {}),
|
|
547
|
+
**(request_options.get("additional_headers", {}) or {} if request_options is not None else {}),
|
|
548
|
+
}
|
|
549
|
+
)
|
|
550
|
+
),
|
|
551
|
+
params=_encoded_params if _encoded_params else None,
|
|
552
|
+
json=json_body,
|
|
553
|
+
data=data_body,
|
|
554
|
+
content=content,
|
|
555
|
+
files=request_files,
|
|
556
|
+
timeout=timeout,
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
max_retries: int = request_options.get("max_retries", 2) if request_options is not None else 2
|
|
560
|
+
if _should_retry(response=response):
|
|
561
|
+
if retries < max_retries:
|
|
562
|
+
await asyncio.sleep(_retry_timeout(response=response, retries=retries))
|
|
563
|
+
return await self.request(
|
|
564
|
+
path=path,
|
|
565
|
+
method=method,
|
|
566
|
+
base_url=base_url,
|
|
567
|
+
params=params,
|
|
568
|
+
json=json,
|
|
569
|
+
content=content,
|
|
570
|
+
files=files,
|
|
571
|
+
headers=headers,
|
|
572
|
+
request_options=request_options,
|
|
573
|
+
retries=retries + 1,
|
|
574
|
+
omit=omit,
|
|
575
|
+
)
|
|
576
|
+
return response
|
|
577
|
+
|
|
578
|
+
@asynccontextmanager
|
|
579
|
+
async def stream(
|
|
580
|
+
self,
|
|
581
|
+
path: typing.Optional[str] = None,
|
|
582
|
+
*,
|
|
583
|
+
method: str,
|
|
584
|
+
base_url: typing.Optional[str] = None,
|
|
585
|
+
params: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
586
|
+
json: typing.Optional[typing.Any] = None,
|
|
587
|
+
data: typing.Optional[typing.Any] = None,
|
|
588
|
+
content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None,
|
|
589
|
+
files: typing.Optional[
|
|
590
|
+
typing.Union[
|
|
591
|
+
typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]],
|
|
592
|
+
typing.List[typing.Tuple[str, File]],
|
|
593
|
+
]
|
|
594
|
+
] = None,
|
|
595
|
+
headers: typing.Optional[typing.Dict[str, typing.Any]] = None,
|
|
596
|
+
request_options: typing.Optional[RequestOptions] = None,
|
|
597
|
+
retries: int = 0,
|
|
598
|
+
omit: typing.Optional[typing.Any] = None,
|
|
599
|
+
force_multipart: typing.Optional[bool] = None,
|
|
600
|
+
) -> typing.AsyncIterator[httpx.Response]:
|
|
601
|
+
base_url = self.get_base_url(base_url)
|
|
602
|
+
timeout = (
|
|
603
|
+
request_options.get("timeout_in_seconds")
|
|
604
|
+
if request_options is not None and request_options.get("timeout_in_seconds") is not None
|
|
605
|
+
else self.base_timeout()
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
request_files: typing.Optional[RequestFiles] = (
|
|
609
|
+
convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
|
|
610
|
+
if (files is not None and files is not omit and isinstance(files, dict))
|
|
611
|
+
else None
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
if (request_files is None or len(request_files) == 0) and force_multipart:
|
|
615
|
+
request_files = FORCE_MULTIPART
|
|
616
|
+
|
|
617
|
+
json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
|
|
618
|
+
|
|
619
|
+
data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
|
|
620
|
+
|
|
621
|
+
# Get headers (supports async token providers)
|
|
622
|
+
_headers = await self._get_headers()
|
|
623
|
+
|
|
624
|
+
# Compute encoded params separately to avoid passing empty list to httpx
|
|
625
|
+
# (httpx strips existing query params from URL when params=[] is passed)
|
|
626
|
+
_encoded_params = encode_query(
|
|
627
|
+
jsonable_encoder(
|
|
628
|
+
remove_none_from_dict(
|
|
629
|
+
remove_omit_from_dict(
|
|
630
|
+
{
|
|
631
|
+
**(params if params is not None else {}),
|
|
632
|
+
**(
|
|
633
|
+
request_options.get("additional_query_parameters", {})
|
|
634
|
+
if request_options is not None
|
|
635
|
+
else {}
|
|
636
|
+
),
|
|
637
|
+
},
|
|
638
|
+
omit=omit,
|
|
639
|
+
)
|
|
640
|
+
)
|
|
641
|
+
)
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
async with self.httpx_client.stream(
|
|
645
|
+
method=method,
|
|
646
|
+
url=_build_url(base_url, path),
|
|
647
|
+
headers=jsonable_encoder(
|
|
648
|
+
remove_none_from_dict(
|
|
649
|
+
{
|
|
650
|
+
**_headers,
|
|
651
|
+
**(headers if headers is not None else {}),
|
|
652
|
+
**(request_options.get("additional_headers", {}) if request_options is not None else {}),
|
|
653
|
+
}
|
|
654
|
+
)
|
|
655
|
+
),
|
|
656
|
+
params=_encoded_params if _encoded_params else None,
|
|
657
|
+
json=json_body,
|
|
658
|
+
data=data_body,
|
|
659
|
+
content=content,
|
|
660
|
+
files=request_files,
|
|
661
|
+
timeout=timeout,
|
|
662
|
+
) as stream:
|
|
663
|
+
yield stream
|