UncountablePythonSDK 0.0.82__py3-none-any.whl → 0.0.132__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/basic_auth.py +7 -0
- examples/create_ingredient_sdk.py +34 -0
- examples/download_files.py +26 -0
- examples/integration-server/jobs/materials_auto/concurrent_cron.py +11 -0
- examples/integration-server/jobs/materials_auto/example_cron.py +3 -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 +17 -9
- examples/integration-server/jobs/materials_auto/profile.yaml +61 -0
- examples/integration-server/pyproject.toml +10 -10
- examples/oauth.py +7 -0
- examples/set_recipe_metadata_file.py +1 -1
- examples/upload_files.py +1 -2
- pkgs/argument_parser/__init__.py +8 -0
- pkgs/argument_parser/_is_namedtuple.py +3 -0
- pkgs/argument_parser/argument_parser.py +196 -63
- pkgs/filesystem_utils/__init__.py +1 -0
- pkgs/filesystem_utils/_blob_session.py +144 -0
- pkgs/filesystem_utils/_gdrive_session.py +5 -5
- pkgs/filesystem_utils/_s3_session.py +2 -1
- pkgs/filesystem_utils/_sftp_session.py +6 -3
- pkgs/filesystem_utils/file_type_utils.py +30 -10
- 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 +40 -48
- pkgs/serialization/serial_generic.py +16 -0
- pkgs/serialization/serial_union.py +16 -16
- 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 +2 -4
- pkgs/type_spec/builder.py +248 -70
- pkgs/type_spec/builder_types.py +9 -0
- pkgs/type_spec/config.py +40 -7
- pkgs/type_spec/cross_output_links.py +99 -0
- pkgs/type_spec/emit_open_api.py +121 -34
- pkgs/type_spec/emit_open_api_util.py +5 -5
- pkgs/type_spec/emit_python.py +277 -86
- pkgs/type_spec/emit_typescript.py +102 -29
- pkgs/type_spec/emit_typescript_util.py +66 -10
- pkgs/type_spec/load_types.py +16 -3
- 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 +11 -8
- 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 +115 -22
- 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 +3 -3
- pkgs/type_spec/value_spec/__main__.py +26 -9
- pkgs/type_spec/value_spec/convert_type.py +18 -0
- pkgs/type_spec/value_spec/emit_python.py +13 -3
- pkgs/type_spec/value_spec/types.py +1 -1
- uncountable/core/async_batch.py +1 -1
- uncountable/core/client.py +133 -34
- uncountable/core/environment.py +3 -3
- uncountable/core/file_upload.py +39 -15
- uncountable/integration/cli.py +116 -23
- uncountable/integration/construct_client.py +3 -3
- uncountable/integration/executors/executors.py +12 -2
- uncountable/integration/executors/generic_upload_executor.py +66 -14
- uncountable/integration/http_server/__init__.py +5 -0
- uncountable/integration/http_server/types.py +69 -0
- uncountable/integration/job.py +192 -7
- uncountable/integration/queue_runner/command_server/__init__.py +4 -0
- uncountable/integration/queue_runner/command_server/command_client.py +65 -0
- uncountable/integration/queue_runner/command_server/command_server.py +83 -5
- uncountable/integration/queue_runner/command_server/constants.py +4 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server.proto +36 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +28 -11
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +77 -1
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +135 -0
- uncountable/integration/queue_runner/command_server/types.py +25 -2
- uncountable/integration/queue_runner/datastore/datastore_sqlite.py +168 -11
- uncountable/integration/queue_runner/datastore/interface.py +10 -0
- uncountable/integration/queue_runner/datastore/model.py +8 -1
- uncountable/integration/queue_runner/job_scheduler.py +63 -23
- uncountable/integration/queue_runner/queue_runner.py +10 -2
- uncountable/integration/queue_runner/worker.py +22 -17
- uncountable/integration/scan_profiles.py +1 -1
- uncountable/integration/scheduler.py +74 -25
- uncountable/integration/secret_retrieval/retrieve_secret.py +1 -1
- uncountable/integration/server.py +42 -12
- uncountable/integration/telemetry.py +63 -10
- uncountable/integration/webhook_server/entrypoint.py +39 -112
- uncountable/types/__init__.py +58 -1
- uncountable/types/api/batch/execute_batch.py +5 -6
- uncountable/types/api/batch/execute_batch_load_async.py +2 -3
- uncountable/types/api/chemical/convert_chemical_formats.py +10 -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 +7 -7
- uncountable/types/api/entity/create_entity.py +8 -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 +3 -4
- uncountable/types/api/entity/grant_entity_permissions.py +6 -6
- uncountable/types/api/entity/list_aggregate.py +79 -0
- uncountable/types/api/entity/list_entities.py +34 -10
- uncountable/types/api/entity/lock_entity.py +4 -4
- uncountable/types/api/entity/lookup_entity.py +116 -0
- uncountable/types/api/entity/resolve_entity_ids.py +5 -6
- uncountable/types/api/entity/set_entity_field_values.py +44 -0
- uncountable/types/api/entity/set_values.py +3 -3
- uncountable/types/api/entity/transition_entity_phase.py +14 -7
- uncountable/types/api/entity/unlock_entity.py +3 -3
- uncountable/types/api/equipment/associate_equipment_input.py +2 -3
- uncountable/types/api/field_options/upsert_field_options.py +7 -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 +6 -7
- uncountable/types/api/id_source/match_id_source.py +4 -5
- uncountable/types/api/input_groups/get_input_group_names.py +3 -4
- uncountable/types/api/inputs/create_inputs.py +10 -9
- uncountable/types/api/inputs/get_input_data.py +11 -12
- uncountable/types/api/inputs/get_input_names.py +6 -7
- uncountable/types/api/inputs/get_inputs_data.py +6 -7
- uncountable/types/api/inputs/set_input_attribute_values.py +5 -6
- uncountable/types/api/inputs/set_input_category.py +5 -5
- uncountable/types/api/inputs/set_input_subcategories.py +3 -3
- uncountable/types/api/inputs/set_intermediate_type.py +4 -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 +3 -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 +12 -13
- uncountable/types/api/outputs/get_output_names.py +5 -6
- uncountable/types/api/outputs/get_output_organization.py +173 -0
- uncountable/types/api/outputs/resolve_output_conditions.py +7 -8
- uncountable/types/api/permissions/set_core_permissions.py +16 -10
- uncountable/types/api/project/get_projects.py +6 -7
- uncountable/types/api/project/get_projects_data.py +7 -8
- uncountable/types/api/recipe_links/create_recipe_link.py +5 -5
- uncountable/types/api/recipe_links/remove_recipe_link.py +4 -4
- uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +6 -7
- uncountable/types/api/recipes/add_recipe_to_project.py +3 -3
- uncountable/types/api/recipes/add_time_series_data.py +64 -0
- uncountable/types/api/recipes/archive_recipes.py +4 -4
- uncountable/types/api/recipes/associate_recipe_as_input.py +5 -5
- uncountable/types/api/recipes/associate_recipe_as_lot.py +3 -3
- uncountable/types/api/recipes/clear_recipe_outputs.py +3 -3
- uncountable/types/api/recipes/create_mix_order.py +44 -0
- uncountable/types/api/recipes/create_recipe.py +8 -9
- uncountable/types/api/recipes/create_recipes.py +8 -9
- uncountable/types/api/recipes/disassociate_recipe_as_input.py +3 -3
- uncountable/types/api/recipes/edit_recipe_inputs.py +101 -24
- uncountable/types/api/recipes/get_column_calculation_values.py +4 -5
- uncountable/types/api/recipes/get_curve.py +4 -5
- uncountable/types/api/recipes/get_recipe_calculations.py +6 -7
- uncountable/types/api/recipes/get_recipe_links.py +3 -4
- uncountable/types/api/recipes/get_recipe_names.py +3 -4
- uncountable/types/api/recipes/get_recipe_output_metadata.py +5 -6
- uncountable/types/api/recipes/get_recipes_data.py +62 -34
- uncountable/types/api/recipes/lock_recipes.py +9 -8
- uncountable/types/api/recipes/remove_recipe_from_project.py +3 -3
- uncountable/types/api/recipes/set_recipe_inputs.py +9 -10
- uncountable/types/api/recipes/set_recipe_metadata.py +3 -3
- uncountable/types/api/recipes/set_recipe_output_annotations.py +11 -12
- uncountable/types/api/recipes/set_recipe_output_file.py +5 -6
- uncountable/types/api/recipes/set_recipe_outputs.py +24 -13
- uncountable/types/api/recipes/set_recipe_tags.py +14 -9
- uncountable/types/api/recipes/set_recipe_total.py +59 -0
- uncountable/types/api/recipes/unarchive_recipes.py +3 -3
- uncountable/types/api/recipes/unlock_recipes.py +7 -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 +4 -4
- uncountable/types/api/uploader/complete_async_parse.py +46 -0
- uncountable/types/api/uploader/invoke_uploader.py +4 -5
- uncountable/types/api/user/__init__.py +1 -0
- uncountable/types/api/user/get_current_user_info.py +40 -0
- uncountable/types/async_batch.py +1 -1
- uncountable/types/async_batch_processor.py +506 -23
- uncountable/types/async_batch_t.py +35 -8
- uncountable/types/async_jobs.py +0 -1
- uncountable/types/async_jobs_t.py +1 -2
- uncountable/types/auth_retrieval.py +0 -1
- uncountable/types/auth_retrieval_t.py +6 -6
- uncountable/types/base.py +0 -1
- uncountable/types/base_t.py +11 -9
- uncountable/types/calculations.py +0 -1
- uncountable/types/calculations_t.py +1 -2
- uncountable/types/chemical_structure.py +0 -1
- uncountable/types/chemical_structure_t.py +5 -5
- uncountable/types/client_base.py +614 -69
- uncountable/types/client_config.py +1 -1
- uncountable/types/client_config_t.py +13 -3
- uncountable/types/curves.py +0 -1
- uncountable/types/curves_t.py +6 -7
- uncountable/types/data.py +12 -0
- uncountable/types/data_t.py +103 -0
- uncountable/types/entity.py +1 -1
- uncountable/types/entity_t.py +90 -10
- uncountable/types/experiment_groups.py +0 -1
- uncountable/types/experiment_groups_t.py +1 -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 +242 -9
- uncountable/types/fields.py +0 -1
- uncountable/types/fields_t.py +1 -2
- uncountable/types/generic_upload.py +0 -1
- uncountable/types/generic_upload_t.py +14 -14
- uncountable/types/id_source.py +0 -1
- uncountable/types/id_source_t.py +13 -7
- uncountable/types/identifier.py +0 -1
- uncountable/types/identifier_t.py +10 -5
- uncountable/types/input_attributes.py +0 -1
- uncountable/types/input_attributes_t.py +3 -4
- uncountable/types/inputs.py +0 -1
- uncountable/types/inputs_t.py +3 -4
- uncountable/types/integration_server.py +0 -1
- uncountable/types/integration_server_t.py +13 -4
- 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 +2 -1
- uncountable/types/job_definition_t.py +57 -32
- 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 +2 -3
- uncountable/types/overrides.py +0 -1
- uncountable/types/overrides_t.py +10 -4
- uncountable/types/permissions.py +0 -1
- uncountable/types/permissions_t.py +1 -2
- uncountable/types/phases.py +0 -1
- uncountable/types/phases_t.py +1 -2
- uncountable/types/post_base.py +0 -1
- uncountable/types/post_base_t.py +1 -2
- uncountable/types/queued_job.py +2 -1
- uncountable/types/queued_job_t.py +29 -12
- uncountable/types/recipe_identifiers.py +0 -1
- uncountable/types/recipe_identifiers_t.py +18 -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 +3 -4
- uncountable/types/recipe_metadata.py +0 -1
- uncountable/types/recipe_metadata_t.py +9 -10
- uncountable/types/recipe_output_metadata.py +0 -1
- uncountable/types/recipe_output_metadata_t.py +1 -2
- uncountable/types/recipe_tags.py +0 -1
- uncountable/types/recipe_tags_t.py +1 -2
- uncountable/types/recipe_workflow_steps.py +0 -1
- uncountable/types/recipe_workflow_steps_t.py +7 -7
- uncountable/types/recipes.py +0 -1
- uncountable/types/recipes_t.py +2 -2
- uncountable/types/response.py +0 -1
- uncountable/types/response_t.py +2 -2
- uncountable/types/secret_retrieval.py +0 -1
- uncountable/types/secret_retrieval_t.py +7 -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 +1 -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 +1 -2
- uncountable/types/webhook_job.py +1 -1
- uncountable/types/webhook_job_t.py +14 -3
- uncountable/types/workflows.py +0 -1
- uncountable/types/workflows_t.py +3 -4
- uncountablepythonsdk-0.0.132.dist-info/METADATA +64 -0
- uncountablepythonsdk-0.0.132.dist-info/RECORD +363 -0
- {UncountablePythonSDK-0.0.82.dist-info → uncountablepythonsdk-0.0.132.dist-info}/WHEEL +1 -1
- UncountablePythonSDK-0.0.82.dist-info/METADATA +0 -60
- UncountablePythonSDK-0.0.82.dist-info/RECORD +0 -292
- docs/quickstart.md +0 -19
- {UncountablePythonSDK-0.0.82.dist-info → uncountablepythonsdk-0.0.132.dist-info}/top_level.txt +0 -0
|
@@ -1,21 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import dataclasses
|
|
4
|
+
import datetime
|
|
2
5
|
import math
|
|
3
6
|
import types
|
|
4
7
|
import typing
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
5
9
|
from collections import defaultdict
|
|
6
|
-
from
|
|
7
|
-
from datetime import date, datetime
|
|
10
|
+
from datetime import date
|
|
8
11
|
from decimal import Decimal
|
|
12
|
+
from enum import Enum, auto
|
|
9
13
|
from importlib import resources
|
|
10
14
|
|
|
11
15
|
import dateutil.parser
|
|
16
|
+
import msgspec.yaml
|
|
12
17
|
|
|
13
18
|
from pkgs.serialization import (
|
|
14
19
|
MissingSentryType,
|
|
15
20
|
OpaqueKey,
|
|
16
21
|
get_serial_class_data,
|
|
17
22
|
get_serial_union_data,
|
|
18
|
-
yaml,
|
|
19
23
|
)
|
|
20
24
|
|
|
21
25
|
from ._is_enum import is_string_enum_class
|
|
@@ -27,13 +31,41 @@ ParserFunction = typing.Callable[[typing.Any], T]
|
|
|
27
31
|
ParserCache = dict[type[typing.Any], ParserFunction[typing.Any]]
|
|
28
32
|
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
class SourceEncoding(Enum):
|
|
35
|
+
API = auto()
|
|
36
|
+
STORAGE = auto()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclasses.dataclass(frozen=True, eq=True)
|
|
31
40
|
class ParserOptions:
|
|
32
|
-
|
|
41
|
+
encoding: SourceEncoding
|
|
33
42
|
strict_property_parsing: bool = False
|
|
34
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
|
|
35
66
|
|
|
36
|
-
|
|
67
|
+
|
|
68
|
+
@dataclasses.dataclass(frozen=True)
|
|
37
69
|
class ParserContext:
|
|
38
70
|
options: ParserOptions
|
|
39
71
|
cache: ParserCache
|
|
@@ -52,20 +84,38 @@ class ParserExtraFieldsError(ParserError):
|
|
|
52
84
|
return f"extra fields were provided: {', '.join(self.extra_fields)}"
|
|
53
85
|
|
|
54
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
|
+
|
|
55
92
|
def is_optional(field_type: typing.Any) -> bool:
|
|
56
|
-
return
|
|
57
|
-
None
|
|
58
|
-
) in typing.get_args(field_type)
|
|
93
|
+
return is_union(field_type) and type(None) in typing.get_args(field_type)
|
|
59
94
|
|
|
60
95
|
|
|
61
96
|
def is_missing(field_type: typing.Any) -> bool:
|
|
62
|
-
|
|
63
|
-
if origin is not typing.Union:
|
|
97
|
+
if not is_union(field_type):
|
|
64
98
|
return False
|
|
65
99
|
args = typing.get_args(field_type)
|
|
66
100
|
return not (len(args) == 0 or args[0] is not MissingSentryType)
|
|
67
101
|
|
|
68
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
|
+
|
|
69
119
|
def _invoke_tuple_parsers(
|
|
70
120
|
tuple_type: type[T],
|
|
71
121
|
arg_parsers: typing.Sequence[typing.Callable[[typing.Any], object]],
|
|
@@ -118,11 +168,39 @@ def _invoke_membership_parser(
|
|
|
118
168
|
raise ValueError(f"Expected value from {expected_values} but got value {value}")
|
|
119
169
|
|
|
120
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
|
+
|
|
121
188
|
def _build_parser_discriminated_union(
|
|
122
|
-
|
|
189
|
+
context: ParserContext,
|
|
190
|
+
discriminator_raw: str,
|
|
191
|
+
discriminator_map: dict[str, ParserFunction[T]],
|
|
123
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
|
+
|
|
124
199
|
def parse(value: typing.Any) -> typing.Any:
|
|
125
|
-
|
|
200
|
+
if context.options.allow_direct_type and dataclasses.is_dataclass(value):
|
|
201
|
+
discriminant = getattr(value, discriminator)
|
|
202
|
+
else:
|
|
203
|
+
discriminant = value.get(discriminator)
|
|
126
204
|
if discriminant is None:
|
|
127
205
|
raise ValueError("missing-union-discriminant")
|
|
128
206
|
if not isinstance(discriminant, str):
|
|
@@ -138,20 +216,10 @@ def _build_parser_discriminated_union(
|
|
|
138
216
|
def _build_parser_inner(
|
|
139
217
|
parsed_type: type[T],
|
|
140
218
|
context: ParserContext,
|
|
141
|
-
*,
|
|
142
|
-
convert_string_to_snake_case: bool = False,
|
|
143
219
|
) -> ParserFunction[T]:
|
|
144
220
|
"""
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
then the generated parser will convert camel to snake case case
|
|
148
|
-
should only be True for cases like dictionary keys
|
|
149
|
-
should only be True if options.convert_to_snake_case is True
|
|
150
|
-
|
|
151
|
-
NOTE: This argument makes caching at this level difficult, as the cache-map
|
|
152
|
-
would need to vary based on this argument. For this reason only dataclasses
|
|
153
|
-
are cached now, as they don't use the argument, and they're known to be safe.
|
|
154
|
-
This is also enough to support some recursion.
|
|
221
|
+
IMPROVE: We can now cache at this level, to avoid producing redundant
|
|
222
|
+
internal parsers.
|
|
155
223
|
"""
|
|
156
224
|
|
|
157
225
|
serial_union = get_serial_union_data(parsed_type)
|
|
@@ -163,6 +231,7 @@ def _build_parser_inner(
|
|
|
163
231
|
parsed_type = serial_union.get_union_underlying()
|
|
164
232
|
else:
|
|
165
233
|
return _build_parser_discriminated_union(
|
|
234
|
+
context,
|
|
166
235
|
discriminator,
|
|
167
236
|
{
|
|
168
237
|
key: _build_parser_inner(value, context)
|
|
@@ -171,7 +240,7 @@ def _build_parser_inner(
|
|
|
171
240
|
)
|
|
172
241
|
|
|
173
242
|
if dataclasses.is_dataclass(parsed_type):
|
|
174
|
-
return _build_parser_dataclass(parsed_type, context)
|
|
243
|
+
return _build_parser_dataclass(parsed_type, context)
|
|
175
244
|
|
|
176
245
|
# namedtuple support
|
|
177
246
|
if is_namedtuple_type(parsed_type):
|
|
@@ -184,15 +253,17 @@ def _build_parser_inner(
|
|
|
184
253
|
field_name: field_parser(
|
|
185
254
|
value.get(
|
|
186
255
|
snake_to_camel_case(field_name)
|
|
187
|
-
if context.options.
|
|
256
|
+
if context.options.from_camel_case
|
|
188
257
|
else field_name
|
|
189
258
|
)
|
|
190
259
|
)
|
|
191
260
|
for field_name, field_parser in field_parsers
|
|
192
261
|
})
|
|
193
262
|
|
|
263
|
+
# IMPROVE: unclear why we need == here
|
|
194
264
|
if parsed_type == type(None): # noqa: E721
|
|
195
|
-
|
|
265
|
+
# Need to convince type checker that parsed_type is type(None)
|
|
266
|
+
return typing.cast(ParserFunction[T], NONE_IDENTITY_PARSER)
|
|
196
267
|
|
|
197
268
|
origin = typing.get_origin(parsed_type)
|
|
198
269
|
if origin is tuple:
|
|
@@ -219,7 +290,7 @@ def _build_parser_inner(
|
|
|
219
290
|
arg_parsers = [_build_parser_inner(arg, context) for arg in sorted_args]
|
|
220
291
|
return lambda value: _invoke_fallback_parsers(parsed_type, arg_parsers, value)
|
|
221
292
|
|
|
222
|
-
if parsed_type is typing.Any:
|
|
293
|
+
if parsed_type is typing.Any:
|
|
223
294
|
return lambda value: value
|
|
224
295
|
|
|
225
296
|
if origin in (list, set):
|
|
@@ -245,23 +316,32 @@ def _build_parser_inner(
|
|
|
245
316
|
args = typing.get_args(parsed_type)
|
|
246
317
|
if len(args) != 2:
|
|
247
318
|
raise ValueError("Dict types only support two arguments for now")
|
|
248
|
-
|
|
319
|
+
k_inner_parser = _build_parser_inner(
|
|
249
320
|
args[0],
|
|
250
321
|
context,
|
|
251
|
-
convert_string_to_snake_case=context.options.convert_to_snake_case,
|
|
252
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
|
+
|
|
253
336
|
v_parser = _build_parser_inner(args[1], context)
|
|
254
337
|
return lambda value: origin(
|
|
255
|
-
(
|
|
338
|
+
(key_parser(k), v_parser(v)) for k, v in value.items()
|
|
256
339
|
)
|
|
257
340
|
|
|
258
341
|
if origin == typing.Literal:
|
|
259
342
|
valid_values: set[T] = set(typing.get_args(parsed_type))
|
|
260
343
|
return lambda value: _invoke_membership_parser(valid_values, value)
|
|
261
344
|
|
|
262
|
-
if parsed_type is str and convert_string_to_snake_case:
|
|
263
|
-
return lambda value: camel_to_snake_case(value) # type: ignore
|
|
264
|
-
|
|
265
345
|
if parsed_type is int:
|
|
266
346
|
# first parse ints to decimal to allow scientific notation and decimals
|
|
267
347
|
# e.g. (1) 1e4 => 1000, (2) 3.0 => 3
|
|
@@ -281,11 +361,25 @@ def _build_parser_inner(
|
|
|
281
361
|
|
|
282
362
|
return parse_int
|
|
283
363
|
|
|
284
|
-
if parsed_type is datetime:
|
|
285
|
-
|
|
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
|
|
286
374
|
|
|
287
375
|
if parsed_type is date:
|
|
288
|
-
|
|
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
|
|
289
383
|
|
|
290
384
|
# MyPy: It's unclear why `parsed_type in (str, OpaqueKey)` is flagged as invalid
|
|
291
385
|
# Thus an or statement is used instead, which isn't flagged as invalid.
|
|
@@ -320,7 +414,17 @@ def _build_parser_inner(
|
|
|
320
414
|
raise ValueError("Missing type cannot be parsed directly")
|
|
321
415
|
|
|
322
416
|
return error
|
|
323
|
-
|
|
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}")
|
|
324
428
|
|
|
325
429
|
|
|
326
430
|
def _build_parser_dataclass(
|
|
@@ -335,8 +439,7 @@ def _build_parser_dataclass(
|
|
|
335
439
|
cur_parser = context.cache.get(parsed_type)
|
|
336
440
|
if cur_parser is not None:
|
|
337
441
|
return cur_parser
|
|
338
|
-
|
|
339
|
-
type_hints = typing.get_type_hints(parsed_type)
|
|
442
|
+
type_hints = typing.get_type_hints(parsed_type, include_extras=True)
|
|
340
443
|
dc_field_parsers: list[
|
|
341
444
|
tuple[
|
|
342
445
|
dataclasses.Field[typing.Any],
|
|
@@ -351,29 +454,31 @@ def _build_parser_dataclass(
|
|
|
351
454
|
return (
|
|
352
455
|
snake_to_camel_case(field_name)
|
|
353
456
|
if (
|
|
354
|
-
context.options.
|
|
457
|
+
context.options.from_camel_case
|
|
355
458
|
and not serial_class_data.has_unconverted_key(field_name)
|
|
356
459
|
)
|
|
357
460
|
else field_name
|
|
358
461
|
)
|
|
359
462
|
|
|
360
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
|
+
|
|
361
468
|
data: dict[typing.Any, typing.Any] = {}
|
|
362
469
|
for field, field_type, field_parser in dc_field_parsers:
|
|
363
470
|
field_raw_value = None
|
|
364
471
|
try:
|
|
365
472
|
field_raw_value = value.get(
|
|
366
473
|
resolve_serialized_field_name(field_name=field.name),
|
|
367
|
-
MISSING,
|
|
474
|
+
dataclasses.MISSING,
|
|
368
475
|
)
|
|
369
476
|
field_value: typing.Any
|
|
370
|
-
if field_raw_value == MISSING:
|
|
477
|
+
if field_raw_value == dataclasses.MISSING:
|
|
371
478
|
if serial_class_data.has_parse_require(field.name):
|
|
372
479
|
raise ValueError("missing-required-field", field.name)
|
|
373
|
-
if field
|
|
374
|
-
field_value = field
|
|
375
|
-
elif field.default_factory != MISSING:
|
|
376
|
-
field_value = field.default_factory()
|
|
480
|
+
if _has_field_default(field):
|
|
481
|
+
field_value = _get_field_default(field)
|
|
377
482
|
elif is_missing(field_type):
|
|
378
483
|
field_value = MissingSentryType()
|
|
379
484
|
elif is_optional(field_type):
|
|
@@ -385,6 +490,13 @@ def _build_parser_dataclass(
|
|
|
385
490
|
field_value = False
|
|
386
491
|
else:
|
|
387
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)
|
|
388
500
|
elif serial_class_data.has_unconverted_value(field.name):
|
|
389
501
|
field_value = field_raw_value
|
|
390
502
|
else:
|
|
@@ -452,15 +564,46 @@ def build_parser(
|
|
|
452
564
|
return built_parser
|
|
453
565
|
|
|
454
566
|
|
|
455
|
-
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]):
|
|
456
599
|
def __init__(
|
|
457
600
|
self,
|
|
458
601
|
args: type[T],
|
|
459
602
|
strict_property_parsing: bool = False,
|
|
460
603
|
):
|
|
461
604
|
self.arguments = args
|
|
462
|
-
self.parser_api:
|
|
463
|
-
self.parser_storage:
|
|
605
|
+
self.parser_api: ParserFunction[T] | None = None
|
|
606
|
+
self.parser_storage: ParserFunction[T] | None = None
|
|
464
607
|
self.strict_property_parsing = strict_property_parsing
|
|
465
608
|
|
|
466
609
|
def parse_api(self, args: typing.Any) -> T:
|
|
@@ -474,8 +617,7 @@ class CachedParser(typing.Generic[T]):
|
|
|
474
617
|
if self.parser_api is None:
|
|
475
618
|
self.parser_api = build_parser(
|
|
476
619
|
self.arguments,
|
|
477
|
-
ParserOptions(
|
|
478
|
-
convert_to_snake_case=True,
|
|
620
|
+
ParserOptions.Api(
|
|
479
621
|
strict_property_parsing=self.strict_property_parsing,
|
|
480
622
|
),
|
|
481
623
|
)
|
|
@@ -489,18 +631,9 @@ class CachedParser(typing.Generic[T]):
|
|
|
489
631
|
if self.parser_storage is None:
|
|
490
632
|
self.parser_storage = build_parser(
|
|
491
633
|
self.arguments,
|
|
492
|
-
ParserOptions(
|
|
493
|
-
convert_to_snake_case=False,
|
|
634
|
+
ParserOptions.Storage(
|
|
494
635
|
strict_property_parsing=self.strict_property_parsing,
|
|
495
636
|
),
|
|
496
637
|
)
|
|
497
638
|
assert self.parser_storage is not None
|
|
498
639
|
return self.parser_storage(args)
|
|
499
|
-
|
|
500
|
-
def parse_yaml_file(self, path: str) -> T:
|
|
501
|
-
with open(path, encoding="utf-8") as data_in:
|
|
502
|
-
return self.parse_storage(yaml.safe_load(data_in))
|
|
503
|
-
|
|
504
|
-
def parse_yaml_resource(self, package: resources.Package, resource: str) -> T:
|
|
505
|
-
with resources.open_text(package, resource) as fp:
|
|
506
|
-
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()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from io import BytesIO
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any
|
|
4
4
|
|
|
5
5
|
from google.oauth2 import service_account
|
|
6
6
|
from googleapiclient.discovery import build as build_gdrive_connection
|
|
@@ -30,7 +30,7 @@ def download_gdrive_file(
|
|
|
30
30
|
mime_type: str,
|
|
31
31
|
*,
|
|
32
32
|
verbose: bool = False,
|
|
33
|
-
) ->
|
|
33
|
+
) -> FileObjectData | None:
|
|
34
34
|
if "folder" in mime_type:
|
|
35
35
|
if verbose:
|
|
36
36
|
print(f"{filename} is a folder and will not be downloaded.")
|
|
@@ -63,7 +63,7 @@ def download_gdrive_file(
|
|
|
63
63
|
downloader = MediaIoBaseDownload(file_handler, file_request)
|
|
64
64
|
download_complete = False
|
|
65
65
|
while not download_complete:
|
|
66
|
-
|
|
66
|
+
_status, download_complete = downloader.next_chunk()
|
|
67
67
|
|
|
68
68
|
file_handler.seek(0)
|
|
69
69
|
file_data = file_handler.read()
|
|
@@ -148,7 +148,7 @@ def move_gdrive_file(
|
|
|
148
148
|
src_file_id: str,
|
|
149
149
|
dest_folder_id: str,
|
|
150
150
|
*,
|
|
151
|
-
dest_filename:
|
|
151
|
+
dest_filename: str | None = None,
|
|
152
152
|
) -> None:
|
|
153
153
|
# Retrieve the existing parents to remove
|
|
154
154
|
file = (
|
|
@@ -197,7 +197,7 @@ class GDriveSession(FileSystemSession):
|
|
|
197
197
|
dir_path: FileSystemObject,
|
|
198
198
|
*,
|
|
199
199
|
recursive: bool = False,
|
|
200
|
-
valid_file_extensions:
|
|
200
|
+
valid_file_extensions: tuple[str, ...] | None = None,
|
|
201
201
|
) -> list[FileSystemObject]:
|
|
202
202
|
if not isinstance(dir_path, RemoteObjectReference):
|
|
203
203
|
raise IncompatibleFileReference(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from io import BytesIO
|
|
2
2
|
|
|
3
3
|
from boto3.session import Session
|
|
4
|
+
from mypy_boto3_s3.service_resource import Bucket
|
|
4
5
|
|
|
5
6
|
from pkgs.filesystem_utils.file_type_utils import (
|
|
6
7
|
FileObjectData,
|
|
@@ -37,7 +38,7 @@ class S3Session(FileSystemSession):
|
|
|
37
38
|
aws_session_token=self.config.session_token,
|
|
38
39
|
)
|
|
39
40
|
|
|
40
|
-
self.bucket = s3_resource.Bucket(self.config.bucket_name)
|
|
41
|
+
self.bucket: Bucket | None = s3_resource.Bucket(self.config.bucket_name)
|
|
41
42
|
|
|
42
43
|
def __enter__(self) -> "S3Session":
|
|
43
44
|
self.start()
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from collections.abc import Iterable
|
|
3
3
|
from io import BytesIO
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
import paramiko
|
|
7
6
|
import pysftp
|
|
@@ -30,8 +29,8 @@ def list_sftp_files(
|
|
|
30
29
|
connection: pysftp.Connection,
|
|
31
30
|
dir_path: str,
|
|
32
31
|
*,
|
|
33
|
-
valid_extensions:
|
|
34
|
-
parent_dir_path:
|
|
32
|
+
valid_extensions: Iterable[str] | None = None,
|
|
33
|
+
parent_dir_path: str | None = None,
|
|
35
34
|
recursive: bool = True,
|
|
36
35
|
) -> list[str]:
|
|
37
36
|
file_paths: list[str] = []
|
|
@@ -55,6 +54,10 @@ def list_sftp_files(
|
|
|
55
54
|
os.path.join(dir_path, file)
|
|
56
55
|
for file in connection.listdir(dir_path)
|
|
57
56
|
if connection.isfile(os.path.join(dir_path, file))
|
|
57
|
+
and (
|
|
58
|
+
valid_extensions is None
|
|
59
|
+
or os.path.splitext(file)[1] in valid_extensions
|
|
60
|
+
)
|
|
58
61
|
])
|
|
59
62
|
return file_paths
|
|
60
63
|
|