UncountablePythonSDK 0.0.52__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 +54 -7
- 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 +6 -4
- examples/async_batch.py +3 -3
- examples/basic_auth.py +7 -0
- examples/create_entity.py +3 -1
- examples/create_ingredient_sdk.py +34 -0
- examples/download_files.py +26 -0
- examples/edit_recipe_inputs.py +4 -2
- 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 +4 -1
- 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 +1 -2
- pkgs/argument_parser/__init__.py +9 -0
- pkgs/argument_parser/_is_namedtuple.py +3 -0
- pkgs/argument_parser/argument_parser.py +217 -70
- pkgs/filesystem_utils/__init__.py +1 -0
- pkgs/filesystem_utils/_blob_session.py +144 -0
- pkgs/filesystem_utils/_gdrive_session.py +10 -7
- pkgs/filesystem_utils/_s3_session.py +15 -13
- pkgs/filesystem_utils/_sftp_session.py +11 -7
- pkgs/filesystem_utils/file_type_utils.py +30 -10
- pkgs/py.typed +0 -0
- pkgs/serialization/__init__.py +7 -2
- 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 +47 -26
- pkgs/serialization/serial_generic.py +16 -0
- pkgs/serialization/serial_union.py +17 -14
- pkgs/serialization/yaml.py +4 -1
- pkgs/serialization_util/__init__.py +6 -0
- pkgs/serialization_util/dataclasses.py +14 -0
- pkgs/serialization_util/serialization_helpers.py +15 -5
- pkgs/type_spec/actions_registry/__main__.py +0 -4
- pkgs/type_spec/actions_registry/emit_typescript.py +5 -5
- pkgs/type_spec/builder.py +354 -119
- pkgs/type_spec/builder_types.py +9 -0
- pkgs/type_spec/config.py +51 -11
- 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 +127 -36
- pkgs/type_spec/emit_open_api_util.py +5 -6
- pkgs/type_spec/emit_python.py +329 -121
- pkgs/type_spec/emit_typescript.py +117 -256
- pkgs/type_spec/emit_typescript_util.py +291 -2
- pkgs/type_spec/load_types.py +18 -4
- 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 +124 -29
- 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 +26 -9
- 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/core/async_batch.py +1 -1
- uncountable/core/client.py +142 -39
- uncountable/core/environment.py +41 -0
- uncountable/core/file_upload.py +52 -18
- uncountable/integration/cli.py +142 -0
- uncountable/integration/construct_client.py +8 -8
- uncountable/integration/cron.py +11 -37
- uncountable/integration/db/connect.py +12 -2
- uncountable/integration/db/session.py +25 -0
- uncountable/integration/entrypoint.py +8 -37
- uncountable/integration/executors/executors.py +125 -2
- uncountable/integration/executors/generic_upload_executor.py +87 -29
- uncountable/integration/executors/script_executor.py +3 -3
- uncountable/integration/http_server/__init__.py +5 -0
- uncountable/integration/http_server/types.py +69 -0
- uncountable/integration/job.py +242 -12
- 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/retrieve_secret.py +26 -4
- uncountable/integration/server.py +94 -69
- uncountable/integration/telemetry.py +150 -34
- uncountable/integration/webhook_server/entrypoint.py +97 -0
- uncountable/types/__init__.py +78 -1
- uncountable/types/api/batch/execute_batch.py +13 -6
- uncountable/types/api/batch/execute_batch_load_async.py +9 -3
- uncountable/types/api/chemical/convert_chemical_formats.py +17 -5
- 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 +19 -7
- uncountable/types/api/entity/create_entity.py +17 -8
- 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 +13 -4
- 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 +42 -10
- uncountable/types/api/entity/lock_entity.py +11 -4
- uncountable/types/api/entity/lookup_entity.py +116 -0
- uncountable/types/api/entity/resolve_entity_ids.py +15 -6
- uncountable/types/api/entity/set_entity_field_values.py +44 -0
- uncountable/types/api/entity/set_values.py +10 -3
- uncountable/types/api/entity/transition_entity_phase.py +22 -7
- uncountable/types/api/entity/unlock_entity.py +10 -3
- uncountable/types/api/equipment/associate_equipment_input.py +9 -3
- uncountable/types/api/field_options/upsert_field_options.py +17 -7
- 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 +16 -7
- uncountable/types/api/id_source/match_id_source.py +14 -5
- uncountable/types/api/input_groups/get_input_group_names.py +13 -4
- uncountable/types/api/inputs/create_inputs.py +23 -9
- uncountable/types/api/inputs/get_input_data.py +30 -12
- uncountable/types/api/inputs/get_input_names.py +16 -7
- uncountable/types/api/inputs/get_inputs_data.py +25 -7
- uncountable/types/api/inputs/set_input_attribute_values.py +12 -6
- uncountable/types/api/inputs/set_input_category.py +12 -5
- uncountable/types/api/inputs/set_input_subcategories.py +10 -3
- uncountable/types/api/inputs/set_intermediate_type.py +11 -4
- 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/update_entity_material_families.py +10 -4
- 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 +28 -13
- uncountable/types/api/outputs/get_output_names.py +15 -6
- uncountable/types/api/outputs/get_output_organization.py +173 -0
- uncountable/types/api/outputs/resolve_output_conditions.py +20 -8
- uncountable/types/api/permissions/set_core_permissions.py +26 -10
- uncountable/types/api/project/get_projects.py +16 -7
- uncountable/types/api/project/get_projects_data.py +17 -8
- uncountable/types/api/recipe_links/create_recipe_link.py +12 -5
- uncountable/types/api/recipe_links/remove_recipe_link.py +11 -4
- uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +16 -7
- uncountable/types/api/recipes/add_recipe_to_project.py +10 -3
- uncountable/types/api/recipes/add_time_series_data.py +64 -0
- uncountable/types/api/recipes/archive_recipes.py +11 -4
- uncountable/types/api/recipes/associate_recipe_as_input.py +12 -5
- uncountable/types/api/recipes/associate_recipe_as_lot.py +10 -3
- 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 +15 -9
- uncountable/types/api/recipes/create_recipes.py +21 -9
- uncountable/types/api/recipes/disassociate_recipe_as_input.py +10 -3
- uncountable/types/api/recipes/edit_recipe_inputs.py +134 -22
- uncountable/types/api/recipes/get_column_calculation_values.py +57 -0
- uncountable/types/api/recipes/get_curve.py +11 -5
- uncountable/types/api/recipes/get_recipe_calculations.py +13 -7
- uncountable/types/api/recipes/get_recipe_links.py +10 -4
- uncountable/types/api/recipes/get_recipe_names.py +13 -4
- uncountable/types/api/recipes/get_recipe_output_metadata.py +12 -6
- uncountable/types/api/recipes/get_recipes_data.py +87 -33
- uncountable/types/api/recipes/lock_recipes.py +19 -8
- uncountable/types/api/recipes/remove_recipe_from_project.py +10 -3
- uncountable/types/api/recipes/set_recipe_inputs.py +16 -10
- uncountable/types/api/recipes/set_recipe_metadata.py +10 -3
- uncountable/types/api/recipes/set_recipe_output_annotations.py +24 -12
- uncountable/types/api/recipes/set_recipe_output_file.py +55 -0
- uncountable/types/api/recipes/set_recipe_outputs.py +35 -12
- uncountable/types/api/recipes/set_recipe_tags.py +26 -9
- uncountable/types/api/recipes/set_recipe_total.py +59 -0
- uncountable/types/api/recipes/unarchive_recipes.py +10 -3
- uncountable/types/api/recipes/unlock_recipes.py +14 -6
- 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 +11 -4
- uncountable/types/api/uploader/complete_async_parse.py +46 -0
- uncountable/types/api/uploader/invoke_uploader.py +13 -6
- uncountable/types/api/user/__init__.py +1 -0
- uncountable/types/api/user/get_current_user_info.py +40 -0
- uncountable/types/async_batch.py +2 -1
- uncountable/types/async_batch_processor.py +618 -18
- uncountable/types/async_batch_t.py +54 -7
- 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 +0 -1
- uncountable/types/base_t.py +13 -11
- uncountable/types/calculations.py +0 -1
- uncountable/types/calculations_t.py +5 -2
- uncountable/types/chemical_structure.py +0 -1
- uncountable/types/chemical_structure_t.py +6 -5
- uncountable/types/client_base.py +751 -70
- uncountable/types/client_config.py +1 -1
- uncountable/types/client_config_t.py +17 -3
- uncountable/types/curves.py +0 -1
- uncountable/types/curves_t.py +10 -7
- uncountable/types/data.py +12 -0
- uncountable/types/data_t.py +103 -0
- uncountable/types/entity.py +4 -1
- uncountable/types/entity_t.py +125 -7
- uncountable/types/experiment_groups.py +0 -1
- uncountable/types/experiment_groups_t.py +5 -2
- uncountable/types/exports.py +8 -0
- uncountable/types/exports_t.py +34 -0
- uncountable/types/field_values.py +19 -1
- uncountable/types/field_values_t.py +246 -9
- uncountable/types/fields.py +0 -1
- uncountable/types/fields_t.py +5 -2
- uncountable/types/generic_upload.py +6 -1
- uncountable/types/generic_upload_t.py +88 -9
- uncountable/types/id_source.py +0 -1
- uncountable/types/id_source_t.py +26 -7
- uncountable/types/identifier.py +0 -1
- uncountable/types/identifier_t.py +13 -5
- uncountable/types/input_attributes.py +0 -1
- uncountable/types/input_attributes_t.py +4 -4
- uncountable/types/inputs.py +1 -1
- uncountable/types/inputs_t.py +24 -4
- 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 +4 -6
- uncountable/types/job_definition_t.py +96 -65
- 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 +0 -1
- uncountable/types/outputs_t.py +6 -3
- uncountable/types/overrides.py +9 -0
- uncountable/types/overrides_t.py +49 -0
- uncountable/types/permissions.py +0 -1
- uncountable/types/permissions_t.py +1 -2
- uncountable/types/phases.py +0 -1
- uncountable/types/phases_t.py +5 -2
- uncountable/types/post_base.py +0 -1
- uncountable/types/post_base_t.py +1 -2
- uncountable/types/queued_job.py +17 -0
- uncountable/types/queued_job_t.py +140 -0
- uncountable/types/recipe_identifiers.py +0 -1
- uncountable/types/recipe_identifiers_t.py +21 -8
- uncountable/types/recipe_inputs.py +0 -1
- uncountable/types/recipe_inputs_t.py +1 -2
- uncountable/types/recipe_links.py +0 -1
- uncountable/types/recipe_links_t.py +7 -4
- uncountable/types/recipe_metadata.py +0 -1
- uncountable/types/recipe_metadata_t.py +14 -9
- uncountable/types/recipe_output_metadata.py +0 -1
- uncountable/types/recipe_output_metadata_t.py +5 -2
- uncountable/types/recipe_tags.py +0 -1
- uncountable/types/recipe_tags_t.py +5 -2
- uncountable/types/recipe_workflow_steps.py +0 -1
- uncountable/types/recipe_workflow_steps_t.py +14 -7
- uncountable/types/recipes.py +0 -1
- uncountable/types/recipes_t.py +6 -2
- uncountable/types/response.py +0 -1
- uncountable/types/response_t.py +3 -2
- uncountable/types/secret_retrieval.py +0 -1
- uncountable/types/secret_retrieval_t.py +13 -7
- 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 +0 -1
- uncountable/types/units_t.py +5 -2
- uncountable/types/uploader.py +24 -0
- uncountable/types/uploader_t.py +222 -0
- uncountable/types/users.py +0 -1
- uncountable/types/users_t.py +5 -2
- uncountable/types/webhook_job.py +9 -0
- uncountable/types/webhook_job_t.py +48 -0
- uncountable/types/workflows.py +0 -1
- uncountable/types/workflows_t.py +10 -4
- uncountablepythonsdk-0.0.131.dist-info/METADATA +64 -0
- uncountablepythonsdk-0.0.131.dist-info/RECORD +363 -0
- {UncountablePythonSDK-0.0.52.dist-info → uncountablepythonsdk-0.0.131.dist-info}/WHEEL +1 -1
- UncountablePythonSDK-0.0.52.dist-info/METADATA +0 -56
- UncountablePythonSDK-0.0.52.dist-info/RECORD +0 -246
- docs/quickstart.md +0 -19
- uncountable/core/version.py +0 -11
- {UncountablePythonSDK-0.0.52.dist-info → uncountablepythonsdk-0.0.131.dist-info}/top_level.txt +0 -0
|
@@ -13,11 +13,14 @@ One of the following can be specified on the name of a argument:
|
|
|
13
13
|
After that you can also specify a `!` indicating the argument may not be null.
|
|
14
14
|
If this is not specified, then a null input on this argument should produce a null output.
|
|
15
15
|
We prefer not to use `!` as we want to encourage null pass-through where possible.
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
If null is allowed as a legitimate value, such as in conditionals like `is_null`,
|
|
18
|
+
then `!usenull` must be specified, this distinguishes it from the pass-through case.
|
|
19
|
+
The accepted argument type must accept "None", it is not implied.
|
|
17
20
|
"""
|
|
18
21
|
|
|
19
22
|
import sys
|
|
20
|
-
from typing import TypeVar, cast
|
|
23
|
+
from typing import Match, Pattern, TypeVar, cast
|
|
21
24
|
|
|
22
25
|
import regex as re
|
|
23
26
|
|
|
@@ -53,7 +56,7 @@ class Source:
|
|
|
53
56
|
def has_more(self) -> bool:
|
|
54
57
|
return self._at < len(self._text)
|
|
55
58
|
|
|
56
|
-
def match(self, expression:
|
|
59
|
+
def match(self, expression: Pattern[str]) -> Match[str] | None:
|
|
57
60
|
self.skip_space()
|
|
58
61
|
m = expression.match(self._text, self._at)
|
|
59
62
|
if m is not None:
|
|
@@ -84,7 +87,7 @@ class Source:
|
|
|
84
87
|
return self._text[start : self._at]
|
|
85
88
|
|
|
86
89
|
|
|
87
|
-
_re_argument_name = re.compile(r"([a-z_]+)(\?|\+)?(
|
|
90
|
+
_re_argument_name = re.compile(r"([a-z_]+)(\?|\+)?(!|!usenull)?:")
|
|
88
91
|
|
|
89
92
|
|
|
90
93
|
def parse_function_signature(text: str) -> ParsedFunctionSignature:
|
|
@@ -101,11 +104,18 @@ def parse_function_signature(text: str) -> ParsedFunctionSignature:
|
|
|
101
104
|
|
|
102
105
|
type_str = source.extract_type()
|
|
103
106
|
ref_name = arg_group.group(1)
|
|
104
|
-
is_missing = arg_group.group(2) == "?"
|
|
105
|
-
is_repeating = arg_group.group(2) == "+"
|
|
106
|
-
pass_null = arg_group.group(3) is None
|
|
107
|
+
# is_missing = arg_group.group(2) == "?"
|
|
108
|
+
# is_repeating = arg_group.group(2) == "+"
|
|
107
109
|
type_path = parse_type_str(type_str)
|
|
108
110
|
|
|
111
|
+
match arg_group.group(3):
|
|
112
|
+
case "!":
|
|
113
|
+
on_null = value_spec_t.OnNull.DISALLOW
|
|
114
|
+
case "!usenull":
|
|
115
|
+
on_null = value_spec_t.OnNull.USE
|
|
116
|
+
case _:
|
|
117
|
+
on_null = value_spec_t.OnNull.PASS
|
|
118
|
+
|
|
109
119
|
extant = value_spec_t.ArgumentExtant.REQUIRED
|
|
110
120
|
extant_marker = arg_group.group(2)
|
|
111
121
|
if extant_marker == "?":
|
|
@@ -116,7 +126,7 @@ def parse_function_signature(text: str) -> ParsedFunctionSignature:
|
|
|
116
126
|
arguments.append(
|
|
117
127
|
ParsedFunctionArgument(
|
|
118
128
|
ref_name=ref_name,
|
|
119
|
-
|
|
129
|
+
on_null=on_null,
|
|
120
130
|
extant=extant,
|
|
121
131
|
type_path=type_path,
|
|
122
132
|
)
|
|
@@ -145,6 +155,7 @@ key_return = "return"
|
|
|
145
155
|
key_description = "description"
|
|
146
156
|
key_brief = "brief"
|
|
147
157
|
key_name = "name"
|
|
158
|
+
key_draft = "draft"
|
|
148
159
|
|
|
149
160
|
|
|
150
161
|
TypeT = TypeVar("TypeT")
|
|
@@ -208,7 +219,7 @@ def main() -> None:
|
|
|
208
219
|
name=arg_name,
|
|
209
220
|
description=arg_description,
|
|
210
221
|
type=convert_to_value_spec_type(in_argument.type_path),
|
|
211
|
-
|
|
222
|
+
on_null=in_argument.on_null,
|
|
212
223
|
extant=in_argument.extant,
|
|
213
224
|
)
|
|
214
225
|
)
|
|
@@ -219,6 +230,11 @@ def main() -> None:
|
|
|
219
230
|
|
|
220
231
|
brief = get_as(spec, key_brief, str)
|
|
221
232
|
description = get_as(spec, key_description, str)
|
|
233
|
+
draft = (
|
|
234
|
+
get_as(spec, key_draft, bool)
|
|
235
|
+
if spec.get(key_draft) is not None
|
|
236
|
+
else None
|
|
237
|
+
)
|
|
222
238
|
|
|
223
239
|
return_value = get(spec, key_return)
|
|
224
240
|
where.append("return")
|
|
@@ -235,6 +251,7 @@ def main() -> None:
|
|
|
235
251
|
type=convert_to_value_spec_type(parsed.return_type_path),
|
|
236
252
|
description=return_description,
|
|
237
253
|
),
|
|
254
|
+
draft=draft,
|
|
238
255
|
)
|
|
239
256
|
)
|
|
240
257
|
where.pop()
|
|
@@ -25,10 +25,17 @@ TYPE_MAP = {
|
|
|
25
25
|
"List": MappedType(base_type=value_spec_t.BaseType.LIST, param_count=1),
|
|
26
26
|
"Optional": MappedType(base_type=value_spec_t.BaseType.OPTIONAL, param_count=1),
|
|
27
27
|
"String": MappedType(base_type=value_spec_t.BaseType.STRING),
|
|
28
|
-
"Union": MappedType(
|
|
28
|
+
"Union": MappedType(
|
|
29
|
+
base_type=value_spec_t.BaseType.UNION, variable_param_count=True
|
|
30
|
+
),
|
|
29
31
|
# not part of type_spec's types now
|
|
30
32
|
"Symbol": MappedType(base_type=value_spec_t.BaseType.SYMBOL),
|
|
31
33
|
"Any": MappedType(base_type=value_spec_t.BaseType.ANY),
|
|
34
|
+
"None": MappedType(base_type=value_spec_t.BaseType.NONE),
|
|
35
|
+
"Tuple": MappedType(
|
|
36
|
+
base_type=value_spec_t.BaseType.TUPLE, variable_param_count=True
|
|
37
|
+
),
|
|
38
|
+
"Never": MappedType(base_type=value_spec_t.BaseType.NEVER),
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
|
|
@@ -54,3 +61,16 @@ def convert_to_value_spec_type(parsed: ParsedTypePath) -> value_spec_t.ValueType
|
|
|
54
61
|
return value_spec_t.ValueType(base_type=mapped.base_type, parameters=parameters)
|
|
55
62
|
|
|
56
63
|
# Our formatter was duplicating the previous line for an unknown reason, this comment blocks that
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def convert_from_value_spec_type(
|
|
67
|
+
base_type: value_spec_t.BaseType,
|
|
68
|
+
) -> str:
|
|
69
|
+
for type_spec_type, mapped_type in TYPE_MAP.items():
|
|
70
|
+
if (
|
|
71
|
+
mapped_type.base_type == base_type
|
|
72
|
+
and mapped_type.param_count == 0
|
|
73
|
+
and mapped_type.variable_param_count is False
|
|
74
|
+
):
|
|
75
|
+
return type_spec_type
|
|
76
|
+
raise ValueError(f"invalid value spec type {base_type}")
|
|
@@ -70,14 +70,19 @@ def _emit_function_wrapper(function: value_spec_t.Function) -> str:
|
|
|
70
70
|
else:
|
|
71
71
|
python_type = _emit_python_type(argument.type)
|
|
72
72
|
if (
|
|
73
|
-
argument.
|
|
73
|
+
argument.on_null == value_spec_t.OnNull.PASS
|
|
74
74
|
or argument.extant == value_spec_t.ArgumentExtant.MISSING
|
|
75
75
|
):
|
|
76
76
|
python_type += " | None"
|
|
77
77
|
any_pass_null = True
|
|
78
|
+
|
|
79
|
+
if python_type.startswith("base_t.ExtJsonValue"):
|
|
80
|
+
return_statement = f"self._extract({index})"
|
|
81
|
+
else:
|
|
82
|
+
return_statement = f"cast({python_type}, self._extract({index}))"
|
|
78
83
|
out.write(
|
|
79
84
|
f"""{INDENT}def get_{argument.ref_name}(self) -> {python_type}:
|
|
80
|
-
{INDENT}{INDENT}return
|
|
85
|
+
{INDENT}{INDENT}return {return_statement}
|
|
81
86
|
"""
|
|
82
87
|
)
|
|
83
88
|
out.write("\n")
|
|
@@ -160,8 +165,12 @@ def _emit_function(function: value_spec_t.Function, indent: str) -> str:
|
|
|
160
165
|
sub_indent = indent + INDENT
|
|
161
166
|
out.write(f"{_function_symbol_name(function)} = value_spec_t.Function(\n")
|
|
162
167
|
out.write(f"{sub_indent}name={encode_common_string(function.name)},\n")
|
|
163
|
-
out.write(
|
|
168
|
+
out.write(
|
|
169
|
+
f"{sub_indent}description={encode_common_string(function.description)},\n"
|
|
170
|
+
)
|
|
164
171
|
out.write(f"{sub_indent}brief={encode_common_string(function.brief)},\n")
|
|
172
|
+
if function.draft:
|
|
173
|
+
out.write(f"{sub_indent}draft={function.draft},\n")
|
|
165
174
|
out.write(
|
|
166
175
|
f"{sub_indent}return_value={_emit_function_return(function.return_value, sub_indent)},\n"
|
|
167
176
|
)
|
|
@@ -184,16 +193,25 @@ def _emit_argument(argument: value_spec_t.FunctionArgument, indent: str) -> str:
|
|
|
184
193
|
out.write("value_spec_t.FunctionArgument(\n")
|
|
185
194
|
out.write(f"{sub_indent}ref_name={encode_common_string(argument.ref_name)},\n")
|
|
186
195
|
out.write(f"{sub_indent}name={encode_common_string(argument.name)},\n")
|
|
187
|
-
out.write(
|
|
188
|
-
|
|
189
|
-
|
|
196
|
+
out.write(
|
|
197
|
+
f"{sub_indent}description={encode_common_string(argument.description)},\n"
|
|
198
|
+
)
|
|
199
|
+
# Quick enum emit since we have only one such type here
|
|
200
|
+
out.write(
|
|
201
|
+
f"{sub_indent}on_null=value_spec_t.OnNull.{str(argument.on_null).upper()},\n"
|
|
202
|
+
)
|
|
203
|
+
out.write(
|
|
204
|
+
f"{sub_indent}extant=value_spec_t.ArgumentExtant.{argument.extant.name},\n"
|
|
205
|
+
)
|
|
190
206
|
out.write(f"{sub_indent}type={_emit_type(argument.type, sub_indent)},\n")
|
|
191
207
|
out.write(f"{indent})")
|
|
192
208
|
|
|
193
209
|
return out.getvalue()
|
|
194
210
|
|
|
195
211
|
|
|
196
|
-
def _emit_function_return(
|
|
212
|
+
def _emit_function_return(
|
|
213
|
+
return_value: value_spec_t.FunctionReturn, indent: str
|
|
214
|
+
) -> str:
|
|
197
215
|
out = io.StringIO()
|
|
198
216
|
|
|
199
217
|
sub_indent = indent + INDENT
|
uncountable/core/async_batch.py
CHANGED
uncountable/core/client.py
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import base64
|
|
2
|
-
import
|
|
2
|
+
import datetime
|
|
3
|
+
import re
|
|
3
4
|
import typing
|
|
4
5
|
from dataclasses import dataclass
|
|
5
|
-
from datetime import
|
|
6
|
+
from datetime import UTC, timedelta
|
|
6
7
|
from enum import StrEnum
|
|
7
|
-
from
|
|
8
|
+
from io import BytesIO
|
|
9
|
+
from urllib.parse import unquote, urljoin
|
|
8
10
|
from uuid import uuid4
|
|
9
11
|
|
|
10
12
|
import requests
|
|
13
|
+
import simplejson as json
|
|
11
14
|
from opentelemetry.sdk.resources import Attributes
|
|
12
15
|
from requests.exceptions import JSONDecodeError
|
|
13
16
|
|
|
14
17
|
from pkgs.argument_parser import CachedParser
|
|
15
|
-
from pkgs.serialization_util import serialize_for_api
|
|
16
|
-
from
|
|
17
|
-
from uncountable.
|
|
18
|
-
from uncountable.
|
|
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
|
|
19
22
|
from uncountable.types.client_base import APIRequest, ClientMethods
|
|
20
23
|
from uncountable.types.client_config import ClientConfigOptions
|
|
21
24
|
|
|
@@ -41,14 +44,14 @@ class HTTPRequestBase:
|
|
|
41
44
|
|
|
42
45
|
@dataclass(kw_only=True)
|
|
43
46
|
class HTTPGetRequest(HTTPRequestBase):
|
|
44
|
-
method = EndpointMethod.GET
|
|
47
|
+
method: EndpointMethod = EndpointMethod.GET
|
|
45
48
|
query_params: dict[str, str]
|
|
46
49
|
|
|
47
50
|
|
|
48
51
|
@dataclass(kw_only=True)
|
|
49
52
|
class HTTPPostRequest(HTTPRequestBase):
|
|
50
|
-
method = EndpointMethod.POST
|
|
51
|
-
body:
|
|
53
|
+
method: EndpointMethod = EndpointMethod.POST
|
|
54
|
+
body: str | dict[str, str]
|
|
52
55
|
|
|
53
56
|
|
|
54
57
|
HTTPRequest = HTTPPostRequest | HTTPGetRequest
|
|
@@ -56,29 +59,39 @@ HTTPRequest = HTTPPostRequest | HTTPGetRequest
|
|
|
56
59
|
|
|
57
60
|
@dataclass(kw_only=True)
|
|
58
61
|
class ClientConfig(ClientConfigOptions):
|
|
59
|
-
transform_request: typing.Callable[[requests.Request], requests.Request] | None =
|
|
60
|
-
|
|
62
|
+
transform_request: typing.Callable[[requests.Request], requests.Request] | None = (
|
|
63
|
+
None
|
|
64
|
+
)
|
|
65
|
+
logger: Logger | None = None
|
|
61
66
|
|
|
62
67
|
|
|
63
68
|
OAUTH_REFRESH_WINDOW_SECONDS = 60 * 5
|
|
64
69
|
|
|
65
70
|
|
|
66
|
-
class APIResponseError(
|
|
71
|
+
class APIResponseError(Exception):
|
|
67
72
|
status_code: int
|
|
68
73
|
message: str
|
|
69
74
|
extra_details: dict[str, JsonValue] | None
|
|
70
75
|
|
|
71
76
|
def __init__(
|
|
72
|
-
self,
|
|
77
|
+
self,
|
|
78
|
+
status_code: int,
|
|
79
|
+
message: str,
|
|
80
|
+
extra_details: dict[str, JsonValue] | None,
|
|
81
|
+
request_id: str,
|
|
73
82
|
) -> None:
|
|
74
83
|
super().__init__(status_code, message, extra_details)
|
|
75
84
|
self.status_code = status_code
|
|
76
85
|
self.message = message
|
|
77
86
|
self.extra_details = extra_details
|
|
87
|
+
self.request_id = request_id
|
|
78
88
|
|
|
79
89
|
@classmethod
|
|
80
90
|
def construct_error(
|
|
81
|
-
cls,
|
|
91
|
+
cls,
|
|
92
|
+
status_code: int,
|
|
93
|
+
extra_details: dict[str, JsonValue] | None,
|
|
94
|
+
request_id: str,
|
|
82
95
|
) -> "APIResponseError":
|
|
83
96
|
message: str
|
|
84
97
|
match status_code:
|
|
@@ -101,11 +114,23 @@ class APIResponseError(BaseException):
|
|
|
101
114
|
case _:
|
|
102
115
|
message = "unknown error"
|
|
103
116
|
return APIResponseError(
|
|
104
|
-
status_code=status_code,
|
|
117
|
+
status_code=status_code,
|
|
118
|
+
message=message,
|
|
119
|
+
extra_details=extra_details,
|
|
120
|
+
request_id=request_id,
|
|
105
121
|
)
|
|
106
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}"
|
|
107
131
|
|
|
108
|
-
|
|
132
|
+
|
|
133
|
+
class SDKError(Exception):
|
|
109
134
|
message: str
|
|
110
135
|
request_id: str
|
|
111
136
|
|
|
@@ -121,7 +146,7 @@ class SDKError(BaseException):
|
|
|
121
146
|
@dataclass(kw_only=True)
|
|
122
147
|
class OAuthBearerTokenCache:
|
|
123
148
|
token: str
|
|
124
|
-
expires_at: datetime
|
|
149
|
+
expires_at: datetime.datetime
|
|
125
150
|
|
|
126
151
|
|
|
127
152
|
@dataclass(kw_only=True)
|
|
@@ -135,6 +160,16 @@ class GetOauthBearerTokenData:
|
|
|
135
160
|
oauth_bearer_token_data_parser = CachedParser(GetOauthBearerTokenData)
|
|
136
161
|
|
|
137
162
|
|
|
163
|
+
@dataclass
|
|
164
|
+
class DownloadedFile:
|
|
165
|
+
name: str
|
|
166
|
+
size: int
|
|
167
|
+
data: BytesIO
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
DownloadedFiles = list[DownloadedFile]
|
|
171
|
+
|
|
172
|
+
|
|
138
173
|
class Client(ClientMethods):
|
|
139
174
|
_parser_map: dict[type, CachedParser] = {}
|
|
140
175
|
_auth_details: AuthDetailsAll
|
|
@@ -153,37 +188,53 @@ class Client(ClientMethods):
|
|
|
153
188
|
):
|
|
154
189
|
self._auth_details = auth_details
|
|
155
190
|
self._base_url = base_url
|
|
156
|
-
self._file_uploader = FileUploader(self._base_url, self._auth_details)
|
|
157
191
|
self._cfg = config or ClientConfig()
|
|
158
192
|
self._session = requests.Session()
|
|
159
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
|
+
)
|
|
160
200
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
201
|
+
@classmethod
|
|
202
|
+
def _validate_response_status(
|
|
203
|
+
cls, response: requests.Response, request_id: str
|
|
204
|
+
) -> None:
|
|
164
205
|
if response.status_code < 200 or response.status_code > 299:
|
|
165
206
|
extra_details: dict[str, JsonValue] | None = None
|
|
166
207
|
try:
|
|
167
208
|
data = response.json()
|
|
168
|
-
|
|
169
|
-
extra_details = data
|
|
209
|
+
extra_details = data
|
|
170
210
|
except JSONDecodeError:
|
|
171
|
-
|
|
211
|
+
extra_details = {
|
|
212
|
+
"body": response.text,
|
|
213
|
+
}
|
|
172
214
|
raise APIResponseError.construct_error(
|
|
173
|
-
status_code=response.status_code,
|
|
215
|
+
status_code=response.status_code,
|
|
216
|
+
extra_details=extra_details,
|
|
217
|
+
request_id=request_id,
|
|
174
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)
|
|
175
224
|
try:
|
|
176
225
|
return typing.cast(dict[str, JsonValue], response.json())
|
|
177
226
|
except JSONDecodeError as e:
|
|
178
227
|
raise SDKError("unable to process response", request_id=request_id) from e
|
|
179
228
|
|
|
180
|
-
def _send_request(
|
|
229
|
+
def _send_request(
|
|
230
|
+
self, request: requests.Request, *, timeout: float | None = None
|
|
231
|
+
) -> requests.Response:
|
|
181
232
|
if self._cfg.extra_headers is not None:
|
|
182
233
|
request.headers = {**request.headers, **self._cfg.extra_headers}
|
|
183
234
|
if self._cfg.transform_request is not None:
|
|
184
235
|
request = self._cfg.transform_request(request)
|
|
185
236
|
prepared_request = request.prepare()
|
|
186
|
-
response = self._session.send(prepared_request)
|
|
237
|
+
response = self._session.send(prepared_request, timeout=timeout)
|
|
187
238
|
return response
|
|
188
239
|
|
|
189
240
|
def do_request(self, *, api_request: APIRequest, return_type: type[DT]) -> DT:
|
|
@@ -201,15 +252,19 @@ class Client(ClientMethods):
|
|
|
201
252
|
case _:
|
|
202
253
|
typing.assert_never(http_request)
|
|
203
254
|
request.headers = http_request.headers
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
266
|
+
)
|
|
267
|
+
response = self._send_request(request, timeout=timeout)
|
|
213
268
|
response_data = self._get_response_json(response, request_id=request_id)
|
|
214
269
|
cached_parser = self._get_cached_parser(return_type)
|
|
215
270
|
try:
|
|
@@ -227,7 +282,8 @@ class Client(ClientMethods):
|
|
|
227
282
|
if (
|
|
228
283
|
self._oauth_bearer_token_cache is None
|
|
229
284
|
or (
|
|
230
|
-
self._oauth_bearer_token_cache.expires_at
|
|
285
|
+
self._oauth_bearer_token_cache.expires_at
|
|
286
|
+
- datetime.datetime.now(tz=UTC)
|
|
231
287
|
).total_seconds()
|
|
232
288
|
< OAUTH_REFRESH_WINDOW_SECONDS
|
|
233
289
|
):
|
|
@@ -243,7 +299,8 @@ class Client(ClientMethods):
|
|
|
243
299
|
token_data = oauth_bearer_token_data_parser.parse_storage(data)
|
|
244
300
|
self._oauth_bearer_token_cache = OAuthBearerTokenCache(
|
|
245
301
|
token=token_data.access_token,
|
|
246
|
-
expires_at=datetime.now(
|
|
302
|
+
expires_at=datetime.datetime.now(tz=UTC)
|
|
303
|
+
+ timedelta(seconds=token_data.expires_in),
|
|
247
304
|
)
|
|
248
305
|
|
|
249
306
|
return self._oauth_bearer_token_cache.token
|
|
@@ -286,6 +343,52 @@ class Client(ClientMethods):
|
|
|
286
343
|
case _:
|
|
287
344
|
raise ValueError(f"unsupported request method: {method}")
|
|
288
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
|
+
|
|
289
392
|
def upload_files(
|
|
290
393
|
self: typing.Self, *, file_uploads: list[FileUpload]
|
|
291
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,11 +4,13 @@ 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 uncountable.integration.telemetry import Logger, push_scope_optional
|
|
13
|
+
|
|
12
14
|
from .types import AuthDetailsAll, AuthDetailsApiKey
|
|
13
15
|
|
|
14
16
|
_CHUNK_SIZE = 5 * 1024 * 1024 # s3 requires 5MiB minimum
|
|
@@ -68,10 +70,19 @@ class UploadFailed(Exception):
|
|
|
68
70
|
class FileUploader:
|
|
69
71
|
_auth_details: AuthDetailsAll
|
|
70
72
|
_base_url: str
|
|
71
|
-
|
|
72
|
-
|
|
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:
|
|
73
82
|
self._base_url = base_url
|
|
74
83
|
self._auth_details = auth_details
|
|
84
|
+
self._allow_insecure_tls = allow_insecure_tls
|
|
85
|
+
self._logger = logger
|
|
75
86
|
|
|
76
87
|
async def _upload_file(self: Self, file_upload: FileUpload) -> UploadedFile:
|
|
77
88
|
creation_url = f"{self._base_url}/api/external/file_upload/files"
|
|
@@ -86,21 +97,44 @@ class FileUploader:
|
|
|
86
97
|
auth=auth, headers={"Origin": self._base_url}
|
|
87
98
|
) as session,
|
|
88
99
|
):
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
)
|
|
134
|
+
|
|
135
|
+
def upload_files(
|
|
136
|
+
self: Self, *, file_uploads: list[FileUpload]
|
|
137
|
+
) -> list[UploadedFile]:
|
|
104
138
|
return [
|
|
105
139
|
asyncio.run(self._upload_file(file_upload)) for file_upload in file_uploads
|
|
106
140
|
]
|