UncountablePythonSDK 0.0.7__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.

Files changed (311) hide show
  1. UncountablePythonSDK-0.0.92.dist-info/METADATA +61 -0
  2. UncountablePythonSDK-0.0.92.dist-info/RECORD +301 -0
  3. {UncountablePythonSDK-0.0.7.dist-info → UncountablePythonSDK-0.0.92.dist-info}/WHEEL +1 -1
  4. {UncountablePythonSDK-0.0.7.dist-info → UncountablePythonSDK-0.0.92.dist-info}/top_level.txt +1 -1
  5. docs/.gitignore +1 -0
  6. docs/conf.py +57 -0
  7. docs/index.md +13 -0
  8. docs/justfile +12 -0
  9. docs/quickstart.md +19 -0
  10. docs/requirements.txt +7 -0
  11. docs/static/favicons/android-chrome-192x192.png +0 -0
  12. docs/static/favicons/android-chrome-512x512.png +0 -0
  13. docs/static/favicons/apple-touch-icon.png +0 -0
  14. docs/static/favicons/browserconfig.xml +9 -0
  15. docs/static/favicons/favicon-16x16.png +0 -0
  16. docs/static/favicons/favicon-32x32.png +0 -0
  17. docs/static/favicons/manifest.json +18 -0
  18. docs/static/favicons/mstile-150x150.png +0 -0
  19. docs/static/favicons/safari-pinned-tab.svg +32 -0
  20. docs/static/logo_blue.png +0 -0
  21. examples/async_batch.py +35 -0
  22. examples/create_entity.py +22 -17
  23. examples/download_files.py +26 -0
  24. examples/edit_recipe_inputs.py +50 -0
  25. examples/integration-server/jobs/materials_auto/example_cron.py +18 -0
  26. examples/integration-server/jobs/materials_auto/example_wh.py +15 -0
  27. examples/integration-server/jobs/materials_auto/profile.yaml +43 -0
  28. examples/integration-server/pyproject.toml +224 -0
  29. examples/invoke_uploader.py +26 -0
  30. examples/set_recipe_metadata_file.py +40 -0
  31. examples/set_recipe_output_file_sdk.py +26 -0
  32. examples/upload_files.py +18 -0
  33. pkgs/argument_parser/__init__.py +5 -0
  34. pkgs/argument_parser/_is_enum.py +1 -6
  35. pkgs/argument_parser/argument_parser.py +232 -76
  36. pkgs/argument_parser/case_convert.py +4 -3
  37. pkgs/filesystem_utils/__init__.py +20 -0
  38. pkgs/filesystem_utils/_blob_session.py +137 -0
  39. pkgs/filesystem_utils/_gdrive_session.py +309 -0
  40. pkgs/filesystem_utils/_local_session.py +69 -0
  41. pkgs/filesystem_utils/_s3_session.py +117 -0
  42. pkgs/filesystem_utils/_sftp_session.py +147 -0
  43. pkgs/filesystem_utils/file_type_utils.py +91 -0
  44. pkgs/filesystem_utils/filesystem_session.py +39 -0
  45. pkgs/py.typed +0 -0
  46. pkgs/serialization/__init__.py +8 -1
  47. pkgs/serialization/annotation.py +64 -0
  48. pkgs/serialization/opaque_key.py +1 -1
  49. pkgs/serialization/serial_alias.py +47 -0
  50. pkgs/serialization/serial_class.py +65 -50
  51. pkgs/serialization/serial_generic.py +16 -0
  52. pkgs/serialization/serial_union.py +84 -0
  53. pkgs/serialization/yaml.py +57 -0
  54. pkgs/serialization_util/__init__.py +7 -7
  55. pkgs/serialization_util/_get_type_for_serialization.py +1 -3
  56. pkgs/serialization_util/convert_to_snakecase.py +27 -0
  57. pkgs/serialization_util/dataclasses.py +14 -0
  58. pkgs/serialization_util/serialization_helpers.py +118 -73
  59. pkgs/strenum_compat/strenum_compat.py +1 -9
  60. pkgs/type_spec/actions_registry/__init__.py +0 -0
  61. pkgs/type_spec/actions_registry/__main__.py +126 -0
  62. pkgs/type_spec/actions_registry/emit_typescript.py +182 -0
  63. pkgs/type_spec/builder.py +475 -89
  64. pkgs/type_spec/config.py +24 -19
  65. pkgs/type_spec/emit_io_ts.py +5 -2
  66. pkgs/type_spec/emit_open_api.py +266 -32
  67. pkgs/type_spec/emit_open_api_util.py +32 -13
  68. pkgs/type_spec/emit_python.py +601 -150
  69. pkgs/type_spec/emit_typescript.py +74 -273
  70. pkgs/type_spec/emit_typescript_util.py +239 -5
  71. pkgs/type_spec/load_types.py +55 -10
  72. pkgs/type_spec/open_api_util.py +30 -41
  73. pkgs/type_spec/parts/base.py.prepart +4 -3
  74. pkgs/type_spec/type_info/emit_type_info.py +178 -16
  75. pkgs/type_spec/util.py +11 -11
  76. pkgs/type_spec/value_spec/__main__.py +3 -3
  77. pkgs/type_spec/value_spec/convert_type.py +8 -1
  78. pkgs/type_spec/value_spec/emit_python.py +13 -4
  79. uncountable/__init__.py +1 -2
  80. uncountable/core/__init__.py +12 -2
  81. uncountable/core/async_batch.py +37 -0
  82. uncountable/core/client.py +293 -43
  83. uncountable/core/environment.py +41 -0
  84. uncountable/core/file_upload.py +135 -0
  85. uncountable/core/types.py +17 -0
  86. uncountable/integration/__init__.py +0 -0
  87. uncountable/integration/cli.py +49 -0
  88. uncountable/integration/construct_client.py +51 -0
  89. uncountable/integration/cron.py +29 -0
  90. uncountable/integration/db/__init__.py +0 -0
  91. uncountable/integration/db/connect.py +18 -0
  92. uncountable/integration/db/session.py +25 -0
  93. uncountable/integration/entrypoint.py +13 -0
  94. uncountable/integration/executors/__init__.py +0 -0
  95. uncountable/integration/executors/executors.py +148 -0
  96. uncountable/integration/executors/generic_upload_executor.py +284 -0
  97. uncountable/integration/executors/script_executor.py +25 -0
  98. uncountable/integration/job.py +87 -0
  99. uncountable/integration/queue_runner/__init__.py +0 -0
  100. uncountable/integration/queue_runner/command_server/__init__.py +24 -0
  101. uncountable/integration/queue_runner/command_server/command_client.py +68 -0
  102. uncountable/integration/queue_runner/command_server/command_server.py +64 -0
  103. uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
  104. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +22 -0
  105. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +40 -0
  106. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +38 -0
  107. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +129 -0
  108. uncountable/integration/queue_runner/command_server/types.py +52 -0
  109. uncountable/integration/queue_runner/datastore/__init__.py +3 -0
  110. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +93 -0
  111. uncountable/integration/queue_runner/datastore/interface.py +19 -0
  112. uncountable/integration/queue_runner/datastore/model.py +17 -0
  113. uncountable/integration/queue_runner/job_scheduler.py +163 -0
  114. uncountable/integration/queue_runner/queue_runner.py +26 -0
  115. uncountable/integration/queue_runner/types.py +7 -0
  116. uncountable/integration/queue_runner/worker.py +119 -0
  117. uncountable/integration/scan_profiles.py +67 -0
  118. uncountable/integration/scheduler.py +150 -0
  119. uncountable/integration/secret_retrieval/__init__.py +3 -0
  120. uncountable/integration/secret_retrieval/retrieve_secret.py +93 -0
  121. uncountable/integration/server.py +117 -0
  122. uncountable/integration/telemetry.py +209 -0
  123. uncountable/integration/webhook_server/entrypoint.py +170 -0
  124. uncountable/types/__init__.py +151 -5
  125. uncountable/types/api/batch/execute_batch.py +15 -7
  126. uncountable/types/api/batch/execute_batch_load_async.py +42 -0
  127. uncountable/types/api/chemical/__init__.py +1 -0
  128. uncountable/types/api/chemical/convert_chemical_formats.py +63 -0
  129. uncountable/types/api/entity/create_entities.py +23 -10
  130. uncountable/types/api/entity/create_entity.py +21 -12
  131. uncountable/types/api/entity/get_entities_data.py +19 -29
  132. uncountable/types/api/entity/grant_entity_permissions.py +48 -0
  133. uncountable/types/api/entity/list_entities.py +28 -20
  134. uncountable/types/api/entity/lock_entity.py +45 -0
  135. uncountable/types/api/entity/resolve_entity_ids.py +19 -7
  136. uncountable/types/api/entity/set_entity_field_values.py +44 -0
  137. uncountable/types/api/entity/set_values.py +13 -28
  138. uncountable/types/api/entity/transition_entity_phase.py +80 -0
  139. uncountable/types/api/entity/unlock_entity.py +44 -0
  140. uncountable/types/api/equipment/__init__.py +1 -0
  141. uncountable/types/api/equipment/associate_equipment_input.py +44 -0
  142. uncountable/types/api/field_options/__init__.py +1 -0
  143. uncountable/types/api/field_options/upsert_field_options.py +55 -0
  144. uncountable/types/api/files/__init__.py +1 -0
  145. uncountable/types/api/files/download_file.py +77 -0
  146. uncountable/types/api/id_source/__init__.py +1 -0
  147. uncountable/types/api/id_source/list_id_source.py +56 -0
  148. uncountable/types/api/id_source/match_id_source.py +54 -0
  149. uncountable/types/api/input_groups/get_input_group_names.py +18 -7
  150. uncountable/types/api/inputs/create_inputs.py +25 -24
  151. uncountable/types/api/inputs/get_input_data.py +37 -31
  152. uncountable/types/api/inputs/get_input_names.py +20 -9
  153. uncountable/types/api/inputs/get_inputs_data.py +33 -27
  154. uncountable/types/api/inputs/set_input_attribute_values.py +18 -13
  155. uncountable/types/api/inputs/set_input_category.py +44 -0
  156. uncountable/types/api/inputs/set_input_subcategories.py +45 -0
  157. uncountable/types/api/inputs/set_intermediate_type.py +50 -0
  158. uncountable/types/api/material_families/__init__.py +1 -0
  159. uncountable/types/api/material_families/update_entity_material_families.py +48 -0
  160. uncountable/types/api/outputs/get_output_data.py +38 -29
  161. uncountable/types/api/outputs/get_output_names.py +20 -9
  162. uncountable/types/api/outputs/resolve_output_conditions.py +23 -10
  163. uncountable/types/api/permissions/__init__.py +1 -0
  164. uncountable/types/api/permissions/set_core_permissions.py +105 -0
  165. uncountable/types/api/project/get_projects.py +23 -19
  166. uncountable/types/api/project/get_projects_data.py +26 -43
  167. uncountable/types/api/recipe_links/__init__.py +1 -0
  168. uncountable/types/api/recipe_links/create_recipe_link.py +46 -0
  169. uncountable/types/api/recipe_links/remove_recipe_link.py +45 -0
  170. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +21 -10
  171. uncountable/types/api/recipes/add_recipe_to_project.py +42 -0
  172. uncountable/types/api/recipes/archive_recipes.py +42 -0
  173. uncountable/types/api/recipes/associate_recipe_as_input.py +44 -0
  174. uncountable/types/api/recipes/associate_recipe_as_lot.py +43 -0
  175. uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
  176. uncountable/types/api/recipes/create_recipe.py +51 -0
  177. uncountable/types/api/recipes/create_recipes.py +25 -24
  178. uncountable/types/api/recipes/disassociate_recipe_as_input.py +42 -0
  179. uncountable/types/api/recipes/edit_recipe_inputs.py +283 -0
  180. uncountable/types/api/recipes/get_column_calculation_values.py +58 -0
  181. uncountable/types/api/recipes/get_curve.py +13 -27
  182. uncountable/types/api/recipes/get_recipe_calculations.py +21 -21
  183. uncountable/types/api/recipes/get_recipe_links.py +14 -6
  184. uncountable/types/api/recipes/get_recipe_names.py +18 -7
  185. uncountable/types/api/recipes/get_recipe_output_metadata.py +18 -19
  186. uncountable/types/api/recipes/get_recipes_data.py +83 -144
  187. uncountable/types/api/recipes/lock_recipes.py +63 -0
  188. uncountable/types/api/recipes/remove_recipe_from_project.py +42 -0
  189. uncountable/types/api/recipes/set_recipe_inputs.py +21 -11
  190. uncountable/types/api/recipes/set_recipe_metadata.py +43 -0
  191. uncountable/types/api/recipes/set_recipe_output_annotations.py +115 -0
  192. uncountable/types/api/recipes/set_recipe_output_file.py +56 -0
  193. uncountable/types/api/recipes/set_recipe_outputs.py +28 -15
  194. uncountable/types/api/recipes/set_recipe_tags.py +109 -0
  195. uncountable/types/api/recipes/unarchive_recipes.py +41 -0
  196. uncountable/types/api/recipes/unlock_recipes.py +50 -0
  197. uncountable/types/api/triggers/__init__.py +1 -0
  198. uncountable/types/api/triggers/run_trigger.py +43 -0
  199. uncountable/types/api/uploader/__init__.py +1 -0
  200. uncountable/types/api/uploader/invoke_uploader.py +47 -0
  201. uncountable/types/async_batch.py +13 -0
  202. uncountable/types/async_batch_processor.py +384 -0
  203. uncountable/types/async_batch_t.py +97 -0
  204. uncountable/types/async_jobs.py +9 -0
  205. uncountable/types/async_jobs_t.py +53 -0
  206. uncountable/types/auth_retrieval.py +12 -0
  207. uncountable/types/auth_retrieval_t.py +75 -0
  208. uncountable/types/base.py +5 -78
  209. uncountable/types/base_t.py +85 -0
  210. uncountable/types/calculations.py +8 -0
  211. uncountable/types/calculations_t.py +27 -0
  212. uncountable/types/chemical_structure.py +8 -0
  213. uncountable/types/chemical_structure_t.py +28 -0
  214. uncountable/types/client_base.py +1115 -76
  215. uncountable/types/client_config.py +8 -0
  216. uncountable/types/client_config_t.py +26 -0
  217. uncountable/types/curves.py +10 -0
  218. uncountable/types/curves_t.py +51 -0
  219. uncountable/types/entity.py +8 -266
  220. uncountable/types/entity_t.py +393 -0
  221. uncountable/types/experiment_groups.py +8 -0
  222. uncountable/types/experiment_groups_t.py +27 -0
  223. uncountable/types/field_values.py +17 -23
  224. uncountable/types/field_values_t.py +204 -0
  225. uncountable/types/fields.py +8 -0
  226. uncountable/types/fields_t.py +28 -0
  227. uncountable/types/generic_upload.py +15 -0
  228. uncountable/types/generic_upload_t.py +119 -0
  229. uncountable/types/id_source.py +12 -0
  230. uncountable/types/id_source_t.py +68 -0
  231. uncountable/types/identifier.py +11 -0
  232. uncountable/types/identifier_t.py +63 -0
  233. uncountable/types/input_attributes.py +8 -0
  234. uncountable/types/input_attributes_t.py +30 -0
  235. uncountable/types/inputs.py +11 -0
  236. uncountable/types/inputs_t.py +83 -0
  237. uncountable/types/integration_server.py +9 -0
  238. uncountable/types/integration_server_t.py +42 -0
  239. uncountable/types/job_definition.py +27 -0
  240. uncountable/types/job_definition_t.py +260 -0
  241. uncountable/types/outputs.py +8 -0
  242. uncountable/types/outputs_t.py +30 -0
  243. uncountable/types/overrides.py +10 -0
  244. uncountable/types/overrides_t.py +49 -0
  245. uncountable/types/permissions.py +8 -0
  246. uncountable/types/permissions_t.py +46 -0
  247. uncountable/types/phases.py +8 -0
  248. uncountable/types/phases_t.py +27 -0
  249. uncountable/types/post_base.py +8 -0
  250. uncountable/types/post_base_t.py +30 -0
  251. uncountable/types/queued_job.py +16 -0
  252. uncountable/types/queued_job_t.py +123 -0
  253. uncountable/types/recipe_identifiers.py +12 -0
  254. uncountable/types/recipe_identifiers_t.py +76 -0
  255. uncountable/types/recipe_inputs.py +9 -0
  256. uncountable/types/recipe_inputs_t.py +30 -0
  257. uncountable/types/recipe_links.py +4 -44
  258. uncountable/types/recipe_links_t.py +54 -0
  259. uncountable/types/recipe_metadata.py +10 -0
  260. uncountable/types/recipe_metadata_t.py +58 -0
  261. uncountable/types/recipe_output_metadata.py +8 -0
  262. uncountable/types/recipe_output_metadata_t.py +28 -0
  263. uncountable/types/recipe_tags.py +8 -0
  264. uncountable/types/recipe_tags_t.py +27 -0
  265. uncountable/types/recipe_workflow_steps.py +14 -0
  266. uncountable/types/recipe_workflow_steps_t.py +95 -0
  267. uncountable/types/recipes.py +8 -0
  268. uncountable/types/recipes_t.py +25 -0
  269. uncountable/types/response.py +8 -0
  270. uncountable/types/response_t.py +26 -0
  271. uncountable/types/secret_retrieval.py +12 -0
  272. uncountable/types/secret_retrieval_t.py +75 -0
  273. uncountable/types/units.py +8 -0
  274. uncountable/types/units_t.py +27 -0
  275. uncountable/types/users.py +8 -0
  276. uncountable/types/users_t.py +28 -0
  277. uncountable/types/webhook_job.py +9 -0
  278. uncountable/types/webhook_job_t.py +37 -0
  279. uncountable/types/workflows.py +9 -0
  280. uncountable/types/workflows_t.py +39 -0
  281. UncountablePythonSDK-0.0.7.dist-info/METADATA +0 -27
  282. UncountablePythonSDK-0.0.7.dist-info/RECORD +0 -119
  283. examples/recipe-import/importer.py +0 -39
  284. type_spec/external/api/batch/execute_batch.yaml +0 -56
  285. type_spec/external/api/entity/create_entities.yaml +0 -33
  286. type_spec/external/api/entity/create_entity.yaml +0 -39
  287. type_spec/external/api/entity/get_entities_data.yaml +0 -55
  288. type_spec/external/api/entity/list_entities.yaml +0 -62
  289. type_spec/external/api/entity/resolve_entity_ids.yaml +0 -29
  290. type_spec/external/api/entity/set_values.yaml +0 -45
  291. type_spec/external/api/input_groups/get_input_group_names.yaml +0 -29
  292. type_spec/external/api/inputs/create_inputs.yaml +0 -61
  293. type_spec/external/api/inputs/get_input_data.yaml +0 -108
  294. type_spec/external/api/inputs/get_input_names.yaml +0 -38
  295. type_spec/external/api/inputs/get_inputs_data.yaml +0 -95
  296. type_spec/external/api/inputs/set_input_attribute_values.yaml +0 -37
  297. type_spec/external/api/outputs/get_output_data.yaml +0 -103
  298. type_spec/external/api/outputs/get_output_names.yaml +0 -35
  299. type_spec/external/api/outputs/resolve_output_conditions.yaml +0 -50
  300. type_spec/external/api/project/get_projects.yaml +0 -52
  301. type_spec/external/api/project/get_projects_data.yaml +0 -86
  302. type_spec/external/api/recipe_metadata/get_recipe_metadata_data.yaml +0 -41
  303. type_spec/external/api/recipes/create_recipes.yaml +0 -60
  304. type_spec/external/api/recipes/get_curve.yaml +0 -50
  305. type_spec/external/api/recipes/get_recipe_calculations.yaml +0 -49
  306. type_spec/external/api/recipes/get_recipe_links.yaml +0 -26
  307. type_spec/external/api/recipes/get_recipe_names.yaml +0 -29
  308. type_spec/external/api/recipes/get_recipe_output_metadata.yaml +0 -49
  309. type_spec/external/api/recipes/get_recipes_data.yaml +0 -372
  310. type_spec/external/api/recipes/set_recipe_inputs.yaml +0 -36
  311. type_spec/external/api/recipes/set_recipe_outputs.yaml +0 -56
@@ -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)