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
|
@@ -1,16 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import dataclasses
|
|
4
|
+
import math
|
|
2
5
|
import types
|
|
3
6
|
import typing
|
|
4
7
|
from collections import defaultdict
|
|
5
|
-
from dataclasses import MISSING, dataclass
|
|
6
8
|
from datetime import date, datetime
|
|
7
9
|
from decimal import Decimal
|
|
10
|
+
from enum import Enum, auto
|
|
8
11
|
from importlib import resources
|
|
9
12
|
|
|
10
13
|
import dateutil.parser
|
|
11
|
-
import yaml
|
|
12
14
|
|
|
13
|
-
from pkgs.serialization import
|
|
15
|
+
from pkgs.serialization import (
|
|
16
|
+
MissingSentryType,
|
|
17
|
+
OpaqueKey,
|
|
18
|
+
get_serial_class_data,
|
|
19
|
+
get_serial_union_data,
|
|
20
|
+
yaml,
|
|
21
|
+
)
|
|
14
22
|
|
|
15
23
|
from ._is_enum import is_string_enum_class
|
|
16
24
|
from ._is_namedtuple import is_namedtuple_type
|
|
@@ -18,38 +26,78 @@ from .case_convert import camel_to_snake_case, snake_to_camel_case
|
|
|
18
26
|
|
|
19
27
|
T = typing.TypeVar("T")
|
|
20
28
|
ParserFunction = typing.Callable[[typing.Any], T]
|
|
21
|
-
ParserCache = dict[
|
|
29
|
+
ParserCache = dict[type[typing.Any], ParserFunction[typing.Any]]
|
|
30
|
+
|
|
22
31
|
|
|
32
|
+
class SourceEncoding(Enum):
|
|
33
|
+
API = auto()
|
|
34
|
+
STORAGE = auto()
|
|
23
35
|
|
|
24
|
-
|
|
36
|
+
|
|
37
|
+
@dataclasses.dataclass(frozen=True, eq=True)
|
|
25
38
|
class ParserOptions:
|
|
26
|
-
|
|
39
|
+
encoding: SourceEncoding
|
|
40
|
+
strict_property_parsing: bool = False
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def Api(*, strict_property_parsing: bool = False) -> ParserOptions:
|
|
44
|
+
return ParserOptions(
|
|
45
|
+
encoding=SourceEncoding.API, strict_property_parsing=strict_property_parsing
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def Storage(*, strict_property_parsing: bool = False) -> ParserOptions:
|
|
50
|
+
return ParserOptions(
|
|
51
|
+
encoding=SourceEncoding.STORAGE,
|
|
52
|
+
strict_property_parsing=strict_property_parsing,
|
|
53
|
+
)
|
|
27
54
|
|
|
55
|
+
@property
|
|
56
|
+
def from_camel_case(self) -> bool:
|
|
57
|
+
return self.encoding == SourceEncoding.API
|
|
28
58
|
|
|
29
|
-
@
|
|
59
|
+
@property
|
|
60
|
+
def allow_direct_dataclass(self) -> bool:
|
|
61
|
+
return self.encoding == SourceEncoding.STORAGE
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclasses.dataclass(frozen=True)
|
|
30
65
|
class ParserContext:
|
|
31
66
|
options: ParserOptions
|
|
32
67
|
cache: ParserCache
|
|
33
68
|
|
|
34
69
|
|
|
70
|
+
class ParserError(BaseException): ...
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ParserExtraFieldsError(ParserError):
|
|
74
|
+
extra_fields: set[str]
|
|
75
|
+
|
|
76
|
+
def __init__(self, extra_fields: set[str]) -> None:
|
|
77
|
+
self.extra_fields = extra_fields
|
|
78
|
+
|
|
79
|
+
def __str__(self) -> str:
|
|
80
|
+
return f"extra fields were provided: {', '.join(self.extra_fields)}"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def is_union(field_type: typing.Any) -> bool:
|
|
84
|
+
origin = typing.get_origin(field_type)
|
|
85
|
+
return origin is typing.Union or origin is types.UnionType
|
|
86
|
+
|
|
87
|
+
|
|
35
88
|
def is_optional(field_type: typing.Any) -> bool:
|
|
36
|
-
return
|
|
37
|
-
None
|
|
38
|
-
) in typing.get_args(field_type)
|
|
89
|
+
return is_union(field_type) and type(None) in typing.get_args(field_type)
|
|
39
90
|
|
|
40
91
|
|
|
41
92
|
def is_missing(field_type: typing.Any) -> bool:
|
|
42
|
-
|
|
43
|
-
if origin is not typing.Union:
|
|
93
|
+
if not is_union(field_type):
|
|
44
94
|
return False
|
|
45
95
|
args = typing.get_args(field_type)
|
|
46
|
-
|
|
47
|
-
return False
|
|
48
|
-
return True
|
|
96
|
+
return not (len(args) == 0 or args[0] is not MissingSentryType)
|
|
49
97
|
|
|
50
98
|
|
|
51
99
|
def _invoke_tuple_parsers(
|
|
52
|
-
tuple_type:
|
|
100
|
+
tuple_type: type[T],
|
|
53
101
|
arg_parsers: typing.Sequence[typing.Callable[[typing.Any], object]],
|
|
54
102
|
has_ellipsis: bool,
|
|
55
103
|
value: typing.Any,
|
|
@@ -68,22 +116,25 @@ def _invoke_tuple_parsers(
|
|
|
68
116
|
|
|
69
117
|
|
|
70
118
|
def _invoke_fallback_parsers(
|
|
71
|
-
original_type:
|
|
119
|
+
original_type: type[T],
|
|
72
120
|
arg_parsers: typing.Sequence[typing.Callable[[typing.Any], T]],
|
|
73
121
|
value: typing.Any,
|
|
74
122
|
) -> T:
|
|
123
|
+
exceptions = []
|
|
124
|
+
|
|
75
125
|
for parser in arg_parsers:
|
|
76
126
|
try:
|
|
77
127
|
return parser(value)
|
|
78
|
-
except Exception:
|
|
128
|
+
except Exception as e:
|
|
129
|
+
exceptions.append(e)
|
|
79
130
|
continue
|
|
80
131
|
raise ValueError(
|
|
81
132
|
f"Unhandled value {value} cannot be cast to a member of {original_type}"
|
|
82
|
-
)
|
|
133
|
+
) from ExceptionGroup("Fallback Parser Exception", exceptions)
|
|
83
134
|
|
|
84
135
|
|
|
85
136
|
def _invoke_membership_parser(
|
|
86
|
-
expected_values:
|
|
137
|
+
expected_values: set[T],
|
|
87
138
|
value: typing.Any,
|
|
88
139
|
) -> T:
|
|
89
140
|
"""
|
|
@@ -97,24 +148,54 @@ def _invoke_membership_parser(
|
|
|
97
148
|
raise ValueError(f"Expected value from {expected_values} but got value {value}")
|
|
98
149
|
|
|
99
150
|
|
|
151
|
+
def _build_parser_discriminated_union(
|
|
152
|
+
context: ParserContext,
|
|
153
|
+
discriminator: str,
|
|
154
|
+
discriminator_map: dict[str, ParserFunction[T]],
|
|
155
|
+
) -> ParserFunction[T]:
|
|
156
|
+
def parse(value: typing.Any) -> typing.Any:
|
|
157
|
+
if context.options.allow_direct_dataclass and dataclasses.is_dataclass(value):
|
|
158
|
+
discriminant = getattr(value, discriminator)
|
|
159
|
+
else:
|
|
160
|
+
discriminant = value.get(discriminator)
|
|
161
|
+
if discriminant is None:
|
|
162
|
+
raise ValueError("missing-union-discriminant")
|
|
163
|
+
if not isinstance(discriminant, str):
|
|
164
|
+
raise ValueError("union-discriminant-is-not-string")
|
|
165
|
+
parser = discriminator_map.get(discriminant)
|
|
166
|
+
if parser is None:
|
|
167
|
+
raise ValueError("missing-type-for-union-discriminant", discriminant)
|
|
168
|
+
return parser(value)
|
|
169
|
+
|
|
170
|
+
return parse
|
|
171
|
+
|
|
172
|
+
|
|
100
173
|
def _build_parser_inner(
|
|
101
|
-
parsed_type:
|
|
174
|
+
parsed_type: type[T],
|
|
102
175
|
context: ParserContext,
|
|
103
|
-
*,
|
|
104
|
-
convert_string_to_snake_case: bool = False,
|
|
105
176
|
) -> ParserFunction[T]:
|
|
106
177
|
"""
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
then the generated parser will convert camel to snake case case
|
|
110
|
-
should only be True for cases like dictionary keys
|
|
111
|
-
should only be True if options.convert_to_snake_case is True
|
|
112
|
-
|
|
113
|
-
NOTE: This argument makes caching at this level difficult, as the cache-map
|
|
114
|
-
would need to vary based on this argument. For this reason only dataclasses
|
|
115
|
-
are cached now, as they don't use the argument, and they're known to be safe.
|
|
116
|
-
This is also enough to support some recursion.
|
|
178
|
+
IMPROVE: We can now cache at this level, to avoid producing redundant
|
|
179
|
+
internal parsers.
|
|
117
180
|
"""
|
|
181
|
+
|
|
182
|
+
serial_union = get_serial_union_data(parsed_type)
|
|
183
|
+
if serial_union is not None:
|
|
184
|
+
discriminator = serial_union.discriminator
|
|
185
|
+
discriminator_map = serial_union.discriminator_map
|
|
186
|
+
if discriminator is None or discriminator_map is None:
|
|
187
|
+
# fallback to standard union parsing
|
|
188
|
+
parsed_type = serial_union.get_union_underlying()
|
|
189
|
+
else:
|
|
190
|
+
return _build_parser_discriminated_union(
|
|
191
|
+
context,
|
|
192
|
+
discriminator,
|
|
193
|
+
{
|
|
194
|
+
key: _build_parser_inner(value, context)
|
|
195
|
+
for key, value in discriminator_map.items()
|
|
196
|
+
},
|
|
197
|
+
)
|
|
198
|
+
|
|
118
199
|
if dataclasses.is_dataclass(parsed_type):
|
|
119
200
|
return _build_parser_dataclass(parsed_type, context) # type: ignore[arg-type]
|
|
120
201
|
|
|
@@ -129,7 +210,7 @@ def _build_parser_inner(
|
|
|
129
210
|
field_name: field_parser(
|
|
130
211
|
value.get(
|
|
131
212
|
snake_to_camel_case(field_name)
|
|
132
|
-
if context.options.
|
|
213
|
+
if context.options.from_camel_case
|
|
133
214
|
else field_name
|
|
134
215
|
)
|
|
135
216
|
)
|
|
@@ -142,7 +223,7 @@ def _build_parser_inner(
|
|
|
142
223
|
origin = typing.get_origin(parsed_type)
|
|
143
224
|
if origin is tuple:
|
|
144
225
|
args = typing.get_args(parsed_type)
|
|
145
|
-
element_parsers:
|
|
226
|
+
element_parsers: list[typing.Callable[[typing.Any], object]] = []
|
|
146
227
|
has_ellipsis = False
|
|
147
228
|
for arg in args:
|
|
148
229
|
assert not has_ellipsis
|
|
@@ -164,7 +245,7 @@ def _build_parser_inner(
|
|
|
164
245
|
arg_parsers = [_build_parser_inner(arg, context) for arg in sorted_args]
|
|
165
246
|
return lambda value: _invoke_fallback_parsers(parsed_type, arg_parsers, value)
|
|
166
247
|
|
|
167
|
-
if parsed_type is typing.Any:
|
|
248
|
+
if parsed_type is typing.Any: # type: ignore[comparison-overlap]
|
|
168
249
|
return lambda value: value
|
|
169
250
|
|
|
170
251
|
if origin in (list, set):
|
|
@@ -173,10 +254,16 @@ def _build_parser_inner(
|
|
|
173
254
|
raise ValueError("List types only support one argument")
|
|
174
255
|
arg_parser = _build_parser_inner(args[0], context)
|
|
175
256
|
|
|
257
|
+
def parse_element(value: typing.Any) -> typing.Any:
|
|
258
|
+
try:
|
|
259
|
+
return arg_parser(value)
|
|
260
|
+
except Exception as e:
|
|
261
|
+
raise ValueError("Failed to parse element", value) from e
|
|
262
|
+
|
|
176
263
|
def parse(value: typing.Any) -> typing.Any:
|
|
177
264
|
if not isinstance(value, list):
|
|
178
265
|
raise ValueError("value is not a list", parsed_type)
|
|
179
|
-
return origin(
|
|
266
|
+
return origin(parse_element(x) for x in value)
|
|
180
267
|
|
|
181
268
|
return parse
|
|
182
269
|
|
|
@@ -184,36 +271,47 @@ def _build_parser_inner(
|
|
|
184
271
|
args = typing.get_args(parsed_type)
|
|
185
272
|
if len(args) != 2:
|
|
186
273
|
raise ValueError("Dict types only support two arguments for now")
|
|
187
|
-
|
|
274
|
+
k_inner_parser = _build_parser_inner(
|
|
188
275
|
args[0],
|
|
189
276
|
context,
|
|
190
|
-
convert_string_to_snake_case=context.options.convert_to_snake_case,
|
|
191
277
|
)
|
|
278
|
+
|
|
279
|
+
def key_parser(value: typing.Any) -> object:
|
|
280
|
+
inner = k_inner_parser(value)
|
|
281
|
+
if (
|
|
282
|
+
isinstance(inner, str)
|
|
283
|
+
# enum keys and OpaqueData's would also have string value types,
|
|
284
|
+
# but their explicit type is not a string, thus shouldn't be converted
|
|
285
|
+
and args[0] is str
|
|
286
|
+
and context.options.from_camel_case
|
|
287
|
+
):
|
|
288
|
+
return camel_to_snake_case(value)
|
|
289
|
+
return inner
|
|
290
|
+
|
|
192
291
|
v_parser = _build_parser_inner(args[1], context)
|
|
193
|
-
return lambda value: origin(
|
|
292
|
+
return lambda value: origin(
|
|
293
|
+
(key_parser(k), v_parser(v)) for k, v in value.items()
|
|
294
|
+
)
|
|
194
295
|
|
|
195
296
|
if origin == typing.Literal:
|
|
196
|
-
valid_values:
|
|
297
|
+
valid_values: set[T] = set(typing.get_args(parsed_type))
|
|
197
298
|
return lambda value: _invoke_membership_parser(valid_values, value)
|
|
198
299
|
|
|
199
|
-
if parsed_type is str and convert_string_to_snake_case:
|
|
200
|
-
return lambda value: camel_to_snake_case(value) # type: ignore
|
|
201
|
-
|
|
202
300
|
if parsed_type is int:
|
|
203
301
|
# first parse ints to decimal to allow scientific notation and decimals
|
|
204
302
|
# e.g. (1) 1e4 => 1000, (2) 3.0 => 3
|
|
205
303
|
|
|
206
304
|
def parse_int(value: typing.Any) -> T:
|
|
207
305
|
if isinstance(value, str):
|
|
208
|
-
assert (
|
|
209
|
-
"
|
|
210
|
-
)
|
|
306
|
+
assert "_" not in value, (
|
|
307
|
+
"numbers with underscores not considered integers"
|
|
308
|
+
)
|
|
211
309
|
|
|
212
310
|
dec_value = Decimal(value)
|
|
213
311
|
int_value = int(dec_value)
|
|
214
|
-
assert (
|
|
215
|
-
|
|
216
|
-
)
|
|
312
|
+
assert int_value == dec_value, (
|
|
313
|
+
f"value ({value}) cannot be parsed to int without discarding precision"
|
|
314
|
+
)
|
|
217
315
|
return int_value # type: ignore
|
|
218
316
|
|
|
219
317
|
return parse_int
|
|
@@ -237,7 +335,18 @@ def _build_parser_inner(
|
|
|
237
335
|
|
|
238
336
|
return parse_str
|
|
239
337
|
|
|
240
|
-
if parsed_type in (float,
|
|
338
|
+
if parsed_type in (float, Decimal):
|
|
339
|
+
|
|
340
|
+
def parse_as_numeric_type(value: typing.Any) -> T:
|
|
341
|
+
numeric_value: Decimal | float = parsed_type(value) # type: ignore
|
|
342
|
+
if math.isnan(numeric_value):
|
|
343
|
+
raise ValueError(f"Invalid numeric value: {numeric_value}")
|
|
344
|
+
|
|
345
|
+
return numeric_value # type: ignore
|
|
346
|
+
|
|
347
|
+
return parse_as_numeric_type
|
|
348
|
+
|
|
349
|
+
if parsed_type in (dict, bool) or is_string_enum_class(parsed_type):
|
|
241
350
|
return lambda value: parsed_type(value) # type: ignore
|
|
242
351
|
|
|
243
352
|
if parsed_type is MissingSentryType:
|
|
@@ -246,11 +355,21 @@ def _build_parser_inner(
|
|
|
246
355
|
raise ValueError("Missing type cannot be parsed directly")
|
|
247
356
|
|
|
248
357
|
return error
|
|
358
|
+
|
|
359
|
+
# Check last for generic annotated types and process them unwrapped
|
|
360
|
+
# this must be last, since some of the expected types, like Unions,
|
|
361
|
+
# will also be annotated, but have a special form
|
|
362
|
+
if typing.get_origin(parsed_type) is typing.Annotated:
|
|
363
|
+
return _build_parser_inner(
|
|
364
|
+
parsed_type.__origin__, # type: ignore[attr-defined]
|
|
365
|
+
context,
|
|
366
|
+
)
|
|
367
|
+
|
|
249
368
|
raise ValueError(f"Unhandled type {parsed_type}")
|
|
250
369
|
|
|
251
370
|
|
|
252
371
|
def _build_parser_dataclass(
|
|
253
|
-
parsed_type:
|
|
372
|
+
parsed_type: type[T],
|
|
254
373
|
context: ParserContext,
|
|
255
374
|
) -> ParserFunction[T]:
|
|
256
375
|
"""
|
|
@@ -264,36 +383,45 @@ def _build_parser_dataclass(
|
|
|
264
383
|
|
|
265
384
|
type_hints = typing.get_type_hints(parsed_type)
|
|
266
385
|
dc_field_parsers: list[
|
|
267
|
-
|
|
386
|
+
tuple[
|
|
268
387
|
dataclasses.Field[typing.Any],
|
|
269
|
-
|
|
388
|
+
type[typing.Any],
|
|
270
389
|
ParserFunction[typing.Any],
|
|
271
390
|
]
|
|
272
391
|
] = []
|
|
273
392
|
|
|
274
393
|
serial_class_data = get_serial_class_data(parsed_type)
|
|
275
394
|
|
|
395
|
+
def resolve_serialized_field_name(*, field_name: str) -> str:
|
|
396
|
+
return (
|
|
397
|
+
snake_to_camel_case(field_name)
|
|
398
|
+
if (
|
|
399
|
+
context.options.from_camel_case
|
|
400
|
+
and not serial_class_data.has_unconverted_key(field_name)
|
|
401
|
+
)
|
|
402
|
+
else field_name
|
|
403
|
+
)
|
|
404
|
+
|
|
276
405
|
def parse(value: typing.Any) -> typing.Any:
|
|
277
|
-
|
|
406
|
+
# Use an exact type match to prevent base/derived class mismatches
|
|
407
|
+
if context.options.allow_direct_dataclass and type(value) is parsed_type:
|
|
408
|
+
return value
|
|
409
|
+
|
|
410
|
+
data: dict[typing.Any, typing.Any] = {}
|
|
278
411
|
for field, field_type, field_parser in dc_field_parsers:
|
|
279
412
|
field_raw_value = None
|
|
280
413
|
try:
|
|
281
414
|
field_raw_value = value.get(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
context.options.convert_to_snake_case
|
|
285
|
-
and not serial_class_data.has_unconverted_key(field.name)
|
|
286
|
-
)
|
|
287
|
-
else field.name,
|
|
288
|
-
MISSING,
|
|
415
|
+
resolve_serialized_field_name(field_name=field.name),
|
|
416
|
+
dataclasses.MISSING,
|
|
289
417
|
)
|
|
290
418
|
field_value: typing.Any
|
|
291
|
-
if field_raw_value == MISSING:
|
|
419
|
+
if field_raw_value == dataclasses.MISSING:
|
|
292
420
|
if serial_class_data.has_parse_require(field.name):
|
|
293
421
|
raise ValueError("missing-required-field", field.name)
|
|
294
|
-
if field.default != MISSING:
|
|
422
|
+
if field.default != dataclasses.MISSING:
|
|
295
423
|
field_value = field.default
|
|
296
|
-
elif field.default_factory != MISSING:
|
|
424
|
+
elif field.default_factory != dataclasses.MISSING:
|
|
297
425
|
field_value = field.default_factory()
|
|
298
426
|
elif is_missing(field_type):
|
|
299
427
|
field_value = MissingSentryType()
|
|
@@ -315,9 +443,21 @@ def _build_parser_dataclass(
|
|
|
315
443
|
|
|
316
444
|
except Exception as e:
|
|
317
445
|
raise ValueError(
|
|
318
|
-
f"unable
|
|
446
|
+
f"unable-to-parse-field:{field.name}", field_raw_value
|
|
319
447
|
) from e
|
|
320
448
|
|
|
449
|
+
if context.options.strict_property_parsing:
|
|
450
|
+
all_allowed_field_names = set(
|
|
451
|
+
resolve_serialized_field_name(field_name=field.name)
|
|
452
|
+
for (field, _, _) in dc_field_parsers
|
|
453
|
+
)
|
|
454
|
+
passed_field_names = set(value.keys())
|
|
455
|
+
disallowed_field_names = passed_field_names.difference(
|
|
456
|
+
all_allowed_field_names
|
|
457
|
+
)
|
|
458
|
+
if len(disallowed_field_names) > 0:
|
|
459
|
+
raise ParserExtraFieldsError(disallowed_field_names)
|
|
460
|
+
|
|
321
461
|
return parsed_type(**data)
|
|
322
462
|
|
|
323
463
|
# Add to cache before building inner types, to support recursion
|
|
@@ -340,7 +480,7 @@ _CACHE_MAP: dict[ParserOptions, ParserCache] = defaultdict(ParserCache)
|
|
|
340
480
|
|
|
341
481
|
|
|
342
482
|
def build_parser(
|
|
343
|
-
parsed_type:
|
|
483
|
+
parsed_type: type[T],
|
|
344
484
|
options: ParserOptions,
|
|
345
485
|
) -> ParserFunction[T]:
|
|
346
486
|
"""
|
|
@@ -364,11 +504,27 @@ def build_parser(
|
|
|
364
504
|
class CachedParser(typing.Generic[T]):
|
|
365
505
|
def __init__(
|
|
366
506
|
self,
|
|
367
|
-
args:
|
|
507
|
+
args: type[T],
|
|
508
|
+
strict_property_parsing: bool = False,
|
|
368
509
|
):
|
|
369
510
|
self.arguments = args
|
|
370
|
-
self.parser_api:
|
|
371
|
-
self.parser_storage:
|
|
511
|
+
self.parser_api: ParserFunction[T] | None = None
|
|
512
|
+
self.parser_storage: ParserFunction[T] | None = None
|
|
513
|
+
self.strict_property_parsing = strict_property_parsing
|
|
514
|
+
|
|
515
|
+
def parse_from_encoding(
|
|
516
|
+
self,
|
|
517
|
+
args: typing.Any,
|
|
518
|
+
*,
|
|
519
|
+
source_encoding: SourceEncoding,
|
|
520
|
+
) -> T:
|
|
521
|
+
match source_encoding:
|
|
522
|
+
case SourceEncoding.API:
|
|
523
|
+
return self.parse_api(args)
|
|
524
|
+
case SourceEncoding.STORAGE:
|
|
525
|
+
return self.parse_storage(args)
|
|
526
|
+
case _:
|
|
527
|
+
typing.assert_never(source_encoding)
|
|
372
528
|
|
|
373
529
|
def parse_api(self, args: typing.Any) -> T:
|
|
374
530
|
"""
|
|
@@ -381,8 +537,8 @@ class CachedParser(typing.Generic[T]):
|
|
|
381
537
|
if self.parser_api is None:
|
|
382
538
|
self.parser_api = build_parser(
|
|
383
539
|
self.arguments,
|
|
384
|
-
ParserOptions(
|
|
385
|
-
|
|
540
|
+
ParserOptions.Api(
|
|
541
|
+
strict_property_parsing=self.strict_property_parsing,
|
|
386
542
|
),
|
|
387
543
|
)
|
|
388
544
|
assert self.parser_api is not None
|
|
@@ -395,8 +551,8 @@ class CachedParser(typing.Generic[T]):
|
|
|
395
551
|
if self.parser_storage is None:
|
|
396
552
|
self.parser_storage = build_parser(
|
|
397
553
|
self.arguments,
|
|
398
|
-
ParserOptions(
|
|
399
|
-
|
|
554
|
+
ParserOptions.Storage(
|
|
555
|
+
strict_property_parsing=self.strict_property_parsing,
|
|
400
556
|
),
|
|
401
557
|
)
|
|
402
558
|
assert self.parser_storage is not None
|
|
@@ -407,5 +563,5 @@ class CachedParser(typing.Generic[T]):
|
|
|
407
563
|
return self.parse_storage(yaml.safe_load(data_in))
|
|
408
564
|
|
|
409
565
|
def parse_yaml_resource(self, package: resources.Package, resource: str) -> T:
|
|
410
|
-
|
|
411
|
-
|
|
566
|
+
with resources.open_text(package, resource) as fp:
|
|
567
|
+
return self.parse_storage(yaml.safe_load(fp))
|
|
@@ -4,9 +4,10 @@ import re
|
|
|
4
4
|
|
|
5
5
|
@functools.lru_cache(maxsize=500000)
|
|
6
6
|
def snake_to_camel_case(o: str) -> str:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
tokens = o.split("_")
|
|
8
|
+
if len(tokens) < 2:
|
|
9
|
+
return o
|
|
10
|
+
return "".join([part.title() if i > 0 else part for i, part in enumerate(tokens)])
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def kebab_to_pascal_case(o: str) -> str:
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from ._gdrive_session import GDriveSession as GDriveSession
|
|
2
|
+
from ._gdrive_session import delete_gdrive_file as delete_gdrive_file
|
|
3
|
+
from ._gdrive_session import download_gdrive_file as download_gdrive_file
|
|
4
|
+
from ._gdrive_session import list_gdrive_files as list_gdrive_files
|
|
5
|
+
from ._gdrive_session import move_gdrive_file as move_gdrive_file
|
|
6
|
+
from ._gdrive_session import upload_file_gdrive as upload_file_gdrive
|
|
7
|
+
from ._local_session import LocalSession as LocalSession
|
|
8
|
+
from ._s3_session import S3Session as S3Session
|
|
9
|
+
from ._sftp_session import SFTPSession as SFTPSession
|
|
10
|
+
from ._sftp_session import list_sftp_files as list_sftp_files
|
|
11
|
+
from ._sftp_session import move_sftp_files as move_sftp_files
|
|
12
|
+
from .file_type_utils import FileObjectData as FileObjectData
|
|
13
|
+
from .file_type_utils import FileSystemFileReference as FileSystemFileReference
|
|
14
|
+
from .file_type_utils import FileSystemObject as FileSystemObject
|
|
15
|
+
from .file_type_utils import FileSystemS3Config as FileSystemS3Config
|
|
16
|
+
from .file_type_utils import FileSystemSFTPConfig as FileSystemSFTPConfig
|
|
17
|
+
from .file_type_utils import FileTransfer as FileTransfer
|
|
18
|
+
from .file_type_utils import IncompatibleFileReference as IncompatibleFileReference
|
|
19
|
+
from .file_type_utils import RemoteObjectReference as RemoteObjectReference
|
|
20
|
+
from .filesystem_session import FileSystemSession as FileSystemSession
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from io import BytesIO
|
|
2
|
+
|
|
3
|
+
from azure.storage.blob import BlobServiceClient, ContainerClient
|
|
4
|
+
|
|
5
|
+
from pkgs.filesystem_utils.file_type_utils import (
|
|
6
|
+
FileObjectData,
|
|
7
|
+
FileSystemBlobConfig,
|
|
8
|
+
FileSystemFileReference,
|
|
9
|
+
FileSystemObject,
|
|
10
|
+
FileTransfer,
|
|
11
|
+
IncompatibleFileReference,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .filesystem_session import FileSystemSession
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _add_slash(prefix: str) -> str:
|
|
18
|
+
if len(prefix) > 0 and prefix[-1] != "/":
|
|
19
|
+
prefix = prefix + "/"
|
|
20
|
+
return prefix
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BlobSession(FileSystemSession):
|
|
24
|
+
config: FileSystemBlobConfig
|
|
25
|
+
|
|
26
|
+
def __init__(self, blob_config: FileSystemBlobConfig) -> None:
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.config = blob_config
|
|
29
|
+
|
|
30
|
+
def start(self) -> None:
|
|
31
|
+
self.service_client: BlobServiceClient | None = BlobServiceClient(
|
|
32
|
+
self.config.account_url, credential=self.config.credential
|
|
33
|
+
)
|
|
34
|
+
self.container_client: ContainerClient | None = (
|
|
35
|
+
self.service_client.get_container_client(self.config.container)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def __enter__(self) -> "BlobSession":
|
|
39
|
+
self.start()
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
|
|
43
|
+
self.service_client = None
|
|
44
|
+
self.container_client = None
|
|
45
|
+
|
|
46
|
+
def list_files(
|
|
47
|
+
self,
|
|
48
|
+
dir_path: FileSystemObject,
|
|
49
|
+
*,
|
|
50
|
+
recursive: bool = False,
|
|
51
|
+
valid_extensions: list[str] | None = None,
|
|
52
|
+
) -> list[FileSystemObject]:
|
|
53
|
+
if not isinstance(dir_path, FileSystemFileReference):
|
|
54
|
+
raise IncompatibleFileReference()
|
|
55
|
+
|
|
56
|
+
assert self.service_client is not None and self.container_client is not None, (
|
|
57
|
+
"call to list_files on uninitialized blob session"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
filesystem_file_references: list[FileSystemObject] = []
|
|
61
|
+
prefix = _add_slash(dir_path.filepath)
|
|
62
|
+
for blob in self.container_client.list_blobs(name_starts_with=prefix):
|
|
63
|
+
if not recursive and (
|
|
64
|
+
blob.name == prefix or "/" in blob.name[len(prefix) :]
|
|
65
|
+
):
|
|
66
|
+
continue
|
|
67
|
+
if valid_extensions is None or any(
|
|
68
|
+
blob.name.endswith(valid_extension)
|
|
69
|
+
for valid_extension in valid_extensions
|
|
70
|
+
):
|
|
71
|
+
filesystem_file_references.append(
|
|
72
|
+
FileSystemFileReference(
|
|
73
|
+
filepath=blob.name,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return filesystem_file_references
|
|
78
|
+
|
|
79
|
+
def download_files(
|
|
80
|
+
self,
|
|
81
|
+
filepaths: list[FileSystemObject],
|
|
82
|
+
) -> list[FileObjectData]:
|
|
83
|
+
downloaded_files: list[FileObjectData] = []
|
|
84
|
+
assert self.service_client is not None and self.container_client is not None, (
|
|
85
|
+
"call to download_files on uninitialized blob session"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
for file_object in filepaths:
|
|
89
|
+
if (
|
|
90
|
+
not isinstance(file_object, FileSystemFileReference)
|
|
91
|
+
or file_object.filename is None
|
|
92
|
+
):
|
|
93
|
+
raise IncompatibleFileReference()
|
|
94
|
+
|
|
95
|
+
blob_client = self.container_client.get_blob_client(file_object.filepath)
|
|
96
|
+
download_stream = blob_client.download_blob()
|
|
97
|
+
file_data = download_stream.readall()
|
|
98
|
+
downloaded_files.append(
|
|
99
|
+
FileObjectData(
|
|
100
|
+
file_data=file_data,
|
|
101
|
+
file_IO=BytesIO(file_data),
|
|
102
|
+
filename=file_object.filename,
|
|
103
|
+
filepath=file_object.filepath,
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return downloaded_files
|
|
108
|
+
|
|
109
|
+
def move_files(self, file_mappings: list[FileTransfer]) -> None:
|
|
110
|
+
assert self.service_client is not None and self.container_client is not None, (
|
|
111
|
+
"call to move_files on uninitialized blob session"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
for src_file, dest_file in file_mappings:
|
|
115
|
+
if not isinstance(src_file, FileSystemFileReference) or not isinstance(
|
|
116
|
+
dest_file, FileSystemFileReference
|
|
117
|
+
):
|
|
118
|
+
raise IncompatibleFileReference()
|
|
119
|
+
|
|
120
|
+
source_blob_client = self.container_client.get_blob_client(
|
|
121
|
+
src_file.filepath
|
|
122
|
+
)
|
|
123
|
+
dest_blob_client = self.container_client.get_blob_client(dest_file.filepath)
|
|
124
|
+
|
|
125
|
+
dest_blob_client.start_copy_from_url(source_blob_client.url)
|
|
126
|
+
source_blob_client.delete_blob()
|
|
127
|
+
|
|
128
|
+
def delete_files(self, filepaths: list[FileSystemObject]) -> None:
|
|
129
|
+
assert self.service_client is not None and self.container_client is not None, (
|
|
130
|
+
"call to delete_files on uninitialized blob session"
|
|
131
|
+
)
|
|
132
|
+
for file_object in filepaths:
|
|
133
|
+
if not isinstance(file_object, FileSystemFileReference):
|
|
134
|
+
raise IncompatibleFileReference()
|
|
135
|
+
|
|
136
|
+
blob_client = self.container_client.get_blob_client(file_object.filepath)
|
|
137
|
+
blob_client.delete_blob()
|