UncountablePythonSDK 0.0.8__py3-none-any.whl → 0.0.92__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.
- UncountablePythonSDK-0.0.92.dist-info/METADATA +61 -0
- UncountablePythonSDK-0.0.92.dist-info/RECORD +301 -0
- {UncountablePythonSDK-0.0.8.dist-info → UncountablePythonSDK-0.0.92.dist-info}/WHEEL +1 -1
- {UncountablePythonSDK-0.0.8.dist-info → UncountablePythonSDK-0.0.92.dist-info}/top_level.txt +1 -1
- docs/.gitignore +1 -0
- docs/conf.py +57 -0
- docs/index.md +13 -0
- docs/justfile +12 -0
- docs/quickstart.md +19 -0
- docs/requirements.txt +7 -0
- docs/static/favicons/android-chrome-192x192.png +0 -0
- docs/static/favicons/android-chrome-512x512.png +0 -0
- docs/static/favicons/apple-touch-icon.png +0 -0
- docs/static/favicons/browserconfig.xml +9 -0
- docs/static/favicons/favicon-16x16.png +0 -0
- docs/static/favicons/favicon-32x32.png +0 -0
- docs/static/favicons/manifest.json +18 -0
- docs/static/favicons/mstile-150x150.png +0 -0
- docs/static/favicons/safari-pinned-tab.svg +32 -0
- docs/static/logo_blue.png +0 -0
- examples/async_batch.py +35 -0
- examples/create_entity.py +22 -17
- examples/download_files.py +26 -0
- examples/edit_recipe_inputs.py +50 -0
- examples/integration-server/jobs/materials_auto/example_cron.py +18 -0
- examples/integration-server/jobs/materials_auto/example_wh.py +15 -0
- examples/integration-server/jobs/materials_auto/profile.yaml +43 -0
- examples/integration-server/pyproject.toml +224 -0
- examples/invoke_uploader.py +26 -0
- examples/set_recipe_metadata_file.py +40 -0
- examples/set_recipe_output_file_sdk.py +26 -0
- examples/upload_files.py +18 -0
- pkgs/argument_parser/__init__.py +5 -0
- pkgs/argument_parser/_is_enum.py +1 -6
- pkgs/argument_parser/argument_parser.py +232 -76
- pkgs/argument_parser/case_convert.py +4 -3
- pkgs/filesystem_utils/__init__.py +20 -0
- pkgs/filesystem_utils/_blob_session.py +137 -0
- pkgs/filesystem_utils/_gdrive_session.py +309 -0
- pkgs/filesystem_utils/_local_session.py +69 -0
- pkgs/filesystem_utils/_s3_session.py +117 -0
- pkgs/filesystem_utils/_sftp_session.py +147 -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 +65 -50
- 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/_get_type_for_serialization.py +1 -3
- pkgs/serialization_util/convert_to_snakecase.py +27 -0
- pkgs/serialization_util/dataclasses.py +14 -0
- pkgs/serialization_util/serialization_helpers.py +116 -74
- pkgs/strenum_compat/strenum_compat.py +1 -9
- pkgs/type_spec/actions_registry/__init__.py +0 -0
- pkgs/type_spec/actions_registry/__main__.py +126 -0
- pkgs/type_spec/actions_registry/emit_typescript.py +182 -0
- pkgs/type_spec/builder.py +475 -89
- pkgs/type_spec/config.py +24 -19
- pkgs/type_spec/emit_io_ts.py +5 -2
- pkgs/type_spec/emit_open_api.py +266 -32
- pkgs/type_spec/emit_open_api_util.py +32 -13
- pkgs/type_spec/emit_python.py +599 -151
- pkgs/type_spec/emit_typescript.py +74 -273
- pkgs/type_spec/emit_typescript_util.py +239 -5
- pkgs/type_spec/load_types.py +55 -10
- pkgs/type_spec/open_api_util.py +30 -41
- pkgs/type_spec/parts/base.py.prepart +4 -3
- pkgs/type_spec/type_info/emit_type_info.py +178 -16
- pkgs/type_spec/util.py +11 -11
- pkgs/type_spec/value_spec/__main__.py +3 -3
- pkgs/type_spec/value_spec/convert_type.py +8 -1
- pkgs/type_spec/value_spec/emit_python.py +13 -4
- uncountable/__init__.py +1 -2
- uncountable/core/__init__.py +12 -2
- uncountable/core/async_batch.py +37 -0
- uncountable/core/client.py +293 -43
- uncountable/core/environment.py +41 -0
- uncountable/core/file_upload.py +135 -0
- uncountable/core/types.py +17 -0
- uncountable/integration/__init__.py +0 -0
- uncountable/integration/cli.py +49 -0
- uncountable/integration/construct_client.py +51 -0
- uncountable/integration/cron.py +29 -0
- uncountable/integration/db/__init__.py +0 -0
- uncountable/integration/db/connect.py +18 -0
- uncountable/integration/db/session.py +25 -0
- uncountable/integration/entrypoint.py +13 -0
- uncountable/integration/executors/__init__.py +0 -0
- uncountable/integration/executors/executors.py +148 -0
- uncountable/integration/executors/generic_upload_executor.py +284 -0
- uncountable/integration/executors/script_executor.py +25 -0
- uncountable/integration/job.py +87 -0
- uncountable/integration/queue_runner/__init__.py +0 -0
- uncountable/integration/queue_runner/command_server/__init__.py +24 -0
- uncountable/integration/queue_runner/command_server/command_client.py +68 -0
- uncountable/integration/queue_runner/command_server/command_server.py +64 -0
- uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server.proto +22 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +40 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +38 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +129 -0
- uncountable/integration/queue_runner/command_server/types.py +52 -0
- uncountable/integration/queue_runner/datastore/__init__.py +3 -0
- uncountable/integration/queue_runner/datastore/datastore_sqlite.py +93 -0
- uncountable/integration/queue_runner/datastore/interface.py +19 -0
- uncountable/integration/queue_runner/datastore/model.py +17 -0
- uncountable/integration/queue_runner/job_scheduler.py +163 -0
- uncountable/integration/queue_runner/queue_runner.py +26 -0
- uncountable/integration/queue_runner/types.py +7 -0
- uncountable/integration/queue_runner/worker.py +119 -0
- uncountable/integration/scan_profiles.py +67 -0
- uncountable/integration/scheduler.py +150 -0
- uncountable/integration/secret_retrieval/__init__.py +3 -0
- uncountable/integration/secret_retrieval/retrieve_secret.py +93 -0
- uncountable/integration/server.py +117 -0
- uncountable/integration/telemetry.py +209 -0
- uncountable/integration/webhook_server/entrypoint.py +170 -0
- uncountable/types/__init__.py +136 -20
- uncountable/types/api/batch/execute_batch.py +15 -7
- uncountable/types/api/batch/execute_batch_load_async.py +42 -0
- uncountable/types/api/chemical/__init__.py +1 -0
- uncountable/types/api/chemical/convert_chemical_formats.py +63 -0
- uncountable/types/api/entity/create_entities.py +23 -11
- uncountable/types/api/entity/create_entity.py +21 -12
- uncountable/types/api/entity/get_entities_data.py +18 -8
- uncountable/types/api/entity/grant_entity_permissions.py +48 -0
- uncountable/types/api/entity/list_entities.py +27 -12
- uncountable/types/api/entity/lock_entity.py +45 -0
- uncountable/types/api/entity/resolve_entity_ids.py +17 -7
- uncountable/types/api/entity/set_entity_field_values.py +44 -0
- uncountable/types/api/entity/set_values.py +14 -7
- uncountable/types/api/entity/transition_entity_phase.py +80 -0
- 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 +44 -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/__init__.py +1 -0
- uncountable/types/api/id_source/list_id_source.py +56 -0
- uncountable/types/api/id_source/match_id_source.py +54 -0
- uncountable/types/api/input_groups/get_input_group_names.py +16 -6
- uncountable/types/api/inputs/create_inputs.py +24 -11
- uncountable/types/api/inputs/get_input_data.py +32 -13
- uncountable/types/api/inputs/get_input_names.py +18 -8
- uncountable/types/api/inputs/get_inputs_data.py +29 -10
- uncountable/types/api/inputs/set_input_attribute_values.py +16 -9
- 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/material_families/__init__.py +1 -0
- uncountable/types/api/material_families/update_entity_material_families.py +48 -0
- uncountable/types/api/outputs/get_output_data.py +32 -16
- uncountable/types/api/outputs/get_output_names.py +18 -8
- uncountable/types/api/outputs/resolve_output_conditions.py +23 -10
- uncountable/types/api/permissions/__init__.py +1 -0
- uncountable/types/api/permissions/set_core_permissions.py +105 -0
- uncountable/types/api/project/get_projects.py +17 -7
- uncountable/types/api/project/get_projects_data.py +21 -11
- uncountable/types/api/recipe_links/__init__.py +1 -0
- uncountable/types/api/recipe_links/create_recipe_link.py +46 -0
- uncountable/types/api/recipe_links/remove_recipe_link.py +45 -0
- uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +18 -8
- uncountable/types/api/recipes/add_recipe_to_project.py +42 -0
- uncountable/types/api/recipes/archive_recipes.py +42 -0
- uncountable/types/api/recipes/associate_recipe_as_input.py +44 -0
- uncountable/types/api/recipes/associate_recipe_as_lot.py +43 -0
- uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
- uncountable/types/api/recipes/create_recipe.py +51 -0
- uncountable/types/api/recipes/create_recipes.py +25 -12
- uncountable/types/api/recipes/disassociate_recipe_as_input.py +42 -0
- uncountable/types/api/recipes/edit_recipe_inputs.py +283 -0
- uncountable/types/api/recipes/get_column_calculation_values.py +58 -0
- uncountable/types/api/recipes/get_curve.py +15 -7
- uncountable/types/api/recipes/get_recipe_calculations.py +17 -10
- uncountable/types/api/recipes/get_recipe_links.py +13 -6
- uncountable/types/api/recipes/get_recipe_names.py +16 -6
- uncountable/types/api/recipes/get_recipe_output_metadata.py +14 -7
- uncountable/types/api/recipes/get_recipes_data.py +63 -38
- uncountable/types/api/recipes/lock_recipes.py +63 -0
- uncountable/types/api/recipes/remove_recipe_from_project.py +42 -0
- uncountable/types/api/recipes/set_recipe_inputs.py +19 -10
- uncountable/types/api/recipes/set_recipe_metadata.py +43 -0
- uncountable/types/api/recipes/set_recipe_output_annotations.py +115 -0
- uncountable/types/api/recipes/set_recipe_output_file.py +56 -0
- uncountable/types/api/recipes/set_recipe_outputs.py +26 -12
- uncountable/types/api/recipes/set_recipe_tags.py +109 -0
- uncountable/types/api/recipes/unarchive_recipes.py +41 -0
- uncountable/types/api/recipes/unlock_recipes.py +50 -0
- uncountable/types/api/triggers/__init__.py +1 -0
- uncountable/types/api/triggers/run_trigger.py +43 -0
- uncountable/types/api/uploader/__init__.py +1 -0
- uncountable/types/api/uploader/invoke_uploader.py +47 -0
- uncountable/types/async_batch.py +13 -0
- uncountable/types/async_batch_processor.py +384 -0
- uncountable/types/async_batch_t.py +97 -0
- uncountable/types/async_jobs.py +9 -0
- uncountable/types/async_jobs_t.py +53 -0
- uncountable/types/auth_retrieval.py +12 -0
- uncountable/types/auth_retrieval_t.py +75 -0
- uncountable/types/base.py +5 -78
- uncountable/types/base_t.py +85 -0
- uncountable/types/calculations.py +3 -18
- uncountable/types/calculations_t.py +27 -0
- uncountable/types/chemical_structure.py +8 -0
- uncountable/types/chemical_structure_t.py +28 -0
- uncountable/types/client_base.py +1093 -54
- uncountable/types/client_config.py +8 -0
- uncountable/types/client_config_t.py +26 -0
- uncountable/types/curves.py +5 -42
- uncountable/types/curves_t.py +51 -0
- uncountable/types/entity.py +8 -269
- uncountable/types/entity_t.py +393 -0
- uncountable/types/experiment_groups.py +3 -18
- uncountable/types/experiment_groups_t.py +27 -0
- uncountable/types/field_values.py +17 -60
- uncountable/types/field_values_t.py +204 -0
- uncountable/types/fields.py +3 -19
- uncountable/types/fields_t.py +28 -0
- uncountable/types/generic_upload.py +15 -0
- uncountable/types/generic_upload_t.py +119 -0
- uncountable/types/id_source.py +12 -0
- uncountable/types/id_source_t.py +68 -0
- uncountable/types/identifier.py +11 -0
- uncountable/types/identifier_t.py +63 -0
- uncountable/types/input_attributes.py +3 -24
- uncountable/types/input_attributes_t.py +30 -0
- uncountable/types/inputs.py +6 -56
- uncountable/types/inputs_t.py +83 -0
- uncountable/types/integration_server.py +9 -0
- uncountable/types/integration_server_t.py +42 -0
- uncountable/types/job_definition.py +27 -0
- uncountable/types/job_definition_t.py +260 -0
- uncountable/types/outputs.py +3 -21
- uncountable/types/outputs_t.py +30 -0
- uncountable/types/overrides.py +10 -0
- uncountable/types/overrides_t.py +49 -0
- uncountable/types/permissions.py +8 -0
- uncountable/types/permissions_t.py +46 -0
- uncountable/types/phases.py +3 -18
- uncountable/types/phases_t.py +27 -0
- uncountable/types/post_base.py +8 -0
- uncountable/types/post_base_t.py +30 -0
- uncountable/types/queued_job.py +16 -0
- uncountable/types/queued_job_t.py +123 -0
- uncountable/types/recipe_identifiers.py +12 -0
- uncountable/types/recipe_identifiers_t.py +76 -0
- uncountable/types/recipe_inputs.py +9 -0
- uncountable/types/recipe_inputs_t.py +30 -0
- uncountable/types/recipe_links.py +4 -45
- uncountable/types/recipe_links_t.py +54 -0
- uncountable/types/recipe_metadata.py +5 -45
- uncountable/types/recipe_metadata_t.py +58 -0
- uncountable/types/recipe_output_metadata.py +3 -19
- uncountable/types/recipe_output_metadata_t.py +28 -0
- uncountable/types/recipe_tags.py +3 -18
- uncountable/types/recipe_tags_t.py +27 -0
- uncountable/types/recipe_workflow_steps.py +14 -0
- uncountable/types/recipe_workflow_steps_t.py +95 -0
- uncountable/types/recipes.py +8 -0
- uncountable/types/recipes_t.py +25 -0
- uncountable/types/response.py +3 -20
- uncountable/types/response_t.py +26 -0
- uncountable/types/secret_retrieval.py +12 -0
- uncountable/types/secret_retrieval_t.py +75 -0
- uncountable/types/units.py +3 -18
- uncountable/types/units_t.py +27 -0
- uncountable/types/users.py +3 -19
- uncountable/types/users_t.py +28 -0
- uncountable/types/webhook_job.py +9 -0
- uncountable/types/webhook_job_t.py +37 -0
- uncountable/types/workflows.py +4 -27
- uncountable/types/workflows_t.py +39 -0
- UncountablePythonSDK-0.0.8.dist-info/METADATA +0 -27
- UncountablePythonSDK-0.0.8.dist-info/RECORD +0 -134
- examples/recipe-import/importer.py +0 -39
- type_spec/external/api/batch/execute_batch.yaml +0 -56
- type_spec/external/api/entity/create_entities.yaml +0 -33
- type_spec/external/api/entity/create_entity.yaml +0 -39
- 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/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/project/get_projects.yaml +0 -42
- type_spec/external/api/project/get_projects_data.yaml +0 -50
- type_spec/external/api/recipe_metadata/get_recipe_metadata_data.yaml +0 -41
- type_spec/external/api/recipes/create_recipes.yaml +0 -47
- type_spec/external/api/recipes/get_curve.yaml +0 -18
- 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 -238
- type_spec/external/api/recipes/set_recipe_inputs.yaml +0 -36
- type_spec/external/api/recipes/set_recipe_outputs.yaml +0 -52
uncountable/core/client.py
CHANGED
|
@@ -1,17 +1,32 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from enum import StrEnum
|
|
3
|
-
import json
|
|
4
|
-
from urllib.parse import urljoin
|
|
5
1
|
import base64
|
|
2
|
+
import re
|
|
6
3
|
import typing
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from io import BytesIO
|
|
8
|
+
from urllib.parse import unquote, urljoin
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
7
11
|
import requests
|
|
8
|
-
|
|
9
|
-
from
|
|
12
|
+
import simplejson as json
|
|
13
|
+
from opentelemetry.sdk.resources import Attributes
|
|
14
|
+
from requests.exceptions import JSONDecodeError
|
|
15
|
+
|
|
10
16
|
from pkgs.argument_parser import CachedParser
|
|
11
|
-
from pkgs.serialization_util import serialize_for_api
|
|
17
|
+
from pkgs.serialization_util import JsonValue, serialize_for_api
|
|
18
|
+
from uncountable.core.environment import get_version
|
|
19
|
+
from uncountable.integration.telemetry import Logger, push_scope_optional
|
|
20
|
+
from uncountable.types import download_file_t
|
|
21
|
+
from uncountable.types.client_base import APIRequest, ClientMethods
|
|
22
|
+
from uncountable.types.client_config import ClientConfigOptions
|
|
12
23
|
|
|
24
|
+
from .file_upload import FileUpload, FileUploader, UploadedFile
|
|
25
|
+
from .types import AuthDetailsAll, AuthDetailsApiKey, AuthDetailsOAuth
|
|
13
26
|
|
|
14
27
|
DT = typing.TypeVar("DT")
|
|
28
|
+
UNC_REQUEST_ID_HEADER = "X-UNC-REQUEST-ID"
|
|
29
|
+
UNC_SDK_VERSION_HEADER = "X-UNC-SDK-VERSION"
|
|
15
30
|
|
|
16
31
|
|
|
17
32
|
class EndpointMethod(StrEnum):
|
|
@@ -24,105 +39,340 @@ class HTTPRequestBase:
|
|
|
24
39
|
method: EndpointMethod
|
|
25
40
|
url: str
|
|
26
41
|
headers: dict[str, str]
|
|
27
|
-
body: typing.Optional[str] = None
|
|
28
|
-
query_params: typing.Optional[dict[str, str]]
|
|
29
42
|
|
|
30
43
|
|
|
31
44
|
@dataclass(kw_only=True)
|
|
32
45
|
class HTTPGetRequest(HTTPRequestBase):
|
|
33
|
-
method
|
|
46
|
+
method = EndpointMethod.GET
|
|
34
47
|
query_params: dict[str, str]
|
|
35
48
|
|
|
36
49
|
|
|
37
50
|
@dataclass(kw_only=True)
|
|
38
51
|
class HTTPPostRequest(HTTPRequestBase):
|
|
39
|
-
method
|
|
52
|
+
method = EndpointMethod.POST
|
|
53
|
+
body: typing.Union[str, dict[str, str]]
|
|
40
54
|
|
|
41
55
|
|
|
42
56
|
HTTPRequest = HTTPPostRequest | HTTPGetRequest
|
|
43
57
|
|
|
44
58
|
|
|
45
59
|
@dataclass(kw_only=True)
|
|
46
|
-
class
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
class ClientConfig(ClientConfigOptions):
|
|
61
|
+
transform_request: typing.Callable[[requests.Request], requests.Request] | None = (
|
|
62
|
+
None
|
|
63
|
+
)
|
|
64
|
+
logger: typing.Optional[Logger] = None
|
|
65
|
+
|
|
49
66
|
|
|
67
|
+
OAUTH_REFRESH_WINDOW_SECONDS = 60 * 5
|
|
50
68
|
|
|
51
|
-
|
|
69
|
+
|
|
70
|
+
class APIResponseError(Exception):
|
|
71
|
+
status_code: int
|
|
72
|
+
message: str
|
|
73
|
+
extra_details: dict[str, JsonValue] | None
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
status_code: int,
|
|
78
|
+
message: str,
|
|
79
|
+
extra_details: dict[str, JsonValue] | None,
|
|
80
|
+
request_id: str,
|
|
81
|
+
) -> None:
|
|
82
|
+
super().__init__(status_code, message, extra_details)
|
|
83
|
+
self.status_code = status_code
|
|
84
|
+
self.message = message
|
|
85
|
+
self.extra_details = extra_details
|
|
86
|
+
self.request_id = request_id
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def construct_error(
|
|
90
|
+
cls,
|
|
91
|
+
status_code: int,
|
|
92
|
+
extra_details: dict[str, JsonValue] | None,
|
|
93
|
+
request_id: str,
|
|
94
|
+
) -> "APIResponseError":
|
|
95
|
+
message: str
|
|
96
|
+
match status_code:
|
|
97
|
+
case 403:
|
|
98
|
+
message = "unexpected: unauthorized"
|
|
99
|
+
case 410:
|
|
100
|
+
message = "unexpected: not found"
|
|
101
|
+
case 400:
|
|
102
|
+
message = "unexpected: bad arguments"
|
|
103
|
+
case 501:
|
|
104
|
+
message = "unexpected: unimplemented"
|
|
105
|
+
case 504:
|
|
106
|
+
message = "unexpected: timeout"
|
|
107
|
+
case 404:
|
|
108
|
+
message = "not found"
|
|
109
|
+
case 409:
|
|
110
|
+
message = "bad arguments"
|
|
111
|
+
case 422:
|
|
112
|
+
message = "unprocessable"
|
|
113
|
+
case _:
|
|
114
|
+
message = "unknown error"
|
|
115
|
+
return APIResponseError(
|
|
116
|
+
status_code=status_code,
|
|
117
|
+
message=message,
|
|
118
|
+
extra_details=extra_details,
|
|
119
|
+
request_id=request_id,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def __str__(self) -> str:
|
|
123
|
+
details_obj = {
|
|
124
|
+
"request_id": self.request_id,
|
|
125
|
+
"status_code": self.status_code,
|
|
126
|
+
"extra_details": self.extra_details,
|
|
127
|
+
}
|
|
128
|
+
details = json.dumps(details_obj)
|
|
129
|
+
return f"API response error ({self.status_code}): '{self.message}'. Details: {details}"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class SDKError(Exception):
|
|
133
|
+
message: str
|
|
134
|
+
request_id: str
|
|
135
|
+
|
|
136
|
+
def __init__(self, message: str, *, request_id: str) -> None:
|
|
137
|
+
super().__init__(message)
|
|
138
|
+
self.message = message
|
|
139
|
+
self.request_id = request_id
|
|
140
|
+
|
|
141
|
+
def __str__(self) -> str:
|
|
142
|
+
return f"internal SDK error (request id {self.request_id}), please contact Uncountable support: {self.message}"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(kw_only=True)
|
|
146
|
+
class OAuthBearerTokenCache:
|
|
147
|
+
token: str
|
|
148
|
+
expires_at: datetime
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(kw_only=True)
|
|
152
|
+
class GetOauthBearerTokenData:
|
|
153
|
+
access_token: str
|
|
154
|
+
expires_in: int
|
|
155
|
+
token_type: str
|
|
156
|
+
scope: str
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
oauth_bearer_token_data_parser = CachedParser(GetOauthBearerTokenData)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class DownloadedFile:
|
|
164
|
+
name: str
|
|
165
|
+
size: int
|
|
166
|
+
data: BytesIO
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
DownloadedFiles = list[DownloadedFile]
|
|
52
170
|
|
|
53
171
|
|
|
54
172
|
class Client(ClientMethods):
|
|
55
173
|
_parser_map: dict[type, CachedParser] = {}
|
|
56
|
-
_auth_details:
|
|
174
|
+
_auth_details: AuthDetailsAll
|
|
57
175
|
_base_url: str
|
|
176
|
+
_file_uploader: FileUploader
|
|
177
|
+
_cfg: ClientConfig
|
|
178
|
+
_oauth_bearer_token_cache: OAuthBearerTokenCache | None = None
|
|
179
|
+
_session: requests.Session
|
|
58
180
|
|
|
59
|
-
def __init__(
|
|
181
|
+
def __init__(
|
|
182
|
+
self,
|
|
183
|
+
*,
|
|
184
|
+
base_url: str,
|
|
185
|
+
auth_details: AuthDetailsAll,
|
|
186
|
+
config: ClientConfig | None = None,
|
|
187
|
+
):
|
|
60
188
|
self._auth_details = auth_details
|
|
61
189
|
self._base_url = base_url
|
|
190
|
+
self._cfg = config or ClientConfig()
|
|
191
|
+
self._session = requests.Session()
|
|
192
|
+
self._session.verify = not self._cfg.allow_insecure_tls
|
|
193
|
+
self._file_uploader = FileUploader(
|
|
194
|
+
self._base_url,
|
|
195
|
+
self._auth_details,
|
|
196
|
+
self._cfg.allow_insecure_tls,
|
|
197
|
+
logger=self._cfg.logger,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _get_response_json(
|
|
201
|
+
self, response: requests.Response, request_id: str
|
|
202
|
+
) -> dict[str, JsonValue]:
|
|
203
|
+
if response.status_code < 200 or response.status_code > 299:
|
|
204
|
+
extra_details: dict[str, JsonValue] | None = None
|
|
205
|
+
try:
|
|
206
|
+
data = response.json()
|
|
207
|
+
extra_details = data
|
|
208
|
+
except JSONDecodeError:
|
|
209
|
+
extra_details = {
|
|
210
|
+
"body": response.text,
|
|
211
|
+
}
|
|
212
|
+
raise APIResponseError.construct_error(
|
|
213
|
+
status_code=response.status_code,
|
|
214
|
+
extra_details=extra_details,
|
|
215
|
+
request_id=request_id,
|
|
216
|
+
)
|
|
217
|
+
try:
|
|
218
|
+
return typing.cast(dict[str, JsonValue], response.json())
|
|
219
|
+
except JSONDecodeError as e:
|
|
220
|
+
raise SDKError("unable to process response", request_id=request_id) from e
|
|
221
|
+
|
|
222
|
+
def _send_request(self, request: requests.Request) -> requests.Response:
|
|
223
|
+
if self._cfg.extra_headers is not None:
|
|
224
|
+
request.headers = {**request.headers, **self._cfg.extra_headers}
|
|
225
|
+
if self._cfg.transform_request is not None:
|
|
226
|
+
request = self._cfg.transform_request(request)
|
|
227
|
+
prepared_request = request.prepare()
|
|
228
|
+
response = self._session.send(prepared_request)
|
|
229
|
+
return response
|
|
62
230
|
|
|
63
231
|
def do_request(self, *, api_request: APIRequest, return_type: type[DT]) -> DT:
|
|
64
|
-
|
|
232
|
+
request_id = str(uuid4())
|
|
233
|
+
http_request = self._build_http_request(
|
|
234
|
+
api_request=api_request, request_id=request_id
|
|
235
|
+
)
|
|
65
236
|
match http_request:
|
|
66
237
|
case HTTPGetRequest():
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
headers=http_request.headers,
|
|
70
|
-
params=http_request.query_params,
|
|
71
|
-
)
|
|
238
|
+
request = requests.Request("GET", http_request.url)
|
|
239
|
+
request.params = http_request.query_params
|
|
72
240
|
case HTTPPostRequest():
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
headers=http_request.headers,
|
|
76
|
-
data=http_request.body,
|
|
77
|
-
params=http_request.query_params,
|
|
78
|
-
)
|
|
241
|
+
request = requests.Request("POST", http_request.url)
|
|
242
|
+
request.data = http_request.body
|
|
79
243
|
case _:
|
|
80
244
|
typing.assert_never(http_request)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
245
|
+
request.headers = http_request.headers
|
|
246
|
+
attributes: Attributes = {
|
|
247
|
+
"method": http_request.method,
|
|
248
|
+
"endpoint": api_request.endpoint,
|
|
249
|
+
}
|
|
250
|
+
with push_scope_optional(self._cfg.logger, "api_call", attributes=attributes):
|
|
251
|
+
if self._cfg.logger is not None:
|
|
252
|
+
self._cfg.logger.log_info(api_request.endpoint, attributes=attributes)
|
|
253
|
+
response = self._send_request(request)
|
|
254
|
+
response_data = self._get_response_json(response, request_id=request_id)
|
|
84
255
|
cached_parser = self._get_cached_parser(return_type)
|
|
85
256
|
try:
|
|
86
|
-
data =
|
|
257
|
+
data = response_data["data"]
|
|
87
258
|
return cached_parser.parse_api(data)
|
|
88
|
-
except ValueError as
|
|
89
|
-
|
|
90
|
-
raise err
|
|
259
|
+
except (ValueError, JSONDecodeError, KeyError) as e:
|
|
260
|
+
raise SDKError("unable to process response", request_id=request_id) from e
|
|
91
261
|
|
|
92
262
|
def _get_cached_parser(self, data_type: type[DT]) -> CachedParser[DT]:
|
|
93
263
|
if data_type not in self._parser_map:
|
|
94
264
|
self._parser_map[data_type] = CachedParser(data_type)
|
|
95
265
|
return self._parser_map[data_type]
|
|
96
266
|
|
|
267
|
+
def _get_oauth_bearer_token(self, *, oauth_details: AuthDetailsOAuth) -> str:
|
|
268
|
+
if (
|
|
269
|
+
self._oauth_bearer_token_cache is None
|
|
270
|
+
or (
|
|
271
|
+
self._oauth_bearer_token_cache.expires_at - datetime.now()
|
|
272
|
+
).total_seconds()
|
|
273
|
+
< OAUTH_REFRESH_WINDOW_SECONDS
|
|
274
|
+
):
|
|
275
|
+
refresh_url = urljoin(self._base_url, "/token/get_bearer_token")
|
|
276
|
+
request = requests.Request("POST", refresh_url)
|
|
277
|
+
request.data = {
|
|
278
|
+
"client_secret": oauth_details.refresh_token,
|
|
279
|
+
"scope": oauth_details.scope,
|
|
280
|
+
"grant_type": "client_credentials",
|
|
281
|
+
}
|
|
282
|
+
response = self._send_request(request)
|
|
283
|
+
data = self._get_response_json(response, request_id=str(uuid4()))
|
|
284
|
+
token_data = oauth_bearer_token_data_parser.parse_storage(data)
|
|
285
|
+
self._oauth_bearer_token_cache = OAuthBearerTokenCache(
|
|
286
|
+
token=token_data.access_token,
|
|
287
|
+
expires_at=datetime.now() + timedelta(seconds=token_data.expires_in),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
return self._oauth_bearer_token_cache.token
|
|
291
|
+
|
|
97
292
|
def _build_auth_headers(self) -> dict[str, str]:
|
|
98
293
|
match self._auth_details:
|
|
99
294
|
case AuthDetailsApiKey():
|
|
100
295
|
encoded = base64.standard_b64encode(
|
|
101
|
-
f"{self._auth_details.api_id}:{self._auth_details.api_secret_key}".encode(
|
|
102
|
-
"utf-8"
|
|
103
|
-
)
|
|
296
|
+
f"{self._auth_details.api_id}:{self._auth_details.api_secret_key}".encode()
|
|
104
297
|
).decode("utf-8")
|
|
105
298
|
return {"Authorization": f"Basic {encoded}"}
|
|
299
|
+
case AuthDetailsOAuth():
|
|
300
|
+
token = self._get_oauth_bearer_token(oauth_details=self._auth_details)
|
|
301
|
+
return {"Authorization": f"Bearer {token}"}
|
|
106
302
|
typing.assert_never(self._auth_details)
|
|
107
303
|
|
|
108
|
-
def _build_http_request(
|
|
304
|
+
def _build_http_request(
|
|
305
|
+
self, *, api_request: APIRequest, request_id: str
|
|
306
|
+
) -> HTTPRequest:
|
|
109
307
|
headers = self._build_auth_headers()
|
|
308
|
+
headers[UNC_REQUEST_ID_HEADER] = request_id
|
|
309
|
+
headers[UNC_SDK_VERSION_HEADER] = get_version()
|
|
110
310
|
method = api_request.method.lower()
|
|
111
|
-
|
|
311
|
+
data = {"data": json.dumps(serialize_for_api(api_request.args))}
|
|
112
312
|
match method:
|
|
113
313
|
case "get":
|
|
114
314
|
return HTTPGetRequest(
|
|
115
315
|
method=EndpointMethod.GET,
|
|
116
316
|
url=urljoin(self._base_url, api_request.endpoint),
|
|
117
|
-
query_params=
|
|
317
|
+
query_params=data,
|
|
118
318
|
headers=headers,
|
|
119
319
|
)
|
|
120
320
|
case "post":
|
|
121
321
|
return HTTPPostRequest(
|
|
122
322
|
method=EndpointMethod.POST,
|
|
123
323
|
url=urljoin(self._base_url, api_request.endpoint),
|
|
324
|
+
body=data,
|
|
124
325
|
headers=headers,
|
|
125
|
-
query_params=query_params
|
|
126
326
|
)
|
|
127
327
|
case _:
|
|
128
328
|
raise ValueError(f"unsupported request method: {method}")
|
|
329
|
+
|
|
330
|
+
def _get_downloaded_filename(self, *, cd: typing.Optional[str]) -> str:
|
|
331
|
+
if not cd:
|
|
332
|
+
return "Unknown"
|
|
333
|
+
|
|
334
|
+
fname = re.findall(r"filename\*=UTF-8''(.+)", cd)
|
|
335
|
+
if fname:
|
|
336
|
+
return unquote(fname[0])
|
|
337
|
+
|
|
338
|
+
fname = re.findall(r'filename="?(.+)"?', cd)
|
|
339
|
+
if fname:
|
|
340
|
+
return str(fname[0].strip('"'))
|
|
341
|
+
|
|
342
|
+
return "Unknown"
|
|
343
|
+
|
|
344
|
+
def download_files(
|
|
345
|
+
self, *, file_query: download_file_t.FileDownloadQuery
|
|
346
|
+
) -> DownloadedFiles:
|
|
347
|
+
"""Download a file from uncountable."""
|
|
348
|
+
request_id = str(uuid4())
|
|
349
|
+
api_request = APIRequest(
|
|
350
|
+
method=download_file_t.ENDPOINT_METHOD,
|
|
351
|
+
endpoint=download_file_t.ENDPOINT_PATH,
|
|
352
|
+
args=download_file_t.Arguments(
|
|
353
|
+
file_query=file_query,
|
|
354
|
+
),
|
|
355
|
+
)
|
|
356
|
+
http_request = self._build_http_request(
|
|
357
|
+
api_request=api_request, request_id=request_id
|
|
358
|
+
)
|
|
359
|
+
request = requests.Request(http_request.method.value, http_request.url)
|
|
360
|
+
request.headers = http_request.headers
|
|
361
|
+
assert isinstance(http_request, HTTPGetRequest)
|
|
362
|
+
request.params = http_request.query_params
|
|
363
|
+
response = self._send_request(request)
|
|
364
|
+
content = response.content
|
|
365
|
+
content_disposition = response.headers.get("Content-Disposition", None)
|
|
366
|
+
return [
|
|
367
|
+
DownloadedFile(
|
|
368
|
+
name=self._get_downloaded_filename(cd=content_disposition),
|
|
369
|
+
size=len(content),
|
|
370
|
+
data=BytesIO(content),
|
|
371
|
+
)
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
def upload_files(
|
|
375
|
+
self: typing.Self, *, file_uploads: list[FileUpload]
|
|
376
|
+
) -> list[UploadedFile]:
|
|
377
|
+
"""Upload files to uncountable, returning file ids that are usable with other SDK operations."""
|
|
378
|
+
return self._file_uploader.upload_files(file_uploads=file_uploads)
|
|
@@ -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_webhook_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
|
+
]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Generator, Literal, Self, assert_never
|
|
8
|
+
|
|
9
|
+
import aiohttp
|
|
10
|
+
import aiotus
|
|
11
|
+
|
|
12
|
+
from uncountable.integration.telemetry import Logger, push_scope_optional
|
|
13
|
+
|
|
14
|
+
from .types import AuthDetailsAll, AuthDetailsApiKey
|
|
15
|
+
|
|
16
|
+
_CHUNK_SIZE = 5 * 1024 * 1024 # s3 requires 5MiB minimum
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FileUploadType(StrEnum):
|
|
20
|
+
MEDIA_FILE_UPLOAD = "MEDIA_FILE_UPLOAD"
|
|
21
|
+
DATA_FILE_UPLOAD = "DATA_FILE_UPLOAD"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(kw_only=True)
|
|
25
|
+
class MediaFileUpload:
|
|
26
|
+
"""Upload file from a path on disk"""
|
|
27
|
+
|
|
28
|
+
path: str
|
|
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
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
FileUpload = MediaFileUpload | DataFileUpload
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(kw_only=True)
|
|
43
|
+
class FileBytes:
|
|
44
|
+
name: str
|
|
45
|
+
bytes_data: BytesIO
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@contextmanager
|
|
49
|
+
def file_upload_data(file_upload: FileUpload) -> Generator[FileBytes, None, None]:
|
|
50
|
+
match file_upload:
|
|
51
|
+
case MediaFileUpload():
|
|
52
|
+
with open(file_upload.path, "rb") as f:
|
|
53
|
+
yield FileBytes(
|
|
54
|
+
name=Path(file_upload.path).name, bytes_data=BytesIO(f.read())
|
|
55
|
+
)
|
|
56
|
+
case DataFileUpload():
|
|
57
|
+
yield FileBytes(name=file_upload.name, bytes_data=file_upload.data)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(kw_only=True)
|
|
61
|
+
class UploadedFile:
|
|
62
|
+
name: str
|
|
63
|
+
file_id: int
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class UploadFailed(Exception):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class FileUploader:
|
|
71
|
+
_auth_details: AuthDetailsAll
|
|
72
|
+
_base_url: str
|
|
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:
|
|
82
|
+
self._base_url = base_url
|
|
83
|
+
self._auth_details = auth_details
|
|
84
|
+
self._allow_insecure_tls = allow_insecure_tls
|
|
85
|
+
self._logger = logger
|
|
86
|
+
|
|
87
|
+
async def _upload_file(self: Self, file_upload: FileUpload) -> UploadedFile:
|
|
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
|
+
|
|
92
|
+
auth = aiohttp.BasicAuth(
|
|
93
|
+
self._auth_details.api_id, self._auth_details.api_secret_key
|
|
94
|
+
)
|
|
95
|
+
async with (
|
|
96
|
+
aiohttp.ClientSession(
|
|
97
|
+
auth=auth, headers={"Origin": self._base_url}
|
|
98
|
+
) as session,
|
|
99
|
+
):
|
|
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
|
+
location = await aiotus.upload(
|
|
115
|
+
creation_url,
|
|
116
|
+
file_bytes.bytes_data,
|
|
117
|
+
{"filename": file_bytes.name.encode()},
|
|
118
|
+
client_session=session,
|
|
119
|
+
config=aiotus.RetryConfiguration(
|
|
120
|
+
ssl=not self._allow_insecure_tls
|
|
121
|
+
),
|
|
122
|
+
chunksize=_CHUNK_SIZE,
|
|
123
|
+
)
|
|
124
|
+
if location is None:
|
|
125
|
+
raise UploadFailed(f"Failed to upload: {file_bytes.name}")
|
|
126
|
+
return UploadedFile(
|
|
127
|
+
name=file_bytes.name, file_id=int(location.path.split("/")[-1])
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def upload_files(
|
|
131
|
+
self: Self, *, file_uploads: list[FileUpload]
|
|
132
|
+
) -> list[UploadedFile]:
|
|
133
|
+
return [
|
|
134
|
+
asyncio.run(self._upload_file(file_upload)) for file_upload in file_uploads
|
|
135
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass(kw_only=True)
|
|
5
|
+
class AuthDetailsApiKey:
|
|
6
|
+
api_id: str
|
|
7
|
+
api_secret_key: str
|
|
8
|
+
|
|
9
|
+
|
|
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
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from opentelemetry.trace import get_current_span
|
|
4
|
+
|
|
5
|
+
from uncountable.core.environment import get_local_admin_server_port
|
|
6
|
+
from uncountable.integration.queue_runner.command_server.command_client import (
|
|
7
|
+
send_job_queue_message,
|
|
8
|
+
)
|
|
9
|
+
from uncountable.integration.telemetry import Logger
|
|
10
|
+
from uncountable.types import queued_job_t
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> None:
|
|
14
|
+
logger = Logger(get_current_span())
|
|
15
|
+
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
description="Process a job with a given command and job ID."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"command",
|
|
22
|
+
type=str,
|
|
23
|
+
choices=["run"],
|
|
24
|
+
help="The command to execute (e.g., 'run')",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
parser.add_argument("job_id", type=str, help="The ID of the job to process")
|
|
28
|
+
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--host", type=str, default="localhost", nargs="?", help="The host to run on"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
args = parser.parse_args()
|
|
34
|
+
|
|
35
|
+
with logger.push_scope(args.command):
|
|
36
|
+
if args.command == "run":
|
|
37
|
+
send_job_queue_message(
|
|
38
|
+
job_ref_name=args.job_id,
|
|
39
|
+
payload=queued_job_t.QueuedJobPayload(
|
|
40
|
+
invocation_context=queued_job_t.InvocationContextManual()
|
|
41
|
+
),
|
|
42
|
+
host=args.host,
|
|
43
|
+
port=get_local_admin_server_port(),
|
|
44
|
+
)
|
|
45
|
+
else:
|
|
46
|
+
parser.print_usage()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
main()
|