UncountablePythonSDK 0.0.24__py3-none-any.whl → 0.0.131__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.
Potentially problematic release.
This version of UncountablePythonSDK might be problematic. Click here for more details.
- docs/conf.py +60 -8
- docs/index.md +107 -4
- docs/integration_examples/create_ingredient.md +43 -0
- docs/integration_examples/create_output.md +56 -0
- docs/integration_examples/index.md +6 -0
- docs/justfile +2 -2
- docs/requirements.txt +7 -5
- examples/async_batch.py +5 -6
- examples/basic_auth.py +7 -0
- examples/create_entity.py +4 -6
- examples/create_ingredient_sdk.py +34 -0
- examples/download_files.py +26 -0
- examples/edit_recipe_inputs.py +50 -0
- examples/integration-server/jobs/materials_auto/concurrent_cron.py +11 -0
- examples/integration-server/jobs/materials_auto/example_cron.py +21 -0
- examples/integration-server/jobs/materials_auto/example_http.py +47 -0
- examples/integration-server/jobs/materials_auto/example_instrument.py +100 -0
- examples/integration-server/jobs/materials_auto/example_parse.py +140 -0
- examples/integration-server/jobs/materials_auto/example_predictions.py +61 -0
- examples/integration-server/jobs/materials_auto/example_runsheet_wh.py +39 -0
- examples/integration-server/jobs/materials_auto/example_wh.py +23 -0
- examples/integration-server/jobs/materials_auto/profile.yaml +104 -0
- examples/integration-server/pyproject.toml +224 -0
- examples/invoke_uploader.py +26 -0
- examples/oauth.py +7 -0
- examples/set_recipe_metadata_file.py +40 -0
- examples/set_recipe_output_file_sdk.py +26 -0
- examples/upload_files.py +2 -3
- pkgs/argument_parser/__init__.py +9 -0
- pkgs/argument_parser/_is_namedtuple.py +3 -0
- pkgs/argument_parser/argument_parser.py +295 -74
- pkgs/argument_parser/case_convert.py +4 -3
- pkgs/filesystem_utils/__init__.py +20 -0
- pkgs/filesystem_utils/_blob_session.py +144 -0
- pkgs/filesystem_utils/_gdrive_session.py +309 -0
- pkgs/filesystem_utils/_local_session.py +69 -0
- pkgs/filesystem_utils/_s3_session.py +118 -0
- pkgs/filesystem_utils/_sftp_session.py +151 -0
- pkgs/filesystem_utils/file_type_utils.py +91 -0
- pkgs/filesystem_utils/filesystem_session.py +39 -0
- pkgs/py.typed +0 -0
- pkgs/serialization/__init__.py +8 -1
- pkgs/serialization/annotation.py +64 -0
- pkgs/serialization/missing_sentry.py +1 -1
- pkgs/serialization/opaque_key.py +1 -1
- pkgs/serialization/serial_alias.py +47 -0
- pkgs/serialization/serial_class.py +69 -54
- pkgs/serialization/serial_generic.py +16 -0
- pkgs/serialization/serial_union.py +84 -0
- pkgs/serialization/yaml.py +57 -0
- pkgs/serialization_util/__init__.py +7 -7
- pkgs/serialization_util/convert_to_snakecase.py +27 -0
- pkgs/serialization_util/dataclasses.py +14 -0
- pkgs/serialization_util/serialization_helpers.py +117 -71
- pkgs/type_spec/actions_registry/__main__.py +0 -4
- pkgs/type_spec/actions_registry/emit_typescript.py +5 -5
- pkgs/type_spec/builder.py +438 -109
- pkgs/type_spec/builder_types.py +9 -0
- pkgs/type_spec/config.py +52 -24
- pkgs/type_spec/cross_output_links.py +99 -0
- pkgs/type_spec/emit_io_ts.py +1 -1
- pkgs/type_spec/emit_open_api.py +160 -41
- pkgs/type_spec/emit_open_api_util.py +13 -7
- pkgs/type_spec/emit_python.py +450 -136
- pkgs/type_spec/emit_typescript.py +117 -250
- pkgs/type_spec/emit_typescript_util.py +293 -4
- pkgs/type_spec/load_types.py +20 -5
- pkgs/type_spec/non_discriminated_union_exceptions.py +14 -0
- pkgs/type_spec/open_api_util.py +29 -4
- pkgs/type_spec/parts/base.py.prepart +13 -10
- pkgs/type_spec/parts/base.ts.prepart +4 -0
- pkgs/type_spec/type_info/__main__.py +3 -1
- pkgs/type_spec/type_info/emit_type_info.py +161 -32
- pkgs/type_spec/ui_entry_actions/__init__.py +4 -0
- pkgs/type_spec/ui_entry_actions/generate_ui_entry_actions.py +308 -0
- pkgs/type_spec/util.py +4 -4
- pkgs/type_spec/value_spec/__main__.py +27 -10
- pkgs/type_spec/value_spec/convert_type.py +21 -1
- pkgs/type_spec/value_spec/emit_python.py +25 -7
- pkgs/type_spec/value_spec/types.py +1 -1
- uncountable/__init__.py +1 -2
- uncountable/core/__init__.py +11 -3
- uncountable/core/async_batch.py +16 -1
- uncountable/core/client.py +247 -52
- uncountable/core/environment.py +41 -0
- uncountable/core/file_upload.py +67 -22
- uncountable/core/types.py +8 -13
- uncountable/integration/cli.py +142 -0
- uncountable/integration/construct_client.py +43 -27
- uncountable/integration/cron.py +12 -11
- uncountable/integration/db/connect.py +12 -2
- uncountable/integration/db/session.py +25 -0
- uncountable/integration/entrypoint.py +4 -34
- uncountable/integration/executors/executors.py +147 -0
- uncountable/integration/executors/generic_upload_executor.py +336 -0
- uncountable/integration/executors/script_executor.py +15 -9
- uncountable/integration/http_server/__init__.py +5 -0
- uncountable/integration/http_server/types.py +69 -0
- uncountable/integration/job.py +246 -19
- uncountable/integration/queue_runner/__init__.py +0 -0
- uncountable/integration/queue_runner/command_server/__init__.py +28 -0
- uncountable/integration/queue_runner/command_server/command_client.py +133 -0
- uncountable/integration/queue_runner/command_server/command_server.py +142 -0
- uncountable/integration/queue_runner/command_server/constants.py +4 -0
- uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server.proto +58 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +57 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +114 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +264 -0
- uncountable/integration/queue_runner/command_server/types.py +75 -0
- uncountable/integration/queue_runner/datastore/__init__.py +3 -0
- uncountable/integration/queue_runner/datastore/datastore_sqlite.py +250 -0
- uncountable/integration/queue_runner/datastore/interface.py +29 -0
- uncountable/integration/queue_runner/datastore/model.py +24 -0
- uncountable/integration/queue_runner/job_scheduler.py +200 -0
- uncountable/integration/queue_runner/queue_runner.py +34 -0
- uncountable/integration/queue_runner/types.py +7 -0
- uncountable/integration/queue_runner/worker.py +116 -0
- uncountable/integration/scan_profiles.py +67 -0
- uncountable/integration/scheduler.py +199 -0
- uncountable/integration/secret_retrieval/__init__.py +3 -0
- uncountable/integration/secret_retrieval/retrieve_secret.py +93 -0
- uncountable/integration/server.py +103 -54
- uncountable/integration/telemetry.py +251 -0
- uncountable/integration/webhook_server/entrypoint.py +97 -0
- uncountable/types/__init__.py +149 -30
- uncountable/types/api/batch/execute_batch.py +16 -9
- uncountable/types/api/batch/execute_batch_load_async.py +13 -7
- uncountable/types/api/chemical/convert_chemical_formats.py +20 -8
- uncountable/types/api/condition_parameters/__init__.py +1 -0
- uncountable/types/api/condition_parameters/upsert_condition_match.py +72 -0
- uncountable/types/api/entity/create_entities.py +24 -12
- uncountable/types/api/entity/create_entity.py +22 -13
- uncountable/types/api/entity/create_or_update_entity.py +48 -0
- uncountable/types/api/entity/export_entities.py +59 -0
- uncountable/types/api/entity/get_entities_data.py +18 -9
- uncountable/types/api/entity/grant_entity_permissions.py +48 -0
- uncountable/types/api/entity/list_aggregate.py +79 -0
- uncountable/types/api/entity/list_entities.py +53 -14
- uncountable/types/api/entity/lock_entity.py +45 -0
- uncountable/types/api/entity/lookup_entity.py +116 -0
- uncountable/types/api/entity/resolve_entity_ids.py +19 -10
- uncountable/types/api/entity/set_entity_field_values.py +44 -0
- uncountable/types/api/entity/set_values.py +15 -8
- uncountable/types/api/entity/transition_entity_phase.py +27 -12
- uncountable/types/api/entity/unlock_entity.py +44 -0
- uncountable/types/api/equipment/__init__.py +1 -0
- uncountable/types/api/equipment/associate_equipment_input.py +43 -0
- uncountable/types/api/field_options/__init__.py +1 -0
- uncountable/types/api/field_options/upsert_field_options.py +55 -0
- uncountable/types/api/files/__init__.py +1 -0
- uncountable/types/api/files/download_file.py +77 -0
- uncountable/types/api/id_source/list_id_source.py +20 -11
- uncountable/types/api/id_source/match_id_source.py +15 -10
- uncountable/types/api/input_groups/get_input_group_names.py +16 -7
- uncountable/types/api/inputs/create_inputs.py +28 -14
- uncountable/types/api/inputs/get_input_data.py +34 -16
- uncountable/types/api/inputs/get_input_names.py +19 -10
- uncountable/types/api/inputs/get_inputs_data.py +29 -11
- uncountable/types/api/inputs/set_input_attribute_values.py +16 -10
- uncountable/types/api/inputs/set_input_category.py +44 -0
- uncountable/types/api/inputs/set_input_subcategories.py +45 -0
- uncountable/types/api/inputs/set_intermediate_type.py +50 -0
- uncountable/types/api/integrations/__init__.py +1 -0
- uncountable/types/api/integrations/publish_realtime_data.py +41 -0
- uncountable/types/api/integrations/push_notification.py +49 -0
- uncountable/types/api/integrations/register_sockets_token.py +41 -0
- uncountable/types/api/listing/__init__.py +1 -0
- uncountable/types/api/listing/fetch_listing.py +58 -0
- uncountable/types/api/material_families/__init__.py +1 -0
- uncountable/types/api/material_families/update_entity_material_families.py +47 -0
- uncountable/types/api/notebooks/__init__.py +1 -0
- uncountable/types/api/notebooks/add_notebook_content.py +119 -0
- uncountable/types/api/outputs/get_output_data.py +32 -17
- uncountable/types/api/outputs/get_output_names.py +18 -9
- uncountable/types/api/outputs/get_output_organization.py +173 -0
- uncountable/types/api/outputs/resolve_output_conditions.py +23 -11
- uncountable/types/api/permissions/set_core_permissions.py +31 -15
- uncountable/types/api/project/get_projects.py +20 -11
- uncountable/types/api/project/get_projects_data.py +23 -14
- uncountable/types/api/recipe_links/create_recipe_link.py +17 -10
- uncountable/types/api/recipe_links/remove_recipe_link.py +45 -0
- uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +19 -10
- uncountable/types/api/recipes/add_recipe_to_project.py +42 -0
- uncountable/types/api/recipes/add_time_series_data.py +64 -0
- uncountable/types/api/recipes/archive_recipes.py +14 -7
- uncountable/types/api/recipes/associate_recipe_as_input.py +16 -8
- uncountable/types/api/recipes/associate_recipe_as_lot.py +14 -7
- uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
- uncountable/types/api/recipes/create_mix_order.py +44 -0
- uncountable/types/api/recipes/create_recipe.py +21 -14
- uncountable/types/api/recipes/create_recipes.py +25 -13
- uncountable/types/api/recipes/disassociate_recipe_as_input.py +14 -7
- uncountable/types/api/recipes/edit_recipe_inputs.py +208 -19
- uncountable/types/api/recipes/get_column_calculation_values.py +57 -0
- uncountable/types/api/recipes/get_curve.py +15 -9
- uncountable/types/api/recipes/get_recipe_calculations.py +17 -11
- uncountable/types/api/recipes/get_recipe_links.py +14 -8
- uncountable/types/api/recipes/get_recipe_names.py +16 -7
- uncountable/types/api/recipes/get_recipe_output_metadata.py +16 -10
- uncountable/types/api/recipes/get_recipes_data.py +96 -45
- uncountable/types/api/recipes/lock_recipes.py +64 -0
- uncountable/types/api/recipes/remove_recipe_from_project.py +42 -0
- uncountable/types/api/recipes/set_recipe_inputs.py +19 -13
- uncountable/types/api/recipes/set_recipe_metadata.py +14 -7
- uncountable/types/api/recipes/set_recipe_output_annotations.py +114 -0
- uncountable/types/api/recipes/set_recipe_output_file.py +55 -0
- uncountable/types/api/recipes/set_recipe_outputs.py +40 -15
- uncountable/types/api/recipes/set_recipe_tags.py +30 -13
- uncountable/types/api/recipes/set_recipe_total.py +59 -0
- uncountable/types/api/recipes/unarchive_recipes.py +41 -0
- uncountable/types/api/recipes/unlock_recipes.py +51 -0
- uncountable/types/api/runsheet/__init__.py +1 -0
- uncountable/types/api/runsheet/complete_async_upload.py +41 -0
- uncountable/types/api/triggers/run_trigger.py +15 -8
- uncountable/types/api/uploader/__init__.py +1 -0
- uncountable/types/api/uploader/complete_async_parse.py +46 -0
- uncountable/types/api/uploader/invoke_uploader.py +46 -0
- uncountable/types/api/user/__init__.py +1 -0
- uncountable/types/api/user/get_current_user_info.py +40 -0
- uncountable/types/async_batch.py +8 -52
- uncountable/types/async_batch_processor.py +694 -18
- uncountable/types/async_batch_t.py +108 -0
- uncountable/types/async_jobs.py +8 -0
- uncountable/types/async_jobs_t.py +52 -0
- uncountable/types/auth_retrieval.py +11 -0
- uncountable/types/auth_retrieval_t.py +75 -0
- uncountable/types/base.py +5 -80
- uncountable/types/base_t.py +87 -0
- uncountable/types/calculations.py +3 -19
- uncountable/types/calculations_t.py +26 -0
- uncountable/types/chemical_structure.py +3 -23
- uncountable/types/chemical_structure_t.py +28 -0
- uncountable/types/client_base.py +1170 -88
- uncountable/types/client_config.py +8 -0
- uncountable/types/client_config_t.py +36 -0
- uncountable/types/curves.py +5 -43
- uncountable/types/curves_t.py +50 -0
- uncountable/types/data.py +12 -0
- uncountable/types/data_t.py +103 -0
- uncountable/types/entity.py +8 -270
- uncountable/types/entity_t.py +446 -0
- uncountable/types/experiment_groups.py +3 -19
- uncountable/types/experiment_groups_t.py +26 -0
- uncountable/types/exports.py +8 -0
- uncountable/types/exports_t.py +34 -0
- uncountable/types/field_values.py +25 -61
- uncountable/types/field_values_t.py +302 -0
- uncountable/types/fields.py +3 -20
- uncountable/types/fields_t.py +27 -0
- uncountable/types/generic_upload.py +14 -0
- uncountable/types/generic_upload_t.py +119 -0
- uncountable/types/id_source.py +7 -45
- uncountable/types/id_source_t.py +68 -0
- uncountable/types/identifier.py +6 -50
- uncountable/types/identifier_t.py +62 -0
- uncountable/types/input_attributes.py +3 -25
- uncountable/types/input_attributes_t.py +29 -0
- uncountable/types/inputs.py +6 -57
- uncountable/types/inputs_t.py +82 -0
- uncountable/types/integration_server.py +8 -0
- uncountable/types/integration_server_t.py +46 -0
- uncountable/types/integration_session.py +10 -0
- uncountable/types/integration_session_t.py +60 -0
- uncountable/types/integrations.py +10 -0
- uncountable/types/integrations_t.py +62 -0
- uncountable/types/job_definition.py +28 -0
- uncountable/types/job_definition_t.py +285 -0
- uncountable/types/listing.py +9 -0
- uncountable/types/listing_t.py +51 -0
- uncountable/types/notices.py +8 -0
- uncountable/types/notices_t.py +37 -0
- uncountable/types/notifications.py +11 -0
- uncountable/types/notifications_t.py +74 -0
- uncountable/types/outputs.py +3 -22
- uncountable/types/outputs_t.py +29 -0
- uncountable/types/overrides.py +9 -0
- uncountable/types/overrides_t.py +49 -0
- uncountable/types/permissions.py +3 -42
- uncountable/types/permissions_t.py +45 -0
- uncountable/types/phases.py +3 -19
- uncountable/types/phases_t.py +26 -0
- uncountable/types/post_base.py +3 -26
- uncountable/types/post_base_t.py +29 -0
- uncountable/types/queued_job.py +17 -0
- uncountable/types/queued_job_t.py +140 -0
- uncountable/types/recipe_identifiers.py +7 -58
- uncountable/types/recipe_identifiers_t.py +75 -0
- uncountable/types/recipe_inputs.py +4 -26
- uncountable/types/recipe_inputs_t.py +29 -0
- uncountable/types/recipe_links.py +4 -46
- uncountable/types/recipe_links_t.py +53 -0
- uncountable/types/recipe_metadata.py +5 -48
- uncountable/types/recipe_metadata_t.py +57 -0
- uncountable/types/recipe_output_metadata.py +3 -20
- uncountable/types/recipe_output_metadata_t.py +27 -0
- uncountable/types/recipe_tags.py +3 -19
- uncountable/types/recipe_tags_t.py +26 -0
- uncountable/types/recipe_workflow_steps.py +9 -73
- uncountable/types/recipe_workflow_steps_t.py +95 -0
- uncountable/types/recipes.py +7 -0
- uncountable/types/recipes_t.py +25 -0
- uncountable/types/response.py +3 -21
- uncountable/types/response_t.py +26 -0
- uncountable/types/secret_retrieval.py +11 -0
- uncountable/types/secret_retrieval_t.py +75 -0
- uncountable/types/sockets.py +20 -0
- uncountable/types/sockets_t.py +169 -0
- uncountable/types/structured_filters.py +25 -0
- uncountable/types/structured_filters_t.py +248 -0
- uncountable/types/units.py +3 -19
- uncountable/types/units_t.py +26 -0
- uncountable/types/uploader.py +24 -0
- uncountable/types/uploader_t.py +222 -0
- uncountable/types/users.py +3 -20
- uncountable/types/users_t.py +27 -0
- uncountable/types/webhook_job.py +9 -0
- uncountable/types/webhook_job_t.py +48 -0
- uncountable/types/workflows.py +4 -28
- uncountable/types/workflows_t.py +38 -0
- uncountablepythonsdk-0.0.131.dist-info/METADATA +64 -0
- uncountablepythonsdk-0.0.131.dist-info/RECORD +363 -0
- {UncountablePythonSDK-0.0.24.dist-info → uncountablepythonsdk-0.0.131.dist-info}/WHEEL +1 -1
- {UncountablePythonSDK-0.0.24.dist-info → uncountablepythonsdk-0.0.131.dist-info}/top_level.txt +0 -1
- UncountablePythonSDK-0.0.24.dist-info/METADATA +0 -47
- UncountablePythonSDK-0.0.24.dist-info/RECORD +0 -216
- docs/quickstart.md +0 -19
- examples/recipe-import/importer.py +0 -39
- type_spec/external/api/batch/execute_batch.yaml +0 -56
- type_spec/external/api/batch/execute_batch_load_async.yaml +0 -18
- type_spec/external/api/chemical/convert_chemical_formats.yaml +0 -33
- type_spec/external/api/entity/create_entities.yaml +0 -45
- type_spec/external/api/entity/create_entity.yaml +0 -51
- type_spec/external/api/entity/get_entities_data.yaml +0 -29
- type_spec/external/api/entity/list_entities.yaml +0 -52
- type_spec/external/api/entity/resolve_entity_ids.yaml +0 -29
- type_spec/external/api/entity/set_values.yaml +0 -18
- type_spec/external/api/entity/transition_entity_phase.yaml +0 -44
- type_spec/external/api/id_source/list_id_source.yaml +0 -35
- type_spec/external/api/id_source/match_id_source.yaml +0 -32
- type_spec/external/api/input_groups/get_input_group_names.yaml +0 -29
- type_spec/external/api/inputs/create_inputs.yaml +0 -48
- type_spec/external/api/inputs/get_input_data.yaml +0 -95
- type_spec/external/api/inputs/get_input_names.yaml +0 -38
- type_spec/external/api/inputs/get_inputs_data.yaml +0 -82
- type_spec/external/api/inputs/set_input_attribute_values.yaml +0 -33
- type_spec/external/api/outputs/get_output_data.yaml +0 -92
- type_spec/external/api/outputs/get_output_names.yaml +0 -35
- type_spec/external/api/outputs/resolve_output_conditions.yaml +0 -50
- type_spec/external/api/permissions/set_core_permissions.yaml +0 -69
- type_spec/external/api/project/get_projects.yaml +0 -42
- type_spec/external/api/project/get_projects_data.yaml +0 -50
- type_spec/external/api/recipe_links/create_recipe_link.yaml +0 -25
- type_spec/external/api/recipe_metadata/get_recipe_metadata_data.yaml +0 -41
- type_spec/external/api/recipes/archive_recipes.yaml +0 -20
- type_spec/external/api/recipes/associate_recipe_as_input.yaml +0 -19
- type_spec/external/api/recipes/associate_recipe_as_lot.yaml +0 -19
- type_spec/external/api/recipes/create_recipe.yaml +0 -39
- type_spec/external/api/recipes/create_recipes.yaml +0 -47
- type_spec/external/api/recipes/disassociate_recipe_as_input.yaml +0 -16
- type_spec/external/api/recipes/edit_recipe_inputs.yaml +0 -85
- type_spec/external/api/recipes/get_curve.yaml +0 -21
- type_spec/external/api/recipes/get_recipe_calculations.yaml +0 -39
- type_spec/external/api/recipes/get_recipe_links.yaml +0 -26
- type_spec/external/api/recipes/get_recipe_names.yaml +0 -29
- type_spec/external/api/recipes/get_recipe_output_metadata.yaml +0 -36
- type_spec/external/api/recipes/get_recipes_data.yaml +0 -244
- type_spec/external/api/recipes/set_recipe_inputs.yaml +0 -42
- type_spec/external/api/recipes/set_recipe_metadata.yaml +0 -20
- type_spec/external/api/recipes/set_recipe_outputs.yaml +0 -52
- type_spec/external/api/recipes/set_recipe_tags.yaml +0 -62
- type_spec/external/api/triggers/run_trigger.yaml +0 -18
- uncountable/integration/types.py +0 -89
uncountable/core/client.py
CHANGED
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
import base64
|
|
2
|
-
import
|
|
2
|
+
import datetime
|
|
3
|
+
import re
|
|
3
4
|
import typing
|
|
4
5
|
from dataclasses import dataclass
|
|
6
|
+
from datetime import UTC, timedelta
|
|
5
7
|
from enum import StrEnum
|
|
6
|
-
from
|
|
8
|
+
from io import BytesIO
|
|
9
|
+
from urllib.parse import unquote, urljoin
|
|
10
|
+
from uuid import uuid4
|
|
7
11
|
|
|
8
12
|
import requests
|
|
13
|
+
import simplejson as json
|
|
14
|
+
from opentelemetry.sdk.resources import Attributes
|
|
9
15
|
from requests.exceptions import JSONDecodeError
|
|
10
16
|
|
|
11
17
|
from pkgs.argument_parser import CachedParser
|
|
12
|
-
from pkgs.serialization_util import serialize_for_api
|
|
13
|
-
from
|
|
18
|
+
from pkgs.serialization_util import JsonValue, serialize_for_api
|
|
19
|
+
from uncountable.core.environment import get_version
|
|
20
|
+
from uncountable.integration.telemetry import Logger, push_scope_optional
|
|
21
|
+
from uncountable.types import download_file_t
|
|
14
22
|
from uncountable.types.client_base import APIRequest, ClientMethods
|
|
23
|
+
from uncountable.types.client_config import ClientConfigOptions
|
|
15
24
|
|
|
16
25
|
from .file_upload import FileUpload, FileUploader, UploadedFile
|
|
17
|
-
from .types import
|
|
26
|
+
from .types import AuthDetailsAll, AuthDetailsApiKey, AuthDetailsOAuth
|
|
18
27
|
|
|
19
28
|
DT = typing.TypeVar("DT")
|
|
29
|
+
UNC_REQUEST_ID_HEADER = "X-UNC-REQUEST-ID"
|
|
30
|
+
UNC_SDK_VERSION_HEADER = "X-UNC-SDK-VERSION"
|
|
20
31
|
|
|
21
32
|
|
|
22
33
|
class EndpointMethod(StrEnum):
|
|
@@ -29,47 +40,58 @@ class HTTPRequestBase:
|
|
|
29
40
|
method: EndpointMethod
|
|
30
41
|
url: str
|
|
31
42
|
headers: dict[str, str]
|
|
32
|
-
body: typing.Optional[typing.Union[str, dict[str, str]]] = None
|
|
33
|
-
query_params: typing.Optional[dict[str, str]] = None
|
|
34
43
|
|
|
35
44
|
|
|
36
45
|
@dataclass(kw_only=True)
|
|
37
46
|
class HTTPGetRequest(HTTPRequestBase):
|
|
38
|
-
method:
|
|
47
|
+
method: EndpointMethod = EndpointMethod.GET
|
|
39
48
|
query_params: dict[str, str]
|
|
40
49
|
|
|
41
50
|
|
|
42
51
|
@dataclass(kw_only=True)
|
|
43
52
|
class HTTPPostRequest(HTTPRequestBase):
|
|
44
|
-
method:
|
|
45
|
-
body:
|
|
53
|
+
method: EndpointMethod = EndpointMethod.POST
|
|
54
|
+
body: str | dict[str, str]
|
|
46
55
|
|
|
47
56
|
|
|
48
57
|
HTTPRequest = HTTPPostRequest | HTTPGetRequest
|
|
49
58
|
|
|
50
59
|
|
|
51
|
-
|
|
52
60
|
@dataclass(kw_only=True)
|
|
53
|
-
class ClientConfig():
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
class ClientConfig(ClientConfigOptions):
|
|
62
|
+
transform_request: typing.Callable[[requests.Request], requests.Request] | None = (
|
|
63
|
+
None
|
|
64
|
+
)
|
|
65
|
+
logger: Logger | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
OAUTH_REFRESH_WINDOW_SECONDS = 60 * 5
|
|
56
69
|
|
|
57
|
-
|
|
70
|
+
|
|
71
|
+
class APIResponseError(Exception):
|
|
58
72
|
status_code: int
|
|
59
73
|
message: str
|
|
60
74
|
extra_details: dict[str, JsonValue] | None
|
|
61
75
|
|
|
62
76
|
def __init__(
|
|
63
|
-
self,
|
|
77
|
+
self,
|
|
78
|
+
status_code: int,
|
|
79
|
+
message: str,
|
|
80
|
+
extra_details: dict[str, JsonValue] | None,
|
|
81
|
+
request_id: str,
|
|
64
82
|
) -> None:
|
|
65
83
|
super().__init__(status_code, message, extra_details)
|
|
66
84
|
self.status_code = status_code
|
|
67
85
|
self.message = message
|
|
68
86
|
self.extra_details = extra_details
|
|
87
|
+
self.request_id = request_id
|
|
69
88
|
|
|
70
89
|
@classmethod
|
|
71
90
|
def construct_error(
|
|
72
|
-
cls,
|
|
91
|
+
cls,
|
|
92
|
+
status_code: int,
|
|
93
|
+
extra_details: dict[str, JsonValue] | None,
|
|
94
|
+
request_id: str,
|
|
73
95
|
) -> "APIResponseError":
|
|
74
96
|
message: str
|
|
75
97
|
match status_code:
|
|
@@ -92,77 +114,197 @@ class APIResponseError(BaseException):
|
|
|
92
114
|
case _:
|
|
93
115
|
message = "unknown error"
|
|
94
116
|
return APIResponseError(
|
|
95
|
-
status_code=status_code,
|
|
117
|
+
status_code=status_code,
|
|
118
|
+
message=message,
|
|
119
|
+
extra_details=extra_details,
|
|
120
|
+
request_id=request_id,
|
|
96
121
|
)
|
|
97
122
|
|
|
123
|
+
def __str__(self) -> str:
|
|
124
|
+
details_obj = {
|
|
125
|
+
"request_id": self.request_id,
|
|
126
|
+
"status_code": self.status_code,
|
|
127
|
+
"extra_details": self.extra_details,
|
|
128
|
+
}
|
|
129
|
+
details = json.dumps(details_obj)
|
|
130
|
+
return f"API response error ({self.status_code}): '{self.message}'. Details: {details}"
|
|
131
|
+
|
|
98
132
|
|
|
99
|
-
class SDKError(
|
|
133
|
+
class SDKError(Exception):
|
|
100
134
|
message: str
|
|
135
|
+
request_id: str
|
|
101
136
|
|
|
102
|
-
def __init__(self, message: str) -> None:
|
|
137
|
+
def __init__(self, message: str, *, request_id: str) -> None:
|
|
103
138
|
super().__init__(message)
|
|
104
139
|
self.message = message
|
|
140
|
+
self.request_id = request_id
|
|
105
141
|
|
|
106
142
|
def __str__(self) -> str:
|
|
107
|
-
return f"internal SDK error, please contact Uncountable support: {self.message}"
|
|
143
|
+
return f"internal SDK error (request id {self.request_id}), please contact Uncountable support: {self.message}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass(kw_only=True)
|
|
147
|
+
class OAuthBearerTokenCache:
|
|
148
|
+
token: str
|
|
149
|
+
expires_at: datetime.datetime
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(kw_only=True)
|
|
153
|
+
class GetOauthBearerTokenData:
|
|
154
|
+
access_token: str
|
|
155
|
+
expires_in: int
|
|
156
|
+
token_type: str
|
|
157
|
+
scope: str
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
oauth_bearer_token_data_parser = CachedParser(GetOauthBearerTokenData)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class DownloadedFile:
|
|
165
|
+
name: str
|
|
166
|
+
size: int
|
|
167
|
+
data: BytesIO
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
DownloadedFiles = list[DownloadedFile]
|
|
108
171
|
|
|
109
172
|
|
|
110
173
|
class Client(ClientMethods):
|
|
111
174
|
_parser_map: dict[type, CachedParser] = {}
|
|
112
|
-
_auth_details:
|
|
175
|
+
_auth_details: AuthDetailsAll
|
|
113
176
|
_base_url: str
|
|
114
177
|
_file_uploader: FileUploader
|
|
115
178
|
_cfg: ClientConfig
|
|
179
|
+
_oauth_bearer_token_cache: OAuthBearerTokenCache | None = None
|
|
180
|
+
_session: requests.Session
|
|
116
181
|
|
|
117
|
-
def __init__(
|
|
182
|
+
def __init__(
|
|
183
|
+
self,
|
|
184
|
+
*,
|
|
185
|
+
base_url: str,
|
|
186
|
+
auth_details: AuthDetailsAll,
|
|
187
|
+
config: ClientConfig | None = None,
|
|
188
|
+
):
|
|
118
189
|
self._auth_details = auth_details
|
|
119
190
|
self._base_url = base_url
|
|
120
|
-
self._file_uploader = FileUploader(self._base_url, self._auth_details)
|
|
121
191
|
self._cfg = config or ClientConfig()
|
|
192
|
+
self._session = requests.Session()
|
|
193
|
+
self._session.verify = not self._cfg.allow_insecure_tls
|
|
194
|
+
self._file_uploader = FileUploader(
|
|
195
|
+
self._base_url,
|
|
196
|
+
self._auth_details,
|
|
197
|
+
self._cfg.allow_insecure_tls,
|
|
198
|
+
logger=self._cfg.logger,
|
|
199
|
+
)
|
|
122
200
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
response = requests.get(
|
|
128
|
-
http_request.url,
|
|
129
|
-
headers=http_request.headers,
|
|
130
|
-
params=http_request.query_params,
|
|
131
|
-
verify=not self._cfg.allow_insecure_tls
|
|
132
|
-
)
|
|
133
|
-
case HTTPPostRequest():
|
|
134
|
-
response = requests.post(
|
|
135
|
-
http_request.url,
|
|
136
|
-
headers=http_request.headers,
|
|
137
|
-
data=http_request.body,
|
|
138
|
-
params=http_request.query_params,
|
|
139
|
-
verify=not self._cfg.allow_insecure_tls
|
|
140
|
-
)
|
|
141
|
-
case _:
|
|
142
|
-
typing.assert_never(http_request)
|
|
201
|
+
@classmethod
|
|
202
|
+
def _validate_response_status(
|
|
203
|
+
cls, response: requests.Response, request_id: str
|
|
204
|
+
) -> None:
|
|
143
205
|
if response.status_code < 200 or response.status_code > 299:
|
|
144
206
|
extra_details: dict[str, JsonValue] | None = None
|
|
145
207
|
try:
|
|
146
208
|
data = response.json()
|
|
147
|
-
|
|
148
|
-
extra_details = data["error"]
|
|
209
|
+
extra_details = data
|
|
149
210
|
except JSONDecodeError:
|
|
150
|
-
|
|
211
|
+
extra_details = {
|
|
212
|
+
"body": response.text,
|
|
213
|
+
}
|
|
151
214
|
raise APIResponseError.construct_error(
|
|
152
|
-
status_code=response.status_code,
|
|
215
|
+
status_code=response.status_code,
|
|
216
|
+
extra_details=extra_details,
|
|
217
|
+
request_id=request_id,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def _get_response_json(
|
|
221
|
+
self, response: requests.Response, request_id: str
|
|
222
|
+
) -> dict[str, JsonValue]:
|
|
223
|
+
self._validate_response_status(response, request_id)
|
|
224
|
+
try:
|
|
225
|
+
return typing.cast(dict[str, JsonValue], response.json())
|
|
226
|
+
except JSONDecodeError as e:
|
|
227
|
+
raise SDKError("unable to process response", request_id=request_id) from e
|
|
228
|
+
|
|
229
|
+
def _send_request(
|
|
230
|
+
self, request: requests.Request, *, timeout: float | None = None
|
|
231
|
+
) -> requests.Response:
|
|
232
|
+
if self._cfg.extra_headers is not None:
|
|
233
|
+
request.headers = {**request.headers, **self._cfg.extra_headers}
|
|
234
|
+
if self._cfg.transform_request is not None:
|
|
235
|
+
request = self._cfg.transform_request(request)
|
|
236
|
+
prepared_request = request.prepare()
|
|
237
|
+
response = self._session.send(prepared_request, timeout=timeout)
|
|
238
|
+
return response
|
|
239
|
+
|
|
240
|
+
def do_request(self, *, api_request: APIRequest, return_type: type[DT]) -> DT:
|
|
241
|
+
request_id = str(uuid4())
|
|
242
|
+
http_request = self._build_http_request(
|
|
243
|
+
api_request=api_request, request_id=request_id
|
|
244
|
+
)
|
|
245
|
+
match http_request:
|
|
246
|
+
case HTTPGetRequest():
|
|
247
|
+
request = requests.Request("GET", http_request.url)
|
|
248
|
+
request.params = http_request.query_params
|
|
249
|
+
case HTTPPostRequest():
|
|
250
|
+
request = requests.Request("POST", http_request.url)
|
|
251
|
+
request.data = http_request.body
|
|
252
|
+
case _:
|
|
253
|
+
typing.assert_never(http_request)
|
|
254
|
+
request.headers = http_request.headers
|
|
255
|
+
attributes: Attributes = {
|
|
256
|
+
"method": http_request.method,
|
|
257
|
+
"endpoint": api_request.endpoint,
|
|
258
|
+
}
|
|
259
|
+
with push_scope_optional(self._cfg.logger, "api_call", attributes=attributes):
|
|
260
|
+
if self._cfg.logger is not None:
|
|
261
|
+
self._cfg.logger.log_info(api_request.endpoint, attributes=attributes)
|
|
262
|
+
timeout = (
|
|
263
|
+
api_request.request_options.timeout_secs
|
|
264
|
+
if api_request.request_options is not None
|
|
265
|
+
else None
|
|
153
266
|
)
|
|
267
|
+
response = self._send_request(request, timeout=timeout)
|
|
268
|
+
response_data = self._get_response_json(response, request_id=request_id)
|
|
154
269
|
cached_parser = self._get_cached_parser(return_type)
|
|
155
270
|
try:
|
|
156
|
-
data =
|
|
271
|
+
data = response_data["data"]
|
|
157
272
|
return cached_parser.parse_api(data)
|
|
158
|
-
except ValueError
|
|
159
|
-
raise SDKError("unable to process response")
|
|
273
|
+
except (ValueError, JSONDecodeError, KeyError) as e:
|
|
274
|
+
raise SDKError("unable to process response", request_id=request_id) from e
|
|
160
275
|
|
|
161
276
|
def _get_cached_parser(self, data_type: type[DT]) -> CachedParser[DT]:
|
|
162
277
|
if data_type not in self._parser_map:
|
|
163
278
|
self._parser_map[data_type] = CachedParser(data_type)
|
|
164
279
|
return self._parser_map[data_type]
|
|
165
280
|
|
|
281
|
+
def _get_oauth_bearer_token(self, *, oauth_details: AuthDetailsOAuth) -> str:
|
|
282
|
+
if (
|
|
283
|
+
self._oauth_bearer_token_cache is None
|
|
284
|
+
or (
|
|
285
|
+
self._oauth_bearer_token_cache.expires_at
|
|
286
|
+
- datetime.datetime.now(tz=UTC)
|
|
287
|
+
).total_seconds()
|
|
288
|
+
< OAUTH_REFRESH_WINDOW_SECONDS
|
|
289
|
+
):
|
|
290
|
+
refresh_url = urljoin(self._base_url, "/token/get_bearer_token")
|
|
291
|
+
request = requests.Request("POST", refresh_url)
|
|
292
|
+
request.data = {
|
|
293
|
+
"client_secret": oauth_details.refresh_token,
|
|
294
|
+
"scope": oauth_details.scope,
|
|
295
|
+
"grant_type": "client_credentials",
|
|
296
|
+
}
|
|
297
|
+
response = self._send_request(request)
|
|
298
|
+
data = self._get_response_json(response, request_id=str(uuid4()))
|
|
299
|
+
token_data = oauth_bearer_token_data_parser.parse_storage(data)
|
|
300
|
+
self._oauth_bearer_token_cache = OAuthBearerTokenCache(
|
|
301
|
+
token=token_data.access_token,
|
|
302
|
+
expires_at=datetime.datetime.now(tz=UTC)
|
|
303
|
+
+ timedelta(seconds=token_data.expires_in),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return self._oauth_bearer_token_cache.token
|
|
307
|
+
|
|
166
308
|
def _build_auth_headers(self) -> dict[str, str]:
|
|
167
309
|
match self._auth_details:
|
|
168
310
|
case AuthDetailsApiKey():
|
|
@@ -170,10 +312,17 @@ class Client(ClientMethods):
|
|
|
170
312
|
f"{self._auth_details.api_id}:{self._auth_details.api_secret_key}".encode()
|
|
171
313
|
).decode("utf-8")
|
|
172
314
|
return {"Authorization": f"Basic {encoded}"}
|
|
315
|
+
case AuthDetailsOAuth():
|
|
316
|
+
token = self._get_oauth_bearer_token(oauth_details=self._auth_details)
|
|
317
|
+
return {"Authorization": f"Bearer {token}"}
|
|
173
318
|
typing.assert_never(self._auth_details)
|
|
174
319
|
|
|
175
|
-
def _build_http_request(
|
|
320
|
+
def _build_http_request(
|
|
321
|
+
self, *, api_request: APIRequest, request_id: str
|
|
322
|
+
) -> HTTPRequest:
|
|
176
323
|
headers = self._build_auth_headers()
|
|
324
|
+
headers[UNC_REQUEST_ID_HEADER] = request_id
|
|
325
|
+
headers[UNC_SDK_VERSION_HEADER] = get_version()
|
|
177
326
|
method = api_request.method.lower()
|
|
178
327
|
data = {"data": json.dumps(serialize_for_api(api_request.args))}
|
|
179
328
|
match method:
|
|
@@ -194,6 +343,52 @@ class Client(ClientMethods):
|
|
|
194
343
|
case _:
|
|
195
344
|
raise ValueError(f"unsupported request method: {method}")
|
|
196
345
|
|
|
346
|
+
def _get_downloaded_filename(self, *, cd: str | None) -> str:
|
|
347
|
+
if not cd:
|
|
348
|
+
return "Unknown"
|
|
349
|
+
|
|
350
|
+
fname = re.findall(r"filename\*=UTF-8''(.+)", cd)
|
|
351
|
+
if fname:
|
|
352
|
+
return unquote(fname[0])
|
|
353
|
+
|
|
354
|
+
fname = re.findall(r'filename="?(.+)"?', cd)
|
|
355
|
+
if fname:
|
|
356
|
+
return str(fname[0].strip('"'))
|
|
357
|
+
|
|
358
|
+
return "Unknown"
|
|
359
|
+
|
|
360
|
+
def download_files(
|
|
361
|
+
self, *, file_query: download_file_t.FileDownloadQuery
|
|
362
|
+
) -> DownloadedFiles:
|
|
363
|
+
"""Download a file from uncountable."""
|
|
364
|
+
request_id = str(uuid4())
|
|
365
|
+
api_request = APIRequest(
|
|
366
|
+
method=download_file_t.ENDPOINT_METHOD,
|
|
367
|
+
endpoint=download_file_t.ENDPOINT_PATH,
|
|
368
|
+
args=download_file_t.Arguments(
|
|
369
|
+
file_query=file_query,
|
|
370
|
+
),
|
|
371
|
+
)
|
|
372
|
+
http_request = self._build_http_request(
|
|
373
|
+
api_request=api_request, request_id=request_id
|
|
374
|
+
)
|
|
375
|
+
request = requests.Request(http_request.method.value, http_request.url)
|
|
376
|
+
request.headers = http_request.headers
|
|
377
|
+
assert isinstance(http_request, HTTPGetRequest)
|
|
378
|
+
request.params = http_request.query_params
|
|
379
|
+
response = self._send_request(request)
|
|
380
|
+
self._validate_response_status(response, request_id)
|
|
381
|
+
|
|
382
|
+
content = response.content
|
|
383
|
+
content_disposition = response.headers.get("Content-Disposition", None)
|
|
384
|
+
return [
|
|
385
|
+
DownloadedFile(
|
|
386
|
+
name=self._get_downloaded_filename(cd=content_disposition),
|
|
387
|
+
size=len(content),
|
|
388
|
+
data=BytesIO(content),
|
|
389
|
+
)
|
|
390
|
+
]
|
|
391
|
+
|
|
197
392
|
def upload_files(
|
|
198
393
|
self: typing.Self, *, file_uploads: list[FileUpload]
|
|
199
394
|
) -> list[UploadedFile]:
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import os
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from uncountable.types import integration_server_t
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@functools.cache
|
|
9
|
+
def get_version() -> str:
|
|
10
|
+
try:
|
|
11
|
+
version_str = version("UncountablePythonSDK")
|
|
12
|
+
except PackageNotFoundError:
|
|
13
|
+
version_str = "unknown"
|
|
14
|
+
return version_str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_server_env() -> str | None:
|
|
18
|
+
return os.environ.get("UNC_SERVER_ENV")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_http_server_port() -> int:
|
|
22
|
+
return int(os.environ.get("UNC_WEBHOOK_SERVER_PORT", "5001"))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_local_admin_server_port() -> int:
|
|
26
|
+
return int(os.environ.get("UNC_ADMIN_SERVER_PORT", "50051"))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_otel_enabled() -> bool:
|
|
30
|
+
return os.environ.get("UNC_OTEL_ENABLED") == "true"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_profiles_module() -> str:
|
|
34
|
+
return os.environ["UNC_PROFILES_MODULE"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_integration_envs() -> list[integration_server_t.IntegrationEnvironment]:
|
|
38
|
+
return [
|
|
39
|
+
integration_server_t.IntegrationEnvironment(env)
|
|
40
|
+
for env in os.environ.get("UNC_INTEGRATION_ENVS", "prod").split(",")
|
|
41
|
+
]
|
uncountable/core/file_upload.py
CHANGED
|
@@ -4,18 +4,21 @@ from dataclasses import dataclass
|
|
|
4
4
|
from enum import StrEnum
|
|
5
5
|
from io import BytesIO
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Generator, Literal, Self
|
|
7
|
+
from typing import Generator, Literal, Self, assert_never
|
|
8
8
|
|
|
9
9
|
import aiohttp
|
|
10
10
|
import aiotus
|
|
11
11
|
|
|
12
|
-
from .
|
|
12
|
+
from uncountable.integration.telemetry import Logger, push_scope_optional
|
|
13
|
+
|
|
14
|
+
from .types import AuthDetailsAll, AuthDetailsApiKey
|
|
13
15
|
|
|
14
16
|
_CHUNK_SIZE = 5 * 1024 * 1024 # s3 requires 5MiB minimum
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
class FileUploadType(StrEnum):
|
|
18
20
|
MEDIA_FILE_UPLOAD = "MEDIA_FILE_UPLOAD"
|
|
21
|
+
DATA_FILE_UPLOAD = "DATA_FILE_UPLOAD"
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
@dataclass(kw_only=True)
|
|
@@ -23,10 +26,17 @@ class MediaFileUpload:
|
|
|
23
26
|
"""Upload file from a path on disk"""
|
|
24
27
|
|
|
25
28
|
path: str
|
|
26
|
-
type: FileUploadType.MEDIA_FILE_UPLOAD = FileUploadType.MEDIA_FILE_UPLOAD
|
|
29
|
+
type: Literal[FileUploadType.MEDIA_FILE_UPLOAD] = FileUploadType.MEDIA_FILE_UPLOAD
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(kw_only=True)
|
|
33
|
+
class DataFileUpload:
|
|
34
|
+
data: BytesIO
|
|
35
|
+
name: str
|
|
36
|
+
type: Literal[FileUploadType.DATA_FILE_UPLOAD] = FileUploadType.DATA_FILE_UPLOAD
|
|
27
37
|
|
|
28
38
|
|
|
29
|
-
FileUpload = MediaFileUpload
|
|
39
|
+
FileUpload = MediaFileUpload | DataFileUpload
|
|
30
40
|
|
|
31
41
|
|
|
32
42
|
@dataclass(kw_only=True)
|
|
@@ -37,12 +47,14 @@ class FileBytes:
|
|
|
37
47
|
|
|
38
48
|
@contextmanager
|
|
39
49
|
def file_upload_data(file_upload: FileUpload) -> Generator[FileBytes, None, None]:
|
|
40
|
-
match file_upload
|
|
41
|
-
case
|
|
50
|
+
match file_upload:
|
|
51
|
+
case MediaFileUpload():
|
|
42
52
|
with open(file_upload.path, "rb") as f:
|
|
43
53
|
yield FileBytes(
|
|
44
54
|
name=Path(file_upload.path).name, bytes_data=BytesIO(f.read())
|
|
45
55
|
)
|
|
56
|
+
case DataFileUpload():
|
|
57
|
+
yield FileBytes(name=file_upload.name, bytes_data=file_upload.data)
|
|
46
58
|
|
|
47
59
|
|
|
48
60
|
@dataclass(kw_only=True)
|
|
@@ -56,15 +68,27 @@ class UploadFailed(Exception):
|
|
|
56
68
|
|
|
57
69
|
|
|
58
70
|
class FileUploader:
|
|
59
|
-
_auth_details:
|
|
71
|
+
_auth_details: AuthDetailsAll
|
|
60
72
|
_base_url: str
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
_allow_insecure_tls: bool
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self: Self,
|
|
77
|
+
base_url: str,
|
|
78
|
+
auth_details: AuthDetailsAll,
|
|
79
|
+
allow_insecure_tls: bool = False,
|
|
80
|
+
logger: Logger | None = None,
|
|
81
|
+
) -> None:
|
|
63
82
|
self._base_url = base_url
|
|
64
83
|
self._auth_details = auth_details
|
|
84
|
+
self._allow_insecure_tls = allow_insecure_tls
|
|
85
|
+
self._logger = logger
|
|
65
86
|
|
|
66
87
|
async def _upload_file(self: Self, file_upload: FileUpload) -> UploadedFile:
|
|
67
88
|
creation_url = f"{self._base_url}/api/external/file_upload/files"
|
|
89
|
+
if not isinstance(self._auth_details, AuthDetailsApiKey):
|
|
90
|
+
raise NotImplementedError("Unsupported authentication method.")
|
|
91
|
+
|
|
68
92
|
auth = aiohttp.BasicAuth(
|
|
69
93
|
self._auth_details.api_id, self._auth_details.api_secret_key
|
|
70
94
|
)
|
|
@@ -73,19 +97,40 @@ class FileUploader:
|
|
|
73
97
|
auth=auth, headers={"Origin": self._base_url}
|
|
74
98
|
) as session,
|
|
75
99
|
):
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
attributes = {}
|
|
101
|
+
match file_upload:
|
|
102
|
+
case MediaFileUpload():
|
|
103
|
+
attributes["file_path"] = file_upload.path
|
|
104
|
+
case DataFileUpload():
|
|
105
|
+
attributes["file_name"] = file_upload.name
|
|
106
|
+
case _:
|
|
107
|
+
assert_never(file_upload)
|
|
108
|
+
with push_scope_optional(
|
|
109
|
+
self._logger, "upload_file", attributes=attributes
|
|
110
|
+
):
|
|
111
|
+
if self._logger is not None:
|
|
112
|
+
self._logger.log_info("Uploading file", attributes=attributes)
|
|
113
|
+
with file_upload_data(file_upload) as file_bytes:
|
|
114
|
+
if file_bytes.bytes_data.read(1) == b"":
|
|
115
|
+
raise UploadFailed(
|
|
116
|
+
f"Failed to upload empty file: {file_bytes.name}"
|
|
117
|
+
)
|
|
118
|
+
file_bytes.bytes_data.seek(0)
|
|
119
|
+
location = await aiotus.upload(
|
|
120
|
+
creation_url,
|
|
121
|
+
file_bytes.bytes_data,
|
|
122
|
+
{"filename": file_bytes.name.encode()},
|
|
123
|
+
client_session=session,
|
|
124
|
+
config=aiotus.RetryConfiguration(
|
|
125
|
+
ssl=not self._allow_insecure_tls
|
|
126
|
+
),
|
|
127
|
+
chunksize=_CHUNK_SIZE,
|
|
128
|
+
)
|
|
129
|
+
if location is None:
|
|
130
|
+
raise UploadFailed(f"Failed to upload: {file_bytes.name}")
|
|
131
|
+
return UploadedFile(
|
|
132
|
+
name=file_bytes.name, file_id=int(location.path.split("/")[-1])
|
|
133
|
+
)
|
|
89
134
|
|
|
90
135
|
def upload_files(
|
|
91
136
|
self: Self, *, file_uploads: list[FileUpload]
|
uncountable/core/types.py
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
import json
|
|
3
|
-
import typing
|
|
4
1
|
from dataclasses import dataclass
|
|
5
|
-
from enum import StrEnum
|
|
6
|
-
from urllib.parse import urljoin
|
|
7
|
-
|
|
8
|
-
import aiohttp
|
|
9
|
-
import requests
|
|
10
|
-
|
|
11
|
-
from pkgs.argument_parser import CachedParser
|
|
12
|
-
from pkgs.serialization_util import serialize_for_api
|
|
13
|
-
from uncountable.types.client_base import APIRequest, ClientMethods
|
|
14
2
|
|
|
15
3
|
|
|
16
4
|
@dataclass(kw_only=True)
|
|
@@ -19,4 +7,11 @@ class AuthDetailsApiKey:
|
|
|
19
7
|
api_secret_key: str
|
|
20
8
|
|
|
21
9
|
|
|
22
|
-
|
|
10
|
+
@dataclass(kw_only=True)
|
|
11
|
+
class AuthDetailsOAuth:
|
|
12
|
+
refresh_token: str
|
|
13
|
+
scope: str = "unc.rnd"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
AuthDetails = AuthDetailsApiKey # Legacy Mapping
|
|
17
|
+
AuthDetailsAll = AuthDetailsApiKey | AuthDetailsOAuth
|