UncountablePythonSDK 0.0.41__py3-none-any.whl → 0.0.43__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 (123) hide show
  1. {UncountablePythonSDK-0.0.41.dist-info → UncountablePythonSDK-0.0.43.dist-info}/METADATA +5 -1
  2. {UncountablePythonSDK-0.0.41.dist-info → UncountablePythonSDK-0.0.43.dist-info}/RECORD +122 -104
  3. docs/requirements.txt +3 -3
  4. examples/invoke_uploader.py +23 -0
  5. pkgs/argument_parser/argument_parser.py +1 -1
  6. pkgs/filesystem_utils/__init__.py +17 -0
  7. pkgs/filesystem_utils/_gdrive_session.py +306 -0
  8. pkgs/filesystem_utils/_local_session.py +69 -0
  9. pkgs/filesystem_utils/_sftp_session.py +147 -0
  10. pkgs/filesystem_utils/file_type_utils.py +61 -0
  11. pkgs/filesystem_utils/filesystem_session.py +39 -0
  12. pkgs/type_spec/emit_open_api.py +4 -2
  13. pkgs/type_spec/emit_open_api_util.py +4 -2
  14. pkgs/type_spec/emit_python.py +13 -14
  15. uncountable/core/file_upload.py +13 -3
  16. uncountable/integration/construct_client.py +1 -1
  17. uncountable/integration/cron.py +9 -6
  18. uncountable/integration/entrypoint.py +1 -1
  19. uncountable/integration/executors/executors.py +24 -0
  20. uncountable/integration/executors/generic_upload_executor.py +244 -0
  21. uncountable/integration/executors/script_executor.py +1 -1
  22. uncountable/integration/job.py +18 -1
  23. uncountable/integration/secret_retrieval/__init__.py +3 -0
  24. uncountable/integration/secret_retrieval/retrieve_secret.py +40 -0
  25. uncountable/integration/server.py +1 -1
  26. uncountable/types/__init__.py +8 -0
  27. uncountable/types/api/batch/execute_batch.py +5 -5
  28. uncountable/types/api/batch/execute_batch_load_async.py +3 -3
  29. uncountable/types/api/chemical/convert_chemical_formats.py +4 -4
  30. uncountable/types/api/entity/create_entities.py +4 -4
  31. uncountable/types/api/entity/create_entity.py +4 -4
  32. uncountable/types/api/entity/get_entities_data.py +4 -4
  33. uncountable/types/api/entity/list_entities.py +5 -5
  34. uncountable/types/api/entity/lock_entity.py +3 -3
  35. uncountable/types/api/entity/resolve_entity_ids.py +4 -4
  36. uncountable/types/api/entity/set_values.py +3 -3
  37. uncountable/types/api/entity/transition_entity_phase.py +5 -5
  38. uncountable/types/api/entity/unlock_entity.py +3 -3
  39. uncountable/types/api/equipment/associate_equipment_input.py +3 -3
  40. uncountable/types/api/field_options/upsert_field_options.py +4 -4
  41. uncountable/types/api/id_source/list_id_source.py +4 -4
  42. uncountable/types/api/id_source/match_id_source.py +4 -4
  43. uncountable/types/api/input_groups/get_input_group_names.py +4 -4
  44. uncountable/types/api/inputs/create_inputs.py +5 -5
  45. uncountable/types/api/inputs/get_input_data.py +7 -7
  46. uncountable/types/api/inputs/get_input_names.py +4 -4
  47. uncountable/types/api/inputs/get_inputs_data.py +7 -7
  48. uncountable/types/api/inputs/set_input_attribute_values.py +4 -4
  49. uncountable/types/api/inputs/set_input_category.py +3 -3
  50. uncountable/types/api/inputs/set_input_subcategories.py +3 -3
  51. uncountable/types/api/inputs/set_intermediate_type.py +3 -3
  52. uncountable/types/api/material_families/update_entity_material_families.py +3 -3
  53. uncountable/types/api/outputs/get_output_data.py +7 -7
  54. uncountable/types/api/outputs/get_output_names.py +4 -4
  55. uncountable/types/api/outputs/resolve_output_conditions.py +6 -6
  56. uncountable/types/api/permissions/set_core_permissions.py +7 -7
  57. uncountable/types/api/project/get_projects.py +4 -4
  58. uncountable/types/api/project/get_projects_data.py +4 -4
  59. uncountable/types/api/recipe_links/create_recipe_link.py +3 -3
  60. uncountable/types/api/recipe_links/remove_recipe_link.py +3 -3
  61. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +4 -4
  62. uncountable/types/api/recipes/add_recipe_to_project.py +3 -3
  63. uncountable/types/api/recipes/archive_recipes.py +3 -3
  64. uncountable/types/api/recipes/associate_recipe_as_input.py +3 -3
  65. uncountable/types/api/recipes/associate_recipe_as_lot.py +3 -3
  66. uncountable/types/api/recipes/create_recipe.py +3 -3
  67. uncountable/types/api/recipes/create_recipes.py +5 -5
  68. uncountable/types/api/recipes/disassociate_recipe_as_input.py +3 -3
  69. uncountable/types/api/recipes/edit_recipe_inputs.py +12 -12
  70. uncountable/types/api/recipes/get_curve.py +3 -3
  71. uncountable/types/api/recipes/get_recipe_calculations.py +4 -4
  72. uncountable/types/api/recipes/get_recipe_links.py +3 -3
  73. uncountable/types/api/recipes/get_recipe_names.py +4 -4
  74. uncountable/types/api/recipes/get_recipe_output_metadata.py +4 -4
  75. uncountable/types/api/recipes/get_recipes_data.py +12 -12
  76. uncountable/types/api/recipes/lock_recipes.py +4 -4
  77. uncountable/types/api/recipes/remove_recipe_from_project.py +3 -3
  78. uncountable/types/api/recipes/set_recipe_inputs.py +4 -4
  79. uncountable/types/api/recipes/set_recipe_metadata.py +3 -3
  80. uncountable/types/api/recipes/set_recipe_output_annotations.py +7 -7
  81. uncountable/types/api/recipes/set_recipe_outputs.py +5 -5
  82. uncountable/types/api/recipes/set_recipe_tags.py +7 -7
  83. uncountable/types/api/recipes/unarchive_recipes.py +3 -3
  84. uncountable/types/api/recipes/unlock_recipes.py +3 -3
  85. uncountable/types/api/triggers/run_trigger.py +3 -3
  86. uncountable/types/api/uploader/__init__.py +1 -0
  87. uncountable/types/api/uploader/invoke_uploader.py +38 -0
  88. uncountable/types/async_batch_processor.py +36 -0
  89. uncountable/types/async_batch_t.py +6 -4
  90. uncountable/types/calculations_t.py +2 -2
  91. uncountable/types/chemical_structure_t.py +2 -2
  92. uncountable/types/client_base.py +25 -2
  93. uncountable/types/curves_t.py +3 -3
  94. uncountable/types/entity_t.py +2 -2
  95. uncountable/types/experiment_groups_t.py +2 -2
  96. uncountable/types/field_values_t.py +5 -5
  97. uncountable/types/fields_t.py +2 -2
  98. uncountable/types/generic_upload.py +9 -0
  99. uncountable/types/generic_upload_t.py +41 -0
  100. uncountable/types/id_source_t.py +5 -5
  101. uncountable/types/identifier_t.py +4 -4
  102. uncountable/types/input_attributes_t.py +2 -2
  103. uncountable/types/inputs_t.py +2 -2
  104. uncountable/types/job_definition.py +26 -0
  105. uncountable/types/job_definition_t.py +203 -0
  106. uncountable/types/outputs_t.py +2 -2
  107. uncountable/types/phases_t.py +2 -2
  108. uncountable/types/recipe_identifiers_t.py +4 -4
  109. uncountable/types/recipe_links_t.py +2 -2
  110. uncountable/types/recipe_metadata_t.py +4 -4
  111. uncountable/types/recipe_output_metadata_t.py +2 -2
  112. uncountable/types/recipe_tags_t.py +2 -2
  113. uncountable/types/recipe_workflow_steps_t.py +5 -5
  114. uncountable/types/recipes_t.py +2 -2
  115. uncountable/types/response_t.py +2 -2
  116. uncountable/types/secret_retrieval.py +12 -0
  117. uncountable/types/secret_retrieval_t.py +69 -0
  118. uncountable/types/units_t.py +2 -2
  119. uncountable/types/users_t.py +2 -2
  120. uncountable/types/workflows_t.py +3 -3
  121. uncountable/integration/types.py +0 -89
  122. {UncountablePythonSDK-0.0.41.dist-info → UncountablePythonSDK-0.0.43.dist-info}/WHEEL +0 -0
  123. {UncountablePythonSDK-0.0.41.dist-info → UncountablePythonSDK-0.0.43.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,306 @@
1
+ import os
2
+ from io import BytesIO
3
+ from typing import Any, Optional
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
+ ) -> Optional[FileObjectData]:
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: Optional[str] = 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: Optional[tuple[str, ...]] = 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] not in valid_file_extensions
216
+ ):
217
+ continue
218
+ gdrive_files.append(
219
+ RemoteObjectReference(
220
+ file_id=file_context["id"],
221
+ mime_type=file_context["mimeType"],
222
+ filename=file_context["name"],
223
+ )
224
+ )
225
+ return gdrive_files
226
+
227
+ def delete_files(self, filepaths: list[FileSystemObject]) -> None:
228
+ """Warning:
229
+ Security account must have sufficient permissions to perform delete!
230
+ https://developers.google.com/drive/api/v3/reference/files/delete?hl=en
231
+ https://developers.google.com/drive/api/v3/ref-roles
232
+ """
233
+ for file_object in filepaths:
234
+ if not isinstance(file_object, RemoteObjectReference):
235
+ raise IncompatibleFileReference(
236
+ "Incompatible FileSystemObject provided to GDriveSession.delete_files"
237
+ )
238
+ delete_gdrive_file(self.connection, file_object.file_id)
239
+
240
+ def move_files(self, file_mappings: list[FileTransfer]) -> None:
241
+ for src_file, dest_file in file_mappings:
242
+ if (
243
+ isinstance(src_file, FileSystemFileReference)
244
+ or not isinstance(dest_file, RemoteObjectReference)
245
+ or not dest_file.is_dir
246
+ or (isinstance(src_file, RemoteObjectReference) and src_file.is_dir)
247
+ ):
248
+ continue
249
+ new_filename = dest_file.filename
250
+ if isinstance(src_file, RemoteObjectReference):
251
+ if new_filename is not None:
252
+ move_gdrive_file(
253
+ self.connection,
254
+ src_file.file_id,
255
+ dest_file.file_id,
256
+ dest_filename=new_filename,
257
+ )
258
+ else:
259
+ move_gdrive_file(self.connection, src_file.file_id, dest_file.file_id)
260
+ elif isinstance(src_file, FileObjectData):
261
+ if src_file.mime_type is None:
262
+ raise IncompatibleFileReference(
263
+ "No mime_type present on source file data."
264
+ )
265
+ new_filename = src_file.filename
266
+ if dest_file.filename is not None:
267
+ new_filename = dest_file.filename
268
+ upload_file_gdrive(
269
+ self.connection,
270
+ src_file.file_IO,
271
+ src_file.mime_type,
272
+ dest_file.file_id,
273
+ new_filename,
274
+ )
275
+ else:
276
+ raise IncompatibleFileReference(
277
+ "Unrecognized file reference in FileTransfer object"
278
+ )
279
+
280
+ def download_files(self, filepaths: list[FileSystemObject]) -> list[FileObjectData]:
281
+ downloaded_files: list[FileObjectData] = []
282
+ print(f"Downloading {len(filepaths)} files")
283
+ for file_object in tqdm(filepaths):
284
+ if (
285
+ not isinstance(file_object, RemoteObjectReference)
286
+ or file_object.filename is None
287
+ ):
288
+ raise IncompatibleFileReference(
289
+ "Incompatible FileSystemObject included in filepaths"
290
+ )
291
+ downloaded_file = download_gdrive_file(
292
+ self.connection,
293
+ file_object.file_id,
294
+ file_object.filename,
295
+ file_object.mime_type,
296
+ )
297
+ if downloaded_file is not None:
298
+ downloaded_files.append(downloaded_file)
299
+ return downloaded_files
300
+
301
+ def __enter__(self) -> "GDriveSession":
302
+ self.start()
303
+ return self
304
+
305
+ def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
306
+ 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,147 @@
1
+ import os
2
+ from collections.abc import Iterable
3
+ from io import BytesIO
4
+ from typing import Optional
5
+
6
+ import paramiko
7
+ import pysftp
8
+
9
+ from pkgs.filesystem_utils.file_type_utils import (
10
+ FileObjectData,
11
+ FileSystemFileReference,
12
+ FileSystemObject,
13
+ FileSystemSFTPConfig,
14
+ FileTransfer,
15
+ IncompatibleFileReference,
16
+ )
17
+
18
+ from .filesystem_session import FileSystemSession
19
+
20
+
21
+ def move_sftp_files(
22
+ connection: pysftp.Connection,
23
+ src_filepath: str,
24
+ dest_filepath: str,
25
+ ) -> None:
26
+ connection.rename(src_filepath, dest_filepath)
27
+
28
+
29
+ def list_sftp_files(
30
+ connection: pysftp.Connection,
31
+ dir_path: str,
32
+ *,
33
+ valid_extensions: Optional[Iterable[str]] = None,
34
+ parent_dir_path: Optional[str] = None,
35
+ recursive: bool = True,
36
+ ) -> list[str]:
37
+ file_paths: list[str] = []
38
+ if recursive:
39
+
40
+ def _skip(name: str) -> None:
41
+ return
42
+
43
+ def _add_file(path: str) -> None:
44
+ if (
45
+ valid_extensions is None 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.AgentKey | 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(dir_path, FileSystemFileReference) or not self.connection.isdir(
110
+ dir_path.filepath
111
+ ):
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)
@@ -0,0 +1,61 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+ from io import BytesIO
4
+ from typing import Optional, Union
5
+
6
+ import paramiko
7
+
8
+
9
+ @dataclass
10
+ class FileObjectData:
11
+ file_data: bytes
12
+ file_IO: BytesIO
13
+ filename: str
14
+ filepath: Optional[str] = None
15
+ mime_type: Optional[str] = None
16
+ metadata: Optional[dict[str, str]] = None
17
+
18
+
19
+ @dataclass
20
+ class FileSystemFileReference:
21
+ filepath: str
22
+
23
+ @property
24
+ def filename(self) -> str:
25
+ return os.path.basename(self.filepath)
26
+
27
+ @property
28
+ def dirname(self) -> str:
29
+ return os.path.dirname(self.filepath)
30
+
31
+
32
+ @dataclass
33
+ class RemoteObjectReference:
34
+ file_id: str
35
+ mime_type: str
36
+ filename: Optional[str] = None
37
+
38
+ @property
39
+ def is_dir(self) -> bool:
40
+ return "folder" in self.mime_type
41
+
42
+
43
+ FileSystemObject = Union[FileSystemFileReference, RemoteObjectReference, FileObjectData]
44
+
45
+
46
+ FileTransfer = tuple[FileSystemObject, FileSystemObject]
47
+
48
+
49
+ class IncompatibleFileReference(Exception):
50
+ pass
51
+
52
+
53
+ @dataclass(frozen=True, kw_only=True)
54
+ class FileSystemSFTPConfig:
55
+ ip: str
56
+ username: str
57
+ pem_path: str | None
58
+ pem_key: paramiko.AgentKey | None = None
59
+ password: str | None = None
60
+ valid_extensions: Optional[tuple[str]] = None
61
+ recursive: bool = True
@@ -0,0 +1,39 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from pkgs.filesystem_utils.file_type_utils import (
4
+ FileObjectData,
5
+ FileSystemObject,
6
+ FileTransfer,
7
+ )
8
+
9
+
10
+ class FileSystemSession(ABC):
11
+ def __init__(self) -> None:
12
+ return
13
+
14
+ @abstractmethod
15
+ def start(self) -> None:
16
+ raise NotImplementedError
17
+
18
+ @abstractmethod
19
+ def list_files(
20
+ self, dir_path: FileSystemObject, *, recursive: bool = True
21
+ ) -> list[FileSystemObject]:
22
+ raise NotImplementedError
23
+
24
+ @abstractmethod
25
+ def move_files(self, file_mappings: list[FileTransfer]) -> None:
26
+ raise NotImplementedError
27
+
28
+ @abstractmethod
29
+ def download_files(self, filepaths: list[FileSystemObject]) -> list[FileObjectData]:
30
+ raise NotImplementedError
31
+
32
+ def delete_files(self, filepaths: list[FileSystemObject]) -> None:
33
+ raise NotImplementedError
34
+
35
+ @abstractmethod
36
+ def __enter__(self) -> "FileSystemSession": ...
37
+
38
+ @abstractmethod
39
+ def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None: ...
@@ -10,6 +10,7 @@ import re
10
10
  from typing import Collection, cast
11
11
 
12
12
  from pkgs.serialization import yaml
13
+ from pkgs.serialization_util.serialization_helpers import serialize_for_api
13
14
 
14
15
  from . import builder, util
15
16
  from .builder import EndpointGuideKey, RootGuideKey
@@ -568,6 +569,7 @@ def _emit_endpoint(
568
569
  description = f"**[External API-Endpoint]** <br/> {description}"
569
570
 
570
571
  path_cutoff = min(3, len(namespace.path) - 1)
572
+
571
573
  ctx.endpoint = EmitOpenAPIEndpoint(
572
574
  method=namespace.endpoint.method.lower(),
573
575
  tags=[tag_name],
@@ -580,8 +582,8 @@ def _emit_endpoint(
580
582
  ref_name=f"ex_{i}",
581
583
  summary=example.summary,
582
584
  description=example.description,
583
- arguments=example.arguments,
584
- data=example.data,
585
+ arguments=serialize_for_api(example.arguments),
586
+ data=serialize_for_api(example.data),
585
587
  )
586
588
  for i, example in enumerate(endpoint_examples)
587
589
  ],
@@ -8,6 +8,8 @@ from collections import defaultdict
8
8
  from dataclasses import dataclass, field
9
9
  from typing import TypeAlias
10
10
 
11
+ from pkgs.serialization_util.serialization_helpers import JsonValue
12
+
11
13
  from . import builder
12
14
  from .open_api_util import OpenAPIType
13
15
 
@@ -68,8 +70,8 @@ class EmitOpenAPIEndpointExample:
68
70
  ref_name: str
69
71
  summary: str
70
72
  description: str
71
- arguments: dict[str, object]
72
- data: dict[str, object]
73
+ arguments: dict[str, JsonValue]
74
+ data: dict[str, JsonValue]
73
75
 
74
76
 
75
77
  EmitOpenAPIStabilityLevel = builder.StabilityLevel