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
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from google.oauth2 import service_account
|
|
6
|
+
from googleapiclient.discovery import build as build_gdrive_connection
|
|
7
|
+
from googleapiclient.errors import HttpError
|
|
8
|
+
from googleapiclient.http import MediaIoBaseDownload, MediaIoBaseUpload
|
|
9
|
+
from tqdm import tqdm
|
|
10
|
+
|
|
11
|
+
from pkgs.filesystem_utils.file_type_utils import (
|
|
12
|
+
FileObjectData,
|
|
13
|
+
FileSystemFileReference,
|
|
14
|
+
FileSystemObject,
|
|
15
|
+
FileTransfer,
|
|
16
|
+
IncompatibleFileReference,
|
|
17
|
+
RemoteObjectReference,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from .filesystem_session import FileSystemSession
|
|
21
|
+
|
|
22
|
+
# NOTE: google apis do not have static types
|
|
23
|
+
GDriveResource = Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def download_gdrive_file(
|
|
27
|
+
gdrive_connection: GDriveResource,
|
|
28
|
+
file_id: str,
|
|
29
|
+
filename: str,
|
|
30
|
+
mime_type: str,
|
|
31
|
+
*,
|
|
32
|
+
verbose: bool = False,
|
|
33
|
+
) -> FileObjectData | None:
|
|
34
|
+
if "folder" in mime_type:
|
|
35
|
+
if verbose:
|
|
36
|
+
print(f"{filename} is a folder and will not be downloaded.")
|
|
37
|
+
return None
|
|
38
|
+
elif "google-apps" in mime_type:
|
|
39
|
+
# Handle google workspace doc
|
|
40
|
+
if "spreadsheet" in mime_type:
|
|
41
|
+
if verbose:
|
|
42
|
+
print(f"{filename} is a Google Sheet, exporting.")
|
|
43
|
+
file_request = gdrive_connection.files().export_media(
|
|
44
|
+
fileId=file_id, mimeType="text/csv"
|
|
45
|
+
)
|
|
46
|
+
filename += ".csv"
|
|
47
|
+
elif "document" in mime_type:
|
|
48
|
+
if verbose:
|
|
49
|
+
print(f"{filename} is a Google Doc, exporting.")
|
|
50
|
+
file_request = gdrive_connection.files().export_media(
|
|
51
|
+
fileId=file_id, mimeType="application/msword"
|
|
52
|
+
)
|
|
53
|
+
filename += ".doc"
|
|
54
|
+
else:
|
|
55
|
+
if verbose:
|
|
56
|
+
print(f"{filename} is an unsupported google workspace filetype.")
|
|
57
|
+
print(f"Skipping. mimeType: {mime_type}.")
|
|
58
|
+
return None
|
|
59
|
+
else:
|
|
60
|
+
file_request = gdrive_connection.files().get_media(fileId=file_id)
|
|
61
|
+
|
|
62
|
+
file_handler = BytesIO()
|
|
63
|
+
downloader = MediaIoBaseDownload(file_handler, file_request)
|
|
64
|
+
download_complete = False
|
|
65
|
+
while not download_complete:
|
|
66
|
+
status, download_complete = downloader.next_chunk()
|
|
67
|
+
|
|
68
|
+
file_handler.seek(0)
|
|
69
|
+
file_data = file_handler.read()
|
|
70
|
+
return FileObjectData(
|
|
71
|
+
file_data=file_data,
|
|
72
|
+
file_IO=BytesIO(file_data),
|
|
73
|
+
filename=filename,
|
|
74
|
+
filepath=file_id,
|
|
75
|
+
metadata={"id": file_id},
|
|
76
|
+
mime_type=mime_type,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def list_gdrive_files(
|
|
81
|
+
gdrive_connection: GDriveResource, gdrive_folder_id: str, *, recurse: bool = False
|
|
82
|
+
) -> list[dict[str, str]]:
|
|
83
|
+
query = f"parents = '{gdrive_folder_id}'"
|
|
84
|
+
print("Listing files", end="", flush=True)
|
|
85
|
+
paginated_files_in_folder = [
|
|
86
|
+
(
|
|
87
|
+
gdrive_connection.files()
|
|
88
|
+
.list(
|
|
89
|
+
q=query,
|
|
90
|
+
corpora="allDrives",
|
|
91
|
+
includeItemsFromAllDrives=True,
|
|
92
|
+
supportsAllDrives=True,
|
|
93
|
+
)
|
|
94
|
+
.execute()
|
|
95
|
+
)
|
|
96
|
+
]
|
|
97
|
+
while paginated_files_in_folder[-1].get("nextPageToken") is not None:
|
|
98
|
+
print(".", end="", flush=True)
|
|
99
|
+
paginated_files_in_folder.append(
|
|
100
|
+
gdrive_connection.files()
|
|
101
|
+
.list(
|
|
102
|
+
q=query,
|
|
103
|
+
corpora="allDrives",
|
|
104
|
+
includeItemsFromAllDrives=True,
|
|
105
|
+
supportsAllDrives=True,
|
|
106
|
+
pageToken=paginated_files_in_folder[-1]["nextPageToken"],
|
|
107
|
+
)
|
|
108
|
+
.execute()
|
|
109
|
+
)
|
|
110
|
+
print()
|
|
111
|
+
# Get available files: https://developers.google.com/drive/api/v3/manage-downloads#python
|
|
112
|
+
files: list[dict[str, str]] = []
|
|
113
|
+
for files_in_folder in paginated_files_in_folder:
|
|
114
|
+
files.extend(files_in_folder.get("files", []))
|
|
115
|
+
subfiles: list[dict[str, str]] = []
|
|
116
|
+
if recurse:
|
|
117
|
+
for file in files:
|
|
118
|
+
if file["mimeType"] == "application/vnd.google-apps.folder":
|
|
119
|
+
subfiles.extend(
|
|
120
|
+
list_gdrive_files(
|
|
121
|
+
gdrive_connection=gdrive_connection,
|
|
122
|
+
gdrive_folder_id=file["id"],
|
|
123
|
+
recurse=True,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
return [*files, *subfiles]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def upload_file_gdrive(
|
|
130
|
+
gdrive_connection: GDriveResource,
|
|
131
|
+
src_file: BytesIO,
|
|
132
|
+
mime_type: str,
|
|
133
|
+
dest_folder_id: str,
|
|
134
|
+
dest_filename: str,
|
|
135
|
+
) -> None:
|
|
136
|
+
file_metadata = {"name": dest_filename, "parents": [dest_folder_id]}
|
|
137
|
+
media = MediaIoBaseUpload(src_file, mimetype=mime_type)
|
|
138
|
+
try:
|
|
139
|
+
gdrive_connection.files().create(
|
|
140
|
+
body=file_metadata, media_body=media, fields="id", supportsAllDrives=True
|
|
141
|
+
).execute()
|
|
142
|
+
except HttpError:
|
|
143
|
+
print("FileSystemObject Upload to GDrive Unsuccessful")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def move_gdrive_file(
|
|
147
|
+
gdrive_connection: GDriveResource,
|
|
148
|
+
src_file_id: str,
|
|
149
|
+
dest_folder_id: str,
|
|
150
|
+
*,
|
|
151
|
+
dest_filename: str | None = None,
|
|
152
|
+
) -> None:
|
|
153
|
+
# Retrieve the existing parents to remove
|
|
154
|
+
file = (
|
|
155
|
+
gdrive_connection.files()
|
|
156
|
+
.get(fileId=src_file_id, fields="parents, name", supportsTeamDrives=True)
|
|
157
|
+
.execute()
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
new_filename = file["name"]
|
|
161
|
+
if dest_filename is not None:
|
|
162
|
+
new_filename = dest_filename
|
|
163
|
+
previous_parents = ",".join(file.get("parents"))
|
|
164
|
+
metadata = {"name": new_filename}
|
|
165
|
+
gdrive_connection.files().update(
|
|
166
|
+
fileId=src_file_id, body=metadata, fields="name", supportsTeamDrives=True
|
|
167
|
+
).execute()
|
|
168
|
+
gdrive_connection.files().update(
|
|
169
|
+
fileId=src_file_id,
|
|
170
|
+
addParents=dest_folder_id,
|
|
171
|
+
removeParents=previous_parents,
|
|
172
|
+
fields="id, parents",
|
|
173
|
+
supportsTeamDrives=True,
|
|
174
|
+
).execute()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def delete_gdrive_file(gdrive_connection: GDriveResource, file_id: str) -> None:
|
|
178
|
+
gdrive_connection.files().delete(fileId=file_id, supportsAllDrives=True).execute()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class GDriveSession(FileSystemSession):
|
|
182
|
+
def __init__(self, service_account_json_path: str) -> None:
|
|
183
|
+
super().__init__()
|
|
184
|
+
self.service_account_json_path = service_account_json_path
|
|
185
|
+
|
|
186
|
+
def start(self) -> None:
|
|
187
|
+
credentials = service_account.Credentials.from_service_account_file( # type: ignore[no-untyped-call]
|
|
188
|
+
self.service_account_json_path
|
|
189
|
+
)
|
|
190
|
+
gdrive_connection = build_gdrive_connection(
|
|
191
|
+
"drive", "v3", credentials=credentials
|
|
192
|
+
)
|
|
193
|
+
self.connection = gdrive_connection
|
|
194
|
+
|
|
195
|
+
def list_files(
|
|
196
|
+
self,
|
|
197
|
+
dir_path: FileSystemObject,
|
|
198
|
+
*,
|
|
199
|
+
recursive: bool = False,
|
|
200
|
+
valid_file_extensions: tuple[str, ...] | None = None,
|
|
201
|
+
) -> list[FileSystemObject]:
|
|
202
|
+
if not isinstance(dir_path, RemoteObjectReference):
|
|
203
|
+
raise IncompatibleFileReference(
|
|
204
|
+
"Incompatible FileSystemObject to GDriveSession.list_files"
|
|
205
|
+
)
|
|
206
|
+
if not dir_path.is_dir:
|
|
207
|
+
raise IncompatibleFileReference(
|
|
208
|
+
"FileSystemObject does not reference a directory"
|
|
209
|
+
)
|
|
210
|
+
files = list_gdrive_files(self.connection, dir_path.file_id, recurse=recursive)
|
|
211
|
+
gdrive_files: list[FileSystemObject] = []
|
|
212
|
+
for file_context in files:
|
|
213
|
+
if (
|
|
214
|
+
valid_file_extensions is not None
|
|
215
|
+
and os.path.splitext(file_context["name"])[1]
|
|
216
|
+
not in valid_file_extensions
|
|
217
|
+
):
|
|
218
|
+
continue
|
|
219
|
+
gdrive_files.append(
|
|
220
|
+
RemoteObjectReference(
|
|
221
|
+
file_id=file_context["id"],
|
|
222
|
+
mime_type=file_context["mimeType"],
|
|
223
|
+
filename=file_context["name"],
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
return gdrive_files
|
|
227
|
+
|
|
228
|
+
def delete_files(self, filepaths: list[FileSystemObject]) -> None:
|
|
229
|
+
"""Warning:
|
|
230
|
+
Security account must have sufficient permissions to perform delete!
|
|
231
|
+
https://developers.google.com/drive/api/v3/reference/files/delete?hl=en
|
|
232
|
+
https://developers.google.com/drive/api/v3/ref-roles
|
|
233
|
+
"""
|
|
234
|
+
for file_object in filepaths:
|
|
235
|
+
if not isinstance(file_object, RemoteObjectReference):
|
|
236
|
+
raise IncompatibleFileReference(
|
|
237
|
+
"Incompatible FileSystemObject provided to GDriveSession.delete_files"
|
|
238
|
+
)
|
|
239
|
+
delete_gdrive_file(self.connection, file_object.file_id)
|
|
240
|
+
|
|
241
|
+
def move_files(self, file_mappings: list[FileTransfer]) -> None:
|
|
242
|
+
for src_file, dest_file in file_mappings:
|
|
243
|
+
if (
|
|
244
|
+
isinstance(src_file, FileSystemFileReference)
|
|
245
|
+
or not isinstance(dest_file, RemoteObjectReference)
|
|
246
|
+
or not dest_file.is_dir
|
|
247
|
+
or (isinstance(src_file, RemoteObjectReference) and src_file.is_dir)
|
|
248
|
+
):
|
|
249
|
+
continue
|
|
250
|
+
new_filename = dest_file.filename
|
|
251
|
+
if isinstance(src_file, RemoteObjectReference):
|
|
252
|
+
if new_filename is not None:
|
|
253
|
+
move_gdrive_file(
|
|
254
|
+
self.connection,
|
|
255
|
+
src_file.file_id,
|
|
256
|
+
dest_file.file_id,
|
|
257
|
+
dest_filename=new_filename,
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
move_gdrive_file(
|
|
261
|
+
self.connection, src_file.file_id, dest_file.file_id
|
|
262
|
+
)
|
|
263
|
+
elif isinstance(src_file, FileObjectData):
|
|
264
|
+
if src_file.mime_type is None:
|
|
265
|
+
raise IncompatibleFileReference(
|
|
266
|
+
"No mime_type present on source file data."
|
|
267
|
+
)
|
|
268
|
+
new_filename = src_file.filename
|
|
269
|
+
if dest_file.filename is not None:
|
|
270
|
+
new_filename = dest_file.filename
|
|
271
|
+
upload_file_gdrive(
|
|
272
|
+
self.connection,
|
|
273
|
+
src_file.file_IO,
|
|
274
|
+
src_file.mime_type,
|
|
275
|
+
dest_file.file_id,
|
|
276
|
+
new_filename,
|
|
277
|
+
)
|
|
278
|
+
else:
|
|
279
|
+
raise IncompatibleFileReference(
|
|
280
|
+
"Unrecognized file reference in FileTransfer object"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def download_files(self, filepaths: list[FileSystemObject]) -> list[FileObjectData]:
|
|
284
|
+
downloaded_files: list[FileObjectData] = []
|
|
285
|
+
print(f"Downloading {len(filepaths)} files")
|
|
286
|
+
for file_object in tqdm(filepaths):
|
|
287
|
+
if (
|
|
288
|
+
not isinstance(file_object, RemoteObjectReference)
|
|
289
|
+
or file_object.filename is None
|
|
290
|
+
):
|
|
291
|
+
raise IncompatibleFileReference(
|
|
292
|
+
"Incompatible FileSystemObject included in filepaths"
|
|
293
|
+
)
|
|
294
|
+
downloaded_file = download_gdrive_file(
|
|
295
|
+
self.connection,
|
|
296
|
+
file_object.file_id,
|
|
297
|
+
file_object.filename,
|
|
298
|
+
file_object.mime_type,
|
|
299
|
+
)
|
|
300
|
+
if downloaded_file is not None:
|
|
301
|
+
downloaded_files.append(downloaded_file)
|
|
302
|
+
return downloaded_files
|
|
303
|
+
|
|
304
|
+
def __enter__(self) -> "GDriveSession":
|
|
305
|
+
self.start()
|
|
306
|
+
return self
|
|
307
|
+
|
|
308
|
+
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
|
|
309
|
+
self.connection.close()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
|
|
4
|
+
from pkgs.filesystem_utils.file_type_utils import (
|
|
5
|
+
FileObjectData,
|
|
6
|
+
FileSystemFileReference,
|
|
7
|
+
FileSystemObject,
|
|
8
|
+
FileTransfer,
|
|
9
|
+
IncompatibleFileReference,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from .filesystem_session import FileSystemSession
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LocalSession(FileSystemSession):
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
super().__init__()
|
|
18
|
+
|
|
19
|
+
def start(self) -> None:
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
def __enter__(self) -> "LocalSession":
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
def move_files(self, file_mappings: list[FileTransfer]) -> None:
|
|
29
|
+
for src_file, dest_file in file_mappings:
|
|
30
|
+
if not (
|
|
31
|
+
isinstance(src_file, FileSystemFileReference)
|
|
32
|
+
and isinstance(dest_file, FileSystemFileReference)
|
|
33
|
+
):
|
|
34
|
+
raise IncompatibleFileReference()
|
|
35
|
+
os.rename(src_file.filepath, dest_file.filepath)
|
|
36
|
+
|
|
37
|
+
def download_files(self, filepaths: list[FileSystemObject]) -> list[FileObjectData]:
|
|
38
|
+
downloaded_files: list[FileObjectData] = []
|
|
39
|
+
for file_object in filepaths:
|
|
40
|
+
if (
|
|
41
|
+
not isinstance(file_object, FileSystemFileReference)
|
|
42
|
+
or file_object.filename is None
|
|
43
|
+
):
|
|
44
|
+
raise IncompatibleFileReference()
|
|
45
|
+
with open(file_object.filepath, "rb") as file_data:
|
|
46
|
+
file_bytes = file_data.read()
|
|
47
|
+
downloaded_files.append(
|
|
48
|
+
FileObjectData(
|
|
49
|
+
file_bytes,
|
|
50
|
+
BytesIO(file_bytes),
|
|
51
|
+
file_object.filename,
|
|
52
|
+
filepath=file_object.filepath,
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
return downloaded_files
|
|
56
|
+
|
|
57
|
+
def list_files(
|
|
58
|
+
self, dir_path: FileSystemObject, *, recursive: bool = False
|
|
59
|
+
) -> list[FileSystemObject]:
|
|
60
|
+
if not isinstance(dir_path, FileSystemFileReference) or not os.path.isdir(
|
|
61
|
+
dir_path.filepath
|
|
62
|
+
):
|
|
63
|
+
raise IncompatibleFileReference()
|
|
64
|
+
if recursive:
|
|
65
|
+
raise NotImplementedError("recursive not implemented for local session")
|
|
66
|
+
return [
|
|
67
|
+
FileSystemFileReference(os.path.join(dir_path.filepath, filename))
|
|
68
|
+
for filename in os.listdir(dir_path.filepath)
|
|
69
|
+
]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from io import BytesIO
|
|
2
|
+
|
|
3
|
+
from boto3.session import Session
|
|
4
|
+
|
|
5
|
+
from pkgs.filesystem_utils.file_type_utils import (
|
|
6
|
+
FileObjectData,
|
|
7
|
+
FileSystemFileReference,
|
|
8
|
+
FileSystemObject,
|
|
9
|
+
FileSystemS3Config,
|
|
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 S3Session(FileSystemSession):
|
|
24
|
+
config: FileSystemS3Config
|
|
25
|
+
|
|
26
|
+
def __init__(self, s3_config: FileSystemS3Config) -> None:
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.config = s3_config
|
|
29
|
+
|
|
30
|
+
def start(self) -> None:
|
|
31
|
+
session = Session(region_name=self.config.region_name)
|
|
32
|
+
s3_resource = session.resource(
|
|
33
|
+
service_name="s3",
|
|
34
|
+
endpoint_url=self.config.endpoint_url,
|
|
35
|
+
aws_access_key_id=self.config.access_key_id,
|
|
36
|
+
aws_secret_access_key=self.config.secret_access_key,
|
|
37
|
+
aws_session_token=self.config.session_token,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
self.bucket = s3_resource.Bucket(self.config.bucket_name)
|
|
41
|
+
|
|
42
|
+
def __enter__(self) -> "S3Session":
|
|
43
|
+
self.start()
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
|
|
47
|
+
self.bucket = None
|
|
48
|
+
|
|
49
|
+
def list_files(
|
|
50
|
+
self,
|
|
51
|
+
dir_path: FileSystemObject,
|
|
52
|
+
*,
|
|
53
|
+
recursive: bool = False,
|
|
54
|
+
valid_extensions: list[str] | None = None,
|
|
55
|
+
) -> list[FileSystemObject]:
|
|
56
|
+
if not isinstance(dir_path, FileSystemFileReference):
|
|
57
|
+
raise IncompatibleFileReference()
|
|
58
|
+
|
|
59
|
+
assert self.bucket is not None, "call to list_files on uninitialized s3 session"
|
|
60
|
+
|
|
61
|
+
filesystem_references: list[FileSystemObject] = []
|
|
62
|
+
prefix = _add_slash(dir_path.filepath)
|
|
63
|
+
for obj in self.bucket.objects.filter(Prefix=prefix):
|
|
64
|
+
if not recursive and (obj.key == prefix or "/" in obj.key[len(prefix) :]):
|
|
65
|
+
continue
|
|
66
|
+
if valid_extensions is None or any(
|
|
67
|
+
obj.key.endswith(valid_extension)
|
|
68
|
+
for valid_extension in valid_extensions
|
|
69
|
+
):
|
|
70
|
+
filesystem_references.append(FileSystemFileReference(obj.key))
|
|
71
|
+
|
|
72
|
+
return filesystem_references
|
|
73
|
+
|
|
74
|
+
def download_files(
|
|
75
|
+
self,
|
|
76
|
+
filepaths: list[FileSystemObject],
|
|
77
|
+
) -> list[FileObjectData]:
|
|
78
|
+
downloaded_files: list[FileObjectData] = []
|
|
79
|
+
assert self.bucket is not None, (
|
|
80
|
+
"call to download_files on uninitialized s3 session"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
for file_object in filepaths:
|
|
84
|
+
if (
|
|
85
|
+
not isinstance(file_object, FileSystemFileReference)
|
|
86
|
+
or file_object.filename is None
|
|
87
|
+
):
|
|
88
|
+
raise IncompatibleFileReference()
|
|
89
|
+
s3_file_obj = self.bucket.Object(file_object.filepath)
|
|
90
|
+
response = s3_file_obj.get()
|
|
91
|
+
file_obj_bytes = response["Body"].read()
|
|
92
|
+
downloaded_files.append(
|
|
93
|
+
FileObjectData(
|
|
94
|
+
file_data=file_obj_bytes,
|
|
95
|
+
file_IO=BytesIO(file_obj_bytes),
|
|
96
|
+
filename=file_object.filename,
|
|
97
|
+
filepath=file_object.filepath,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return downloaded_files
|
|
102
|
+
|
|
103
|
+
def move_files(self, file_mappings: list[FileTransfer]) -> None:
|
|
104
|
+
assert self.bucket is not None, "call to move_files on uninitialized s3 session"
|
|
105
|
+
|
|
106
|
+
for src_file, dest_file in file_mappings:
|
|
107
|
+
if not isinstance(src_file, FileSystemFileReference) or not isinstance(
|
|
108
|
+
dest_file, FileSystemFileReference
|
|
109
|
+
):
|
|
110
|
+
raise IncompatibleFileReference()
|
|
111
|
+
self.bucket.Object(dest_file.filepath).copy_from(
|
|
112
|
+
CopySource={
|
|
113
|
+
"Bucket": self.bucket.name,
|
|
114
|
+
"Key": src_file.filepath,
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
self.bucket.Object(src_file.filepath).delete()
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
|
|
5
|
+
import paramiko
|
|
6
|
+
import pysftp
|
|
7
|
+
|
|
8
|
+
from pkgs.filesystem_utils.file_type_utils import (
|
|
9
|
+
FileObjectData,
|
|
10
|
+
FileSystemFileReference,
|
|
11
|
+
FileSystemObject,
|
|
12
|
+
FileSystemSFTPConfig,
|
|
13
|
+
FileTransfer,
|
|
14
|
+
IncompatibleFileReference,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .filesystem_session import FileSystemSession
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def move_sftp_files(
|
|
21
|
+
connection: pysftp.Connection,
|
|
22
|
+
src_filepath: str,
|
|
23
|
+
dest_filepath: str,
|
|
24
|
+
) -> None:
|
|
25
|
+
connection.rename(src_filepath, dest_filepath)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def list_sftp_files(
|
|
29
|
+
connection: pysftp.Connection,
|
|
30
|
+
dir_path: str,
|
|
31
|
+
*,
|
|
32
|
+
valid_extensions: Iterable[str] | None = None,
|
|
33
|
+
parent_dir_path: str | None = None,
|
|
34
|
+
recursive: bool = True,
|
|
35
|
+
) -> list[str]:
|
|
36
|
+
file_paths: list[str] = []
|
|
37
|
+
if recursive:
|
|
38
|
+
|
|
39
|
+
def _skip(name: str) -> None:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
def _add_file(path: str) -> None:
|
|
43
|
+
if (
|
|
44
|
+
valid_extensions is None
|
|
45
|
+
or os.path.splitext(path)[1] in valid_extensions
|
|
46
|
+
) and (parent_dir_path is None or os.path.dirname(path) == parent_dir_path):
|
|
47
|
+
file_paths.append(path)
|
|
48
|
+
|
|
49
|
+
connection.walktree(
|
|
50
|
+
dir_path, fcallback=_add_file, dcallback=_skip, ucallback=_skip
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
file_paths.extend([
|
|
54
|
+
os.path.join(dir_path, file)
|
|
55
|
+
for file in connection.listdir(dir_path)
|
|
56
|
+
if connection.isfile(os.path.join(dir_path, file))
|
|
57
|
+
])
|
|
58
|
+
return file_paths
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SFTPSession(FileSystemSession):
|
|
62
|
+
def __init__(self, sftp_config: FileSystemSFTPConfig) -> None:
|
|
63
|
+
super().__init__()
|
|
64
|
+
self.host: str = sftp_config.ip
|
|
65
|
+
self.username: str = sftp_config.username
|
|
66
|
+
self.key_file: str | paramiko.RSAKey | None = (
|
|
67
|
+
sftp_config.pem_path
|
|
68
|
+
if sftp_config.pem_path is not None
|
|
69
|
+
else sftp_config.pem_key
|
|
70
|
+
)
|
|
71
|
+
self.password: str | None = sftp_config.password
|
|
72
|
+
|
|
73
|
+
def start(self) -> None:
|
|
74
|
+
cnopts = pysftp.CnOpts()
|
|
75
|
+
cnopts.hostkeys = None
|
|
76
|
+
if self.key_file is not None:
|
|
77
|
+
self.connection = pysftp.Connection(
|
|
78
|
+
self.host,
|
|
79
|
+
username=self.username,
|
|
80
|
+
private_key=self.key_file,
|
|
81
|
+
cnopts=cnopts,
|
|
82
|
+
)
|
|
83
|
+
elif self.password is not None:
|
|
84
|
+
self.connection = pysftp.Connection(
|
|
85
|
+
self.host,
|
|
86
|
+
username=self.username,
|
|
87
|
+
password=self.password,
|
|
88
|
+
cnopts=cnopts,
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
raise pysftp.CredentialException(
|
|
92
|
+
"Must specify either a private key path or a password."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def __enter__(self) -> "SFTPSession":
|
|
96
|
+
self.start()
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
|
|
100
|
+
self.connection.close()
|
|
101
|
+
|
|
102
|
+
def list_files(
|
|
103
|
+
self,
|
|
104
|
+
dir_path: FileSystemObject,
|
|
105
|
+
*,
|
|
106
|
+
recursive: bool = True,
|
|
107
|
+
valid_extensions: list[str] | None = None,
|
|
108
|
+
) -> list[FileSystemObject]:
|
|
109
|
+
if not isinstance(
|
|
110
|
+
dir_path, FileSystemFileReference
|
|
111
|
+
) or not self.connection.isdir(dir_path.filepath):
|
|
112
|
+
raise IncompatibleFileReference()
|
|
113
|
+
|
|
114
|
+
return [
|
|
115
|
+
FileSystemFileReference(file_path)
|
|
116
|
+
for file_path in list_sftp_files(
|
|
117
|
+
self.connection,
|
|
118
|
+
dir_path.filepath,
|
|
119
|
+
recursive=recursive,
|
|
120
|
+
valid_extensions=valid_extensions,
|
|
121
|
+
)
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
def download_files(self, filepaths: list[FileSystemObject]) -> list[FileObjectData]:
|
|
125
|
+
downloaded_files: list[FileObjectData] = []
|
|
126
|
+
for file_object in filepaths:
|
|
127
|
+
if (
|
|
128
|
+
not isinstance(file_object, FileSystemFileReference)
|
|
129
|
+
or file_object.filename is None
|
|
130
|
+
):
|
|
131
|
+
raise IncompatibleFileReference()
|
|
132
|
+
filepath = file_object.filepath
|
|
133
|
+
file_data = self.connection.open(filepath).read()
|
|
134
|
+
downloaded_file = FileObjectData(
|
|
135
|
+
file_data, BytesIO(file_data), file_object.filename, filepath=filepath
|
|
136
|
+
)
|
|
137
|
+
if downloaded_file is not None:
|
|
138
|
+
downloaded_files.append(downloaded_file)
|
|
139
|
+
return downloaded_files
|
|
140
|
+
|
|
141
|
+
def move_files(self, file_mappings: list[FileTransfer]) -> None:
|
|
142
|
+
for src_file, dest_file in file_mappings:
|
|
143
|
+
if not isinstance(src_file, FileSystemFileReference) or not isinstance(
|
|
144
|
+
dest_file, FileSystemFileReference
|
|
145
|
+
):
|
|
146
|
+
raise IncompatibleFileReference()
|
|
147
|
+
move_sftp_files(self.connection, src_file.filepath, dest_file.filepath)
|