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
examples/upload_files.py
CHANGED
pkgs/argument_parser/__init__.py
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
from ._is_enum import is_string_enum_class as is_string_enum_class
|
|
1
2
|
from .argument_parser import CachedParser as CachedParser
|
|
3
|
+
from .argument_parser import ParserBase as ParserBase
|
|
4
|
+
from .argument_parser import ParserError as ParserError
|
|
5
|
+
from .argument_parser import ParserExtraFieldsError as ParserExtraFieldsError
|
|
6
|
+
from .argument_parser import ParserFunction as ParserFunction
|
|
2
7
|
from .argument_parser import ParserOptions as ParserOptions
|
|
8
|
+
from .argument_parser import SourceEncoding as SourceEncoding
|
|
3
9
|
from .argument_parser import build_parser as build_parser
|
|
10
|
+
from .argument_parser import is_missing as is_missing
|
|
11
|
+
from .argument_parser import is_optional as is_optional
|
|
12
|
+
from .argument_parser import is_union as is_union
|
|
4
13
|
from .case_convert import camel_to_snake_case as camel_to_snake_case
|
|
5
14
|
from .case_convert import kebab_to_pascal_case as kebab_to_pascal_case
|
|
6
15
|
from .case_convert import snake_to_camel_case as snake_to_camel_case
|
|
@@ -1,20 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import dataclasses
|
|
4
|
+
import datetime
|
|
5
|
+
import math
|
|
2
6
|
import types
|
|
3
7
|
import typing
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
4
9
|
from collections import defaultdict
|
|
5
|
-
from
|
|
6
|
-
from datetime import date, datetime
|
|
10
|
+
from datetime import date
|
|
7
11
|
from decimal import Decimal
|
|
12
|
+
from enum import Enum, auto
|
|
8
13
|
from importlib import resources
|
|
9
14
|
|
|
10
15
|
import dateutil.parser
|
|
16
|
+
import msgspec.yaml
|
|
11
17
|
|
|
12
18
|
from pkgs.serialization import (
|
|
13
19
|
MissingSentryType,
|
|
14
20
|
OpaqueKey,
|
|
15
21
|
get_serial_class_data,
|
|
16
22
|
get_serial_union_data,
|
|
17
|
-
yaml,
|
|
18
23
|
)
|
|
19
24
|
|
|
20
25
|
from ._is_enum import is_string_enum_class
|
|
@@ -26,13 +31,41 @@ ParserFunction = typing.Callable[[typing.Any], T]
|
|
|
26
31
|
ParserCache = dict[type[typing.Any], ParserFunction[typing.Any]]
|
|
27
32
|
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
class SourceEncoding(Enum):
|
|
35
|
+
API = auto()
|
|
36
|
+
STORAGE = auto()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclasses.dataclass(frozen=True, eq=True)
|
|
30
40
|
class ParserOptions:
|
|
31
|
-
|
|
41
|
+
encoding: SourceEncoding
|
|
32
42
|
strict_property_parsing: bool = False
|
|
33
43
|
|
|
44
|
+
@staticmethod
|
|
45
|
+
def Api(*, strict_property_parsing: bool = False) -> ParserOptions:
|
|
46
|
+
return ParserOptions(
|
|
47
|
+
encoding=SourceEncoding.API, strict_property_parsing=strict_property_parsing
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def Storage(*, strict_property_parsing: bool = False) -> ParserOptions:
|
|
52
|
+
return ParserOptions(
|
|
53
|
+
encoding=SourceEncoding.STORAGE,
|
|
54
|
+
strict_property_parsing=strict_property_parsing,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def from_camel_case(self) -> bool:
|
|
59
|
+
return self.encoding == SourceEncoding.API
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def allow_direct_type(self) -> bool:
|
|
63
|
+
"""This allows parsing from a DB column without having to check whether it's
|
|
64
|
+
the native format of the type, a JSON column, or a string encoding."""
|
|
65
|
+
return self.encoding == SourceEncoding.STORAGE
|
|
66
|
+
|
|
34
67
|
|
|
35
|
-
@dataclass(frozen=True)
|
|
68
|
+
@dataclasses.dataclass(frozen=True)
|
|
36
69
|
class ParserContext:
|
|
37
70
|
options: ParserOptions
|
|
38
71
|
cache: ParserCache
|
|
@@ -51,20 +84,38 @@ class ParserExtraFieldsError(ParserError):
|
|
|
51
84
|
return f"extra fields were provided: {', '.join(self.extra_fields)}"
|
|
52
85
|
|
|
53
86
|
|
|
87
|
+
def is_union(field_type: typing.Any) -> bool:
|
|
88
|
+
origin = typing.get_origin(field_type)
|
|
89
|
+
return origin is typing.Union or origin is types.UnionType
|
|
90
|
+
|
|
91
|
+
|
|
54
92
|
def is_optional(field_type: typing.Any) -> bool:
|
|
55
|
-
return
|
|
56
|
-
None
|
|
57
|
-
) in typing.get_args(field_type)
|
|
93
|
+
return is_union(field_type) and type(None) in typing.get_args(field_type)
|
|
58
94
|
|
|
59
95
|
|
|
60
96
|
def is_missing(field_type: typing.Any) -> bool:
|
|
61
|
-
|
|
62
|
-
if origin is not typing.Union:
|
|
97
|
+
if not is_union(field_type):
|
|
63
98
|
return False
|
|
64
99
|
args = typing.get_args(field_type)
|
|
65
100
|
return not (len(args) == 0 or args[0] is not MissingSentryType)
|
|
66
101
|
|
|
67
102
|
|
|
103
|
+
def _has_field_default(field: dataclasses.Field[typing.Any]) -> bool:
|
|
104
|
+
return (
|
|
105
|
+
field.default != dataclasses.MISSING
|
|
106
|
+
and not isinstance(field.default, MissingSentryType)
|
|
107
|
+
) or field.default_factory != dataclasses.MISSING
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _get_field_default(
|
|
111
|
+
field: dataclasses.Field[typing.Any],
|
|
112
|
+
) -> typing.Any:
|
|
113
|
+
if field.default != dataclasses.MISSING:
|
|
114
|
+
return field.default
|
|
115
|
+
assert field.default_factory != dataclasses.MISSING
|
|
116
|
+
return field.default_factory()
|
|
117
|
+
|
|
118
|
+
|
|
68
119
|
def _invoke_tuple_parsers(
|
|
69
120
|
tuple_type: type[T],
|
|
70
121
|
arg_parsers: typing.Sequence[typing.Callable[[typing.Any], object]],
|
|
@@ -117,11 +168,39 @@ def _invoke_membership_parser(
|
|
|
117
168
|
raise ValueError(f"Expected value from {expected_values} but got value {value}")
|
|
118
169
|
|
|
119
170
|
|
|
171
|
+
# Uses `is` to compare
|
|
172
|
+
def _build_identity_parser(
|
|
173
|
+
identity_value: T,
|
|
174
|
+
) -> ParserFunction[T]:
|
|
175
|
+
def parse(value: typing.Any) -> T:
|
|
176
|
+
if value is identity_value:
|
|
177
|
+
return identity_value
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"Expected value {identity_value} (type: {type(identity_value)}) but got value {value} (type: {type(value)})"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return parse
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
NONE_IDENTITY_PARSER = _build_identity_parser(None)
|
|
186
|
+
|
|
187
|
+
|
|
120
188
|
def _build_parser_discriminated_union(
|
|
121
|
-
|
|
189
|
+
context: ParserContext,
|
|
190
|
+
discriminator_raw: str,
|
|
191
|
+
discriminator_map: dict[str, ParserFunction[T]],
|
|
122
192
|
) -> ParserFunction[T]:
|
|
193
|
+
discriminator = (
|
|
194
|
+
snake_to_camel_case(discriminator_raw)
|
|
195
|
+
if context.options.from_camel_case
|
|
196
|
+
else discriminator_raw
|
|
197
|
+
)
|
|
198
|
+
|
|
123
199
|
def parse(value: typing.Any) -> typing.Any:
|
|
124
|
-
|
|
200
|
+
if context.options.allow_direct_type and dataclasses.is_dataclass(value):
|
|
201
|
+
discriminant = getattr(value, discriminator)
|
|
202
|
+
else:
|
|
203
|
+
discriminant = value.get(discriminator)
|
|
125
204
|
if discriminant is None:
|
|
126
205
|
raise ValueError("missing-union-discriminant")
|
|
127
206
|
if not isinstance(discriminant, str):
|
|
@@ -137,20 +216,10 @@ def _build_parser_discriminated_union(
|
|
|
137
216
|
def _build_parser_inner(
|
|
138
217
|
parsed_type: type[T],
|
|
139
218
|
context: ParserContext,
|
|
140
|
-
*,
|
|
141
|
-
convert_string_to_snake_case: bool = False,
|
|
142
219
|
) -> ParserFunction[T]:
|
|
143
220
|
"""
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
then the generated parser will convert camel to snake case case
|
|
147
|
-
should only be True for cases like dictionary keys
|
|
148
|
-
should only be True if options.convert_to_snake_case is True
|
|
149
|
-
|
|
150
|
-
NOTE: This argument makes caching at this level difficult, as the cache-map
|
|
151
|
-
would need to vary based on this argument. For this reason only dataclasses
|
|
152
|
-
are cached now, as they don't use the argument, and they're known to be safe.
|
|
153
|
-
This is also enough to support some recursion.
|
|
221
|
+
IMPROVE: We can now cache at this level, to avoid producing redundant
|
|
222
|
+
internal parsers.
|
|
154
223
|
"""
|
|
155
224
|
|
|
156
225
|
serial_union = get_serial_union_data(parsed_type)
|
|
@@ -162,6 +231,7 @@ def _build_parser_inner(
|
|
|
162
231
|
parsed_type = serial_union.get_union_underlying()
|
|
163
232
|
else:
|
|
164
233
|
return _build_parser_discriminated_union(
|
|
234
|
+
context,
|
|
165
235
|
discriminator,
|
|
166
236
|
{
|
|
167
237
|
key: _build_parser_inner(value, context)
|
|
@@ -170,7 +240,7 @@ def _build_parser_inner(
|
|
|
170
240
|
)
|
|
171
241
|
|
|
172
242
|
if dataclasses.is_dataclass(parsed_type):
|
|
173
|
-
return _build_parser_dataclass(parsed_type, context)
|
|
243
|
+
return _build_parser_dataclass(parsed_type, context)
|
|
174
244
|
|
|
175
245
|
# namedtuple support
|
|
176
246
|
if is_namedtuple_type(parsed_type):
|
|
@@ -183,15 +253,17 @@ def _build_parser_inner(
|
|
|
183
253
|
field_name: field_parser(
|
|
184
254
|
value.get(
|
|
185
255
|
snake_to_camel_case(field_name)
|
|
186
|
-
if context.options.
|
|
256
|
+
if context.options.from_camel_case
|
|
187
257
|
else field_name
|
|
188
258
|
)
|
|
189
259
|
)
|
|
190
260
|
for field_name, field_parser in field_parsers
|
|
191
261
|
})
|
|
192
262
|
|
|
263
|
+
# IMPROVE: unclear why we need == here
|
|
193
264
|
if parsed_type == type(None): # noqa: E721
|
|
194
|
-
|
|
265
|
+
# Need to convince type checker that parsed_type is type(None)
|
|
266
|
+
return typing.cast(ParserFunction[T], NONE_IDENTITY_PARSER)
|
|
195
267
|
|
|
196
268
|
origin = typing.get_origin(parsed_type)
|
|
197
269
|
if origin is tuple:
|
|
@@ -218,7 +290,7 @@ def _build_parser_inner(
|
|
|
218
290
|
arg_parsers = [_build_parser_inner(arg, context) for arg in sorted_args]
|
|
219
291
|
return lambda value: _invoke_fallback_parsers(parsed_type, arg_parsers, value)
|
|
220
292
|
|
|
221
|
-
if parsed_type is typing.Any:
|
|
293
|
+
if parsed_type is typing.Any:
|
|
222
294
|
return lambda value: value
|
|
223
295
|
|
|
224
296
|
if origin in (list, set):
|
|
@@ -244,45 +316,70 @@ def _build_parser_inner(
|
|
|
244
316
|
args = typing.get_args(parsed_type)
|
|
245
317
|
if len(args) != 2:
|
|
246
318
|
raise ValueError("Dict types only support two arguments for now")
|
|
247
|
-
|
|
319
|
+
k_inner_parser = _build_parser_inner(
|
|
248
320
|
args[0],
|
|
249
321
|
context,
|
|
250
|
-
convert_string_to_snake_case=context.options.convert_to_snake_case,
|
|
251
322
|
)
|
|
323
|
+
|
|
324
|
+
def key_parser(value: typing.Any) -> object:
|
|
325
|
+
inner = k_inner_parser(value)
|
|
326
|
+
if (
|
|
327
|
+
isinstance(inner, str)
|
|
328
|
+
# enum keys and OpaqueData's would also have string value types,
|
|
329
|
+
# but their explicit type is not a string, thus shouldn't be converted
|
|
330
|
+
and args[0] is str
|
|
331
|
+
and context.options.from_camel_case
|
|
332
|
+
):
|
|
333
|
+
return camel_to_snake_case(value)
|
|
334
|
+
return inner
|
|
335
|
+
|
|
252
336
|
v_parser = _build_parser_inner(args[1], context)
|
|
253
|
-
return lambda value: origin(
|
|
337
|
+
return lambda value: origin(
|
|
338
|
+
(key_parser(k), v_parser(v)) for k, v in value.items()
|
|
339
|
+
)
|
|
254
340
|
|
|
255
341
|
if origin == typing.Literal:
|
|
256
342
|
valid_values: set[T] = set(typing.get_args(parsed_type))
|
|
257
343
|
return lambda value: _invoke_membership_parser(valid_values, value)
|
|
258
344
|
|
|
259
|
-
if parsed_type is str and convert_string_to_snake_case:
|
|
260
|
-
return lambda value: camel_to_snake_case(value) # type: ignore
|
|
261
|
-
|
|
262
345
|
if parsed_type is int:
|
|
263
346
|
# first parse ints to decimal to allow scientific notation and decimals
|
|
264
347
|
# e.g. (1) 1e4 => 1000, (2) 3.0 => 3
|
|
265
348
|
|
|
266
349
|
def parse_int(value: typing.Any) -> T:
|
|
267
350
|
if isinstance(value, str):
|
|
268
|
-
assert (
|
|
269
|
-
"
|
|
270
|
-
)
|
|
351
|
+
assert "_" not in value, (
|
|
352
|
+
"numbers with underscores not considered integers"
|
|
353
|
+
)
|
|
271
354
|
|
|
272
355
|
dec_value = Decimal(value)
|
|
273
356
|
int_value = int(dec_value)
|
|
274
|
-
assert (
|
|
275
|
-
|
|
276
|
-
)
|
|
357
|
+
assert int_value == dec_value, (
|
|
358
|
+
f"value ({value}) cannot be parsed to int without discarding precision"
|
|
359
|
+
)
|
|
277
360
|
return int_value # type: ignore
|
|
278
361
|
|
|
279
362
|
return parse_int
|
|
280
363
|
|
|
281
|
-
if parsed_type is datetime:
|
|
282
|
-
|
|
364
|
+
if parsed_type is datetime.datetime:
|
|
365
|
+
|
|
366
|
+
def parse_datetime(value: typing.Any) -> T:
|
|
367
|
+
if context.options.allow_direct_type and isinstance(
|
|
368
|
+
value, datetime.datetime
|
|
369
|
+
):
|
|
370
|
+
return value # type: ignore
|
|
371
|
+
return dateutil.parser.isoparse(value) # type:ignore
|
|
372
|
+
|
|
373
|
+
return parse_datetime
|
|
283
374
|
|
|
284
375
|
if parsed_type is date:
|
|
285
|
-
|
|
376
|
+
|
|
377
|
+
def parse_date(value: typing.Any) -> T:
|
|
378
|
+
if context.options.allow_direct_type and isinstance(value, date):
|
|
379
|
+
return value # type:ignore
|
|
380
|
+
return date.fromisoformat(value) # type:ignore
|
|
381
|
+
|
|
382
|
+
return parse_date
|
|
286
383
|
|
|
287
384
|
# MyPy: It's unclear why `parsed_type in (str, OpaqueKey)` is flagged as invalid
|
|
288
385
|
# Thus an or statement is used instead, which isn't flagged as invalid.
|
|
@@ -297,7 +394,18 @@ def _build_parser_inner(
|
|
|
297
394
|
|
|
298
395
|
return parse_str
|
|
299
396
|
|
|
300
|
-
if parsed_type in (float,
|
|
397
|
+
if parsed_type in (float, Decimal):
|
|
398
|
+
|
|
399
|
+
def parse_as_numeric_type(value: typing.Any) -> T:
|
|
400
|
+
numeric_value: Decimal | float = parsed_type(value) # type: ignore
|
|
401
|
+
if math.isnan(numeric_value):
|
|
402
|
+
raise ValueError(f"Invalid numeric value: {numeric_value}")
|
|
403
|
+
|
|
404
|
+
return numeric_value # type: ignore
|
|
405
|
+
|
|
406
|
+
return parse_as_numeric_type
|
|
407
|
+
|
|
408
|
+
if parsed_type in (dict, bool) or is_string_enum_class(parsed_type):
|
|
301
409
|
return lambda value: parsed_type(value) # type: ignore
|
|
302
410
|
|
|
303
411
|
if parsed_type is MissingSentryType:
|
|
@@ -306,7 +414,17 @@ def _build_parser_inner(
|
|
|
306
414
|
raise ValueError("Missing type cannot be parsed directly")
|
|
307
415
|
|
|
308
416
|
return error
|
|
309
|
-
|
|
417
|
+
|
|
418
|
+
# Check last for generic annotated types and process them unwrapped
|
|
419
|
+
# this must be last, since some of the expected types, like Unions,
|
|
420
|
+
# will also be annotated, but have a special form
|
|
421
|
+
if typing.get_origin(parsed_type) is typing.Annotated:
|
|
422
|
+
return _build_parser_inner(
|
|
423
|
+
parsed_type.__origin__, # type: ignore[attr-defined]
|
|
424
|
+
context,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
raise ValueError(f"Unhandled type {parsed_type}/{origin}")
|
|
310
428
|
|
|
311
429
|
|
|
312
430
|
def _build_parser_dataclass(
|
|
@@ -321,8 +439,7 @@ def _build_parser_dataclass(
|
|
|
321
439
|
cur_parser = context.cache.get(parsed_type)
|
|
322
440
|
if cur_parser is not None:
|
|
323
441
|
return cur_parser
|
|
324
|
-
|
|
325
|
-
type_hints = typing.get_type_hints(parsed_type)
|
|
442
|
+
type_hints = typing.get_type_hints(parsed_type, include_extras=True)
|
|
326
443
|
dc_field_parsers: list[
|
|
327
444
|
tuple[
|
|
328
445
|
dataclasses.Field[typing.Any],
|
|
@@ -337,29 +454,31 @@ def _build_parser_dataclass(
|
|
|
337
454
|
return (
|
|
338
455
|
snake_to_camel_case(field_name)
|
|
339
456
|
if (
|
|
340
|
-
context.options.
|
|
457
|
+
context.options.from_camel_case
|
|
341
458
|
and not serial_class_data.has_unconverted_key(field_name)
|
|
342
459
|
)
|
|
343
460
|
else field_name
|
|
344
461
|
)
|
|
345
462
|
|
|
346
463
|
def parse(value: typing.Any) -> typing.Any:
|
|
464
|
+
# Use an exact type match to prevent base/derived class mismatches
|
|
465
|
+
if context.options.allow_direct_type and type(value) is parsed_type:
|
|
466
|
+
return value
|
|
467
|
+
|
|
347
468
|
data: dict[typing.Any, typing.Any] = {}
|
|
348
469
|
for field, field_type, field_parser in dc_field_parsers:
|
|
349
470
|
field_raw_value = None
|
|
350
471
|
try:
|
|
351
472
|
field_raw_value = value.get(
|
|
352
473
|
resolve_serialized_field_name(field_name=field.name),
|
|
353
|
-
MISSING,
|
|
474
|
+
dataclasses.MISSING,
|
|
354
475
|
)
|
|
355
476
|
field_value: typing.Any
|
|
356
|
-
if field_raw_value == MISSING:
|
|
477
|
+
if field_raw_value == dataclasses.MISSING:
|
|
357
478
|
if serial_class_data.has_parse_require(field.name):
|
|
358
479
|
raise ValueError("missing-required-field", field.name)
|
|
359
|
-
if field
|
|
360
|
-
field_value = field
|
|
361
|
-
elif field.default_factory != MISSING:
|
|
362
|
-
field_value = field.default_factory()
|
|
480
|
+
if _has_field_default(field):
|
|
481
|
+
field_value = _get_field_default(field)
|
|
363
482
|
elif is_missing(field_type):
|
|
364
483
|
field_value = MissingSentryType()
|
|
365
484
|
elif is_optional(field_type):
|
|
@@ -371,6 +490,13 @@ def _build_parser_dataclass(
|
|
|
371
490
|
field_value = False
|
|
372
491
|
else:
|
|
373
492
|
raise ValueError("missing-value-for-field", field.name)
|
|
493
|
+
elif (
|
|
494
|
+
field_raw_value is None
|
|
495
|
+
and not is_optional(field_type)
|
|
496
|
+
and _has_field_default(field)
|
|
497
|
+
and not serial_class_data.has_parse_require(field.name)
|
|
498
|
+
):
|
|
499
|
+
field_value = _get_field_default(field)
|
|
374
500
|
elif serial_class_data.has_unconverted_value(field.name):
|
|
375
501
|
field_value = field_raw_value
|
|
376
502
|
else:
|
|
@@ -438,15 +564,46 @@ def build_parser(
|
|
|
438
564
|
return built_parser
|
|
439
565
|
|
|
440
566
|
|
|
441
|
-
class
|
|
567
|
+
class ParserBase(ABC, typing.Generic[T]):
|
|
568
|
+
def parse_from_encoding(
|
|
569
|
+
self,
|
|
570
|
+
args: typing.Any,
|
|
571
|
+
*,
|
|
572
|
+
source_encoding: SourceEncoding,
|
|
573
|
+
) -> T:
|
|
574
|
+
match source_encoding:
|
|
575
|
+
case SourceEncoding.API:
|
|
576
|
+
return self.parse_api(args)
|
|
577
|
+
case SourceEncoding.STORAGE:
|
|
578
|
+
return self.parse_storage(args)
|
|
579
|
+
case _:
|
|
580
|
+
typing.assert_never(source_encoding)
|
|
581
|
+
|
|
582
|
+
# IMPROVE: Args would be better typed as "object"
|
|
583
|
+
@abstractmethod
|
|
584
|
+
def parse_storage(self, args: typing.Any) -> T: ...
|
|
585
|
+
|
|
586
|
+
@abstractmethod
|
|
587
|
+
def parse_api(self, args: typing.Any) -> T: ...
|
|
588
|
+
|
|
589
|
+
def parse_yaml_file(self, path: str) -> T:
|
|
590
|
+
with open(path, encoding="utf-8") as data_in:
|
|
591
|
+
return self.parse_storage(msgspec.yaml.decode(data_in.read()))
|
|
592
|
+
|
|
593
|
+
def parse_yaml_resource(self, package: resources.Package, resource: str) -> T:
|
|
594
|
+
with resources.open_text(package, resource) as fp:
|
|
595
|
+
return self.parse_storage(msgspec.yaml.decode(fp.read()))
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
class CachedParser(ParserBase[T], typing.Generic[T]):
|
|
442
599
|
def __init__(
|
|
443
600
|
self,
|
|
444
601
|
args: type[T],
|
|
445
602
|
strict_property_parsing: bool = False,
|
|
446
603
|
):
|
|
447
604
|
self.arguments = args
|
|
448
|
-
self.parser_api:
|
|
449
|
-
self.parser_storage:
|
|
605
|
+
self.parser_api: ParserFunction[T] | None = None
|
|
606
|
+
self.parser_storage: ParserFunction[T] | None = None
|
|
450
607
|
self.strict_property_parsing = strict_property_parsing
|
|
451
608
|
|
|
452
609
|
def parse_api(self, args: typing.Any) -> T:
|
|
@@ -460,8 +617,7 @@ class CachedParser(typing.Generic[T]):
|
|
|
460
617
|
if self.parser_api is None:
|
|
461
618
|
self.parser_api = build_parser(
|
|
462
619
|
self.arguments,
|
|
463
|
-
ParserOptions(
|
|
464
|
-
convert_to_snake_case=True,
|
|
620
|
+
ParserOptions.Api(
|
|
465
621
|
strict_property_parsing=self.strict_property_parsing,
|
|
466
622
|
),
|
|
467
623
|
)
|
|
@@ -475,18 +631,9 @@ class CachedParser(typing.Generic[T]):
|
|
|
475
631
|
if self.parser_storage is None:
|
|
476
632
|
self.parser_storage = build_parser(
|
|
477
633
|
self.arguments,
|
|
478
|
-
ParserOptions(
|
|
479
|
-
convert_to_snake_case=False,
|
|
634
|
+
ParserOptions.Storage(
|
|
480
635
|
strict_property_parsing=self.strict_property_parsing,
|
|
481
636
|
),
|
|
482
637
|
)
|
|
483
638
|
assert self.parser_storage is not None
|
|
484
639
|
return self.parser_storage(args)
|
|
485
|
-
|
|
486
|
-
def parse_yaml_file(self, path: str) -> T:
|
|
487
|
-
with open(path, encoding="utf-8") as data_in:
|
|
488
|
-
return self.parse_storage(yaml.safe_load(data_in))
|
|
489
|
-
|
|
490
|
-
def parse_yaml_resource(self, package: resources.Package, resource: str) -> T:
|
|
491
|
-
with resources.open_text(package, resource) as fp:
|
|
492
|
-
return self.parse_storage(yaml.safe_load(fp))
|
|
@@ -17,3 +17,4 @@ from .file_type_utils import FileSystemSFTPConfig as FileSystemSFTPConfig
|
|
|
17
17
|
from .file_type_utils import FileTransfer as FileTransfer
|
|
18
18
|
from .file_type_utils import IncompatibleFileReference as IncompatibleFileReference
|
|
19
19
|
from .file_type_utils import RemoteObjectReference as RemoteObjectReference
|
|
20
|
+
from .filesystem_session import FileSystemSession as FileSystemSession
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from io import BytesIO
|
|
2
|
+
|
|
3
|
+
from azure.core.credentials import AzureSasCredential
|
|
4
|
+
from azure.storage.blob import BlobServiceClient, ContainerClient
|
|
5
|
+
|
|
6
|
+
from pkgs.filesystem_utils.file_type_utils import (
|
|
7
|
+
FileObjectData,
|
|
8
|
+
FileSystemBlobConfig,
|
|
9
|
+
FileSystemFileReference,
|
|
10
|
+
FileSystemObject,
|
|
11
|
+
FileTransfer,
|
|
12
|
+
IncompatibleFileReference,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .filesystem_session import FileSystemSession
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _add_slash(prefix: str) -> str:
|
|
19
|
+
if len(prefix) > 0 and prefix[-1] != "/":
|
|
20
|
+
prefix = prefix + "/"
|
|
21
|
+
return prefix
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BlobSession(FileSystemSession):
|
|
25
|
+
config: FileSystemBlobConfig
|
|
26
|
+
|
|
27
|
+
def __init__(self, blob_config: FileSystemBlobConfig) -> None:
|
|
28
|
+
super().__init__()
|
|
29
|
+
self.config = blob_config
|
|
30
|
+
|
|
31
|
+
def start(self) -> None:
|
|
32
|
+
self.service_client: BlobServiceClient | None = BlobServiceClient(
|
|
33
|
+
self.config.account_url, credential=self.config.credential
|
|
34
|
+
)
|
|
35
|
+
self.container_client: ContainerClient | None = (
|
|
36
|
+
self.service_client.get_container_client(self.config.container)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def __enter__(self) -> "BlobSession":
|
|
40
|
+
self.start()
|
|
41
|
+
return self
|
|
42
|
+
|
|
43
|
+
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
|
|
44
|
+
self.service_client = None
|
|
45
|
+
self.container_client = None
|
|
46
|
+
|
|
47
|
+
def list_files(
|
|
48
|
+
self,
|
|
49
|
+
dir_path: FileSystemObject,
|
|
50
|
+
*,
|
|
51
|
+
recursive: bool = False,
|
|
52
|
+
valid_extensions: list[str] | None = None,
|
|
53
|
+
) -> list[FileSystemObject]:
|
|
54
|
+
if not isinstance(dir_path, FileSystemFileReference):
|
|
55
|
+
raise IncompatibleFileReference()
|
|
56
|
+
|
|
57
|
+
assert self.service_client is not None and self.container_client is not None, (
|
|
58
|
+
"call to list_files on uninitialized blob session"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
filesystem_file_references: list[FileSystemObject] = []
|
|
62
|
+
prefix = _add_slash(dir_path.filepath)
|
|
63
|
+
for blob in self.container_client.list_blobs(name_starts_with=prefix):
|
|
64
|
+
if not recursive and (
|
|
65
|
+
blob.name == prefix or "/" in blob.name[len(prefix) :]
|
|
66
|
+
):
|
|
67
|
+
continue
|
|
68
|
+
if valid_extensions is None or any(
|
|
69
|
+
blob.name.endswith(valid_extension)
|
|
70
|
+
for valid_extension in valid_extensions
|
|
71
|
+
):
|
|
72
|
+
filesystem_file_references.append(
|
|
73
|
+
FileSystemFileReference(
|
|
74
|
+
filepath=blob.name,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return filesystem_file_references
|
|
79
|
+
|
|
80
|
+
def download_files(
|
|
81
|
+
self,
|
|
82
|
+
filepaths: list[FileSystemObject],
|
|
83
|
+
) -> list[FileObjectData]:
|
|
84
|
+
downloaded_files: list[FileObjectData] = []
|
|
85
|
+
assert self.service_client is not None and self.container_client is not None, (
|
|
86
|
+
"call to download_files on uninitialized blob session"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
for file_object in filepaths:
|
|
90
|
+
if (
|
|
91
|
+
not isinstance(file_object, FileSystemFileReference)
|
|
92
|
+
or file_object.filename is None
|
|
93
|
+
):
|
|
94
|
+
raise IncompatibleFileReference()
|
|
95
|
+
|
|
96
|
+
blob_client = self.container_client.get_blob_client(file_object.filepath)
|
|
97
|
+
download_stream = blob_client.download_blob()
|
|
98
|
+
file_data = download_stream.readall()
|
|
99
|
+
downloaded_files.append(
|
|
100
|
+
FileObjectData(
|
|
101
|
+
file_data=file_data,
|
|
102
|
+
file_IO=BytesIO(file_data),
|
|
103
|
+
filename=file_object.filename,
|
|
104
|
+
filepath=file_object.filepath,
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return downloaded_files
|
|
109
|
+
|
|
110
|
+
def move_files(self, file_mappings: list[FileTransfer]) -> None:
|
|
111
|
+
assert self.service_client is not None and self.container_client is not None, (
|
|
112
|
+
"call to move_files on uninitialized blob session"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
for src_file, dest_file in file_mappings:
|
|
116
|
+
if not isinstance(src_file, FileSystemFileReference) or not isinstance(
|
|
117
|
+
dest_file, FileSystemFileReference
|
|
118
|
+
):
|
|
119
|
+
raise IncompatibleFileReference()
|
|
120
|
+
|
|
121
|
+
source_blob_client = self.container_client.get_blob_client(
|
|
122
|
+
src_file.filepath
|
|
123
|
+
)
|
|
124
|
+
dest_blob_client = self.container_client.get_blob_client(dest_file.filepath)
|
|
125
|
+
|
|
126
|
+
source_url = (
|
|
127
|
+
f"{source_blob_client.url}?{self.config.credential.signature}"
|
|
128
|
+
if isinstance(self.config.credential, AzureSasCredential)
|
|
129
|
+
else source_blob_client.url
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
dest_blob_client.start_copy_from_url(source_url)
|
|
133
|
+
source_blob_client.delete_blob()
|
|
134
|
+
|
|
135
|
+
def delete_files(self, filepaths: list[FileSystemObject]) -> None:
|
|
136
|
+
assert self.service_client is not None and self.container_client is not None, (
|
|
137
|
+
"call to delete_files on uninitialized blob session"
|
|
138
|
+
)
|
|
139
|
+
for file_object in filepaths:
|
|
140
|
+
if not isinstance(file_object, FileSystemFileReference):
|
|
141
|
+
raise IncompatibleFileReference()
|
|
142
|
+
|
|
143
|
+
blob_client = self.container_client.get_blob_client(file_object.filepath)
|
|
144
|
+
blob_client.delete_blob()
|