data-sourcerer 0.1.0__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.
- data_sourcerer-0.1.0.dist-info/METADATA +52 -0
- data_sourcerer-0.1.0.dist-info/RECORD +95 -0
- data_sourcerer-0.1.0.dist-info/WHEEL +5 -0
- data_sourcerer-0.1.0.dist-info/entry_points.txt +2 -0
- data_sourcerer-0.1.0.dist-info/licenses/LICENSE +21 -0
- data_sourcerer-0.1.0.dist-info/top_level.txt +1 -0
- sourcerer/__init__.py +15 -0
- sourcerer/domain/__init__.py +13 -0
- sourcerer/domain/access_credentials/__init__.py +7 -0
- sourcerer/domain/access_credentials/entities.py +53 -0
- sourcerer/domain/access_credentials/exceptions.py +17 -0
- sourcerer/domain/access_credentials/repositories.py +84 -0
- sourcerer/domain/access_credentials/services.py +91 -0
- sourcerer/domain/file_system/__init__.py +7 -0
- sourcerer/domain/file_system/entities.py +70 -0
- sourcerer/domain/file_system/exceptions.py +17 -0
- sourcerer/domain/file_system/services.py +64 -0
- sourcerer/domain/shared/__init__.py +6 -0
- sourcerer/domain/shared/entities.py +18 -0
- sourcerer/domain/storage_provider/__init__.py +7 -0
- sourcerer/domain/storage_provider/entities.py +86 -0
- sourcerer/domain/storage_provider/exceptions.py +17 -0
- sourcerer/domain/storage_provider/services.py +130 -0
- sourcerer/infrastructure/__init__.py +13 -0
- sourcerer/infrastructure/access_credentials/__init__.py +7 -0
- sourcerer/infrastructure/access_credentials/exceptions.py +16 -0
- sourcerer/infrastructure/access_credentials/registry.py +120 -0
- sourcerer/infrastructure/access_credentials/repositories.py +119 -0
- sourcerer/infrastructure/access_credentials/services.py +396 -0
- sourcerer/infrastructure/db/__init__.py +6 -0
- sourcerer/infrastructure/db/config.py +73 -0
- sourcerer/infrastructure/db/models.py +47 -0
- sourcerer/infrastructure/file_system/__init__.py +7 -0
- sourcerer/infrastructure/file_system/exceptions.py +89 -0
- sourcerer/infrastructure/file_system/services.py +147 -0
- sourcerer/infrastructure/storage_provider/__init__.py +7 -0
- sourcerer/infrastructure/storage_provider/exceptions.py +78 -0
- sourcerer/infrastructure/storage_provider/registry.py +84 -0
- sourcerer/infrastructure/storage_provider/services.py +509 -0
- sourcerer/infrastructure/utils.py +106 -0
- sourcerer/presentation/__init__.py +12 -0
- sourcerer/presentation/app.py +36 -0
- sourcerer/presentation/di_container.py +46 -0
- sourcerer/presentation/screens/__init__.py +0 -0
- sourcerer/presentation/screens/critical_error/__init__.py +0 -0
- sourcerer/presentation/screens/critical_error/main.py +78 -0
- sourcerer/presentation/screens/critical_error/styles.tcss +41 -0
- sourcerer/presentation/screens/file_system_finder/main.py +248 -0
- sourcerer/presentation/screens/file_system_finder/styles.tcss +44 -0
- sourcerer/presentation/screens/file_system_finder/widgets/__init__.py +0 -0
- sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +810 -0
- sourcerer/presentation/screens/main/__init__.py +0 -0
- sourcerer/presentation/screens/main/main.py +469 -0
- sourcerer/presentation/screens/main/messages/__init__.py +0 -0
- sourcerer/presentation/screens/main/messages/delete_request.py +12 -0
- sourcerer/presentation/screens/main/messages/download_request.py +12 -0
- sourcerer/presentation/screens/main/messages/preview_request.py +10 -0
- sourcerer/presentation/screens/main/messages/resizing_rule.py +21 -0
- sourcerer/presentation/screens/main/messages/select_storage_item.py +11 -0
- sourcerer/presentation/screens/main/messages/uncheck_files_request.py +8 -0
- sourcerer/presentation/screens/main/messages/upload_request.py +10 -0
- sourcerer/presentation/screens/main/mixins/__init__.py +0 -0
- sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +144 -0
- sourcerer/presentation/screens/main/styles.tcss +32 -0
- sourcerer/presentation/screens/main/widgets/__init__.py +0 -0
- sourcerer/presentation/screens/main/widgets/gradient.py +45 -0
- sourcerer/presentation/screens/main/widgets/resizing_rule.py +67 -0
- sourcerer/presentation/screens/main/widgets/storage_content.py +691 -0
- sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +143 -0
- sourcerer/presentation/screens/preview_content/__init__.py +0 -0
- sourcerer/presentation/screens/preview_content/main.py +59 -0
- sourcerer/presentation/screens/preview_content/styles.tcss +26 -0
- sourcerer/presentation/screens/provider_creds_list/__init__.py +0 -0
- sourcerer/presentation/screens/provider_creds_list/main.py +164 -0
- sourcerer/presentation/screens/provider_creds_list/styles.tcss +65 -0
- sourcerer/presentation/screens/provider_creds_registration/__init__.py +0 -0
- sourcerer/presentation/screens/provider_creds_registration/main.py +264 -0
- sourcerer/presentation/screens/provider_creds_registration/styles.tcss +42 -0
- sourcerer/presentation/screens/question/__init__.py +0 -0
- sourcerer/presentation/screens/question/main.py +31 -0
- sourcerer/presentation/screens/question/styles.tcss +33 -0
- sourcerer/presentation/screens/shared/__init__.py +0 -0
- sourcerer/presentation/screens/shared/containers.py +13 -0
- sourcerer/presentation/screens/shared/widgets/__init__.py +0 -0
- sourcerer/presentation/screens/shared/widgets/button.py +54 -0
- sourcerer/presentation/screens/shared/widgets/labeled_input.py +80 -0
- sourcerer/presentation/screens/storage_action_progress/__init__.py +0 -0
- sourcerer/presentation/screens/storage_action_progress/main.py +476 -0
- sourcerer/presentation/screens/storage_action_progress/styles.tcss +43 -0
- sourcerer/presentation/settings.py +31 -0
- sourcerer/presentation/themes/__init__.py +0 -0
- sourcerer/presentation/themes/github_dark.py +21 -0
- sourcerer/presentation/utils.py +69 -0
- sourcerer/settings.py +72 -0
- sourcerer/utils.py +32 -0
@@ -0,0 +1,476 @@
|
|
1
|
+
import os
|
2
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import List
|
6
|
+
|
7
|
+
from rich.text import Text
|
8
|
+
from textual import on
|
9
|
+
from textual.app import ComposeResult
|
10
|
+
from textual.color import Gradient
|
11
|
+
from textual.containers import Container, VerticalScroll, Horizontal, Center
|
12
|
+
from textual.css.query import NoMatches
|
13
|
+
from textual.screen import ModalScreen
|
14
|
+
from textual.widgets import ProgressBar, Label, Rule
|
15
|
+
|
16
|
+
from sourcerer.domain.storage_provider.services import BaseStorageProviderService
|
17
|
+
from sourcerer.presentation.screens.question.main import QuestionScreen
|
18
|
+
from sourcerer.presentation.screens.shared.widgets.button import Button
|
19
|
+
from sourcerer.settings import MAX_PARALLEL_DOWNLOADS
|
20
|
+
|
21
|
+
gradient = Gradient.from_colors(
|
22
|
+
"#881177",
|
23
|
+
"#aa3355",
|
24
|
+
"#cc6666",
|
25
|
+
"#ee9944",
|
26
|
+
"#eedd00",
|
27
|
+
"#99dd55",
|
28
|
+
"#44dd88",
|
29
|
+
"#22ccbb",
|
30
|
+
"#00bbcc",
|
31
|
+
"#0099cc",
|
32
|
+
"#3366bb",
|
33
|
+
"#663399",
|
34
|
+
)
|
35
|
+
|
36
|
+
gradient2 = Gradient.from_colors(
|
37
|
+
"#aa3355",
|
38
|
+
"#663399",
|
39
|
+
"#0099cc",
|
40
|
+
)
|
41
|
+
|
42
|
+
|
43
|
+
@dataclass
|
44
|
+
class Key:
|
45
|
+
"""
|
46
|
+
Base class for representing a key in storage operations.
|
47
|
+
|
48
|
+
Attributes:
|
49
|
+
display_name (str): Name to display in the UI
|
50
|
+
uuid (str): Unique identifier for the key
|
51
|
+
path (str): Path to the file or directory
|
52
|
+
"""
|
53
|
+
|
54
|
+
display_name: str
|
55
|
+
uuid: str
|
56
|
+
path: Path
|
57
|
+
|
58
|
+
|
59
|
+
class DownloadKey(Key):
|
60
|
+
"""
|
61
|
+
Represents a key for download operations.
|
62
|
+
|
63
|
+
Inherits all attributes from the base Key class.
|
64
|
+
"""
|
65
|
+
|
66
|
+
|
67
|
+
class DeleteKey(Key):
|
68
|
+
"""
|
69
|
+
Represents a key for delete operations.
|
70
|
+
|
71
|
+
Inherits all attributes from the base Key class.
|
72
|
+
"""
|
73
|
+
|
74
|
+
|
75
|
+
@dataclass
|
76
|
+
class UploadKey(Key):
|
77
|
+
"""
|
78
|
+
Represents a key for upload operations.
|
79
|
+
|
80
|
+
Inherits all attributes from the base Key class and adds destination path.
|
81
|
+
|
82
|
+
Attributes:
|
83
|
+
dest_path (str): Destination path for the uploaded file
|
84
|
+
"""
|
85
|
+
|
86
|
+
dest_path: str
|
87
|
+
|
88
|
+
|
89
|
+
class StorageActionProgressScreen(ModalScreen):
|
90
|
+
"""
|
91
|
+
A modal screen that displays progress for storage operations (download, upload, delete).
|
92
|
+
|
93
|
+
This screen shows progress bars for the overall operation and for individual files,
|
94
|
+
allowing users to monitor and cancel operations in progress.
|
95
|
+
"""
|
96
|
+
|
97
|
+
CSS_PATH = "styles.tcss"
|
98
|
+
|
99
|
+
def __init__(
|
100
|
+
self,
|
101
|
+
storage_name: str,
|
102
|
+
path: str,
|
103
|
+
provider_service: BaseStorageProviderService | None,
|
104
|
+
keys: List[UploadKey | DownloadKey | DeleteKey],
|
105
|
+
action: str,
|
106
|
+
*args,
|
107
|
+
**kwargs,
|
108
|
+
):
|
109
|
+
"""
|
110
|
+
Initialize the storage action progress screen.
|
111
|
+
|
112
|
+
Args:
|
113
|
+
storage_name (str): Name of the storage being operated on
|
114
|
+
path (str): Path within the storage
|
115
|
+
provider_service (BaseStorageProviderService | None): Service for interacting with the storage provider
|
116
|
+
keys (List[Key]): List of keys representing files/folders to process
|
117
|
+
action (str): Type of action being performed ('download', 'upload', or 'delete')
|
118
|
+
*args: Additional positional arguments to pass to parent class
|
119
|
+
**kwargs: Additional keyword arguments to pass to parent class
|
120
|
+
"""
|
121
|
+
super().__init__(*args, **kwargs)
|
122
|
+
self.storage_name = storage_name
|
123
|
+
self.provider_service = provider_service
|
124
|
+
self.action = action
|
125
|
+
self.path = path
|
126
|
+
self.keys = keys
|
127
|
+
self.files_has_been_processed = False
|
128
|
+
self.active_worker = None
|
129
|
+
self.active_executor = None
|
130
|
+
|
131
|
+
def compose(self) -> ComposeResult:
|
132
|
+
"""
|
133
|
+
Compose the UI elements of the screen.
|
134
|
+
|
135
|
+
Creates a container with a main progress bar, individual progress bars for each file,
|
136
|
+
and a cancel button.
|
137
|
+
|
138
|
+
Returns:
|
139
|
+
ComposeResult: The composed UI elements
|
140
|
+
"""
|
141
|
+
with Container(id="StorageActionProgress"):
|
142
|
+
with Center():
|
143
|
+
yield ProgressBar(
|
144
|
+
show_eta=False,
|
145
|
+
total=len(self.keys),
|
146
|
+
id="progress_bar",
|
147
|
+
gradient=gradient2,
|
148
|
+
)
|
149
|
+
|
150
|
+
yield Rule()
|
151
|
+
|
152
|
+
with VerticalScroll(id="progress_files"):
|
153
|
+
for key in self.keys:
|
154
|
+
with Horizontal(
|
155
|
+
classes="progress_file", id=f"progress_files_{key.uuid}"
|
156
|
+
):
|
157
|
+
yield Label(
|
158
|
+
Text(key.display_name, overflow="ellipsis"),
|
159
|
+
id=f"progress_file_{key.uuid}",
|
160
|
+
classes="label",
|
161
|
+
).with_tooltip(key.display_name)
|
162
|
+
yield ProgressBar(
|
163
|
+
total=1,
|
164
|
+
show_percentage=False,
|
165
|
+
id=f"progress_bar_{key.uuid}",
|
166
|
+
gradient=gradient2,
|
167
|
+
)
|
168
|
+
if Path(key.path).is_dir():
|
169
|
+
with Horizontal(classes="progress_file_details"):
|
170
|
+
yield Label(
|
171
|
+
"",
|
172
|
+
id=f"progress_file_details_{key.uuid}",
|
173
|
+
classes="label",
|
174
|
+
)
|
175
|
+
with Horizontal(id="controls"):
|
176
|
+
yield Button("Cancel", name="cancel")
|
177
|
+
|
178
|
+
def on_mount(self) -> None:
|
179
|
+
"""
|
180
|
+
Handle the mount event when the screen is first displayed.
|
181
|
+
|
182
|
+
Sets the border title and starts the appropriate worker thread based on the action type
|
183
|
+
(download, delete, or upload).
|
184
|
+
"""
|
185
|
+
self.query_one("#StorageActionProgress").border_title = (
|
186
|
+
f"{self.action.capitalize()} {len(self.keys)} files from {self.storage_name}"
|
187
|
+
)
|
188
|
+
|
189
|
+
if self.action == "download":
|
190
|
+
self.active_worker = self.run_worker(self.download_files, thread=True)
|
191
|
+
elif self.action == "delete":
|
192
|
+
self.action_worker = self.run_worker(self.delete_files, thread=True)
|
193
|
+
elif self.action == "upload":
|
194
|
+
self.action_worker = self.run_worker(self.upload_files, thread=True)
|
195
|
+
|
196
|
+
@on(Button.Click)
|
197
|
+
def on_button_click(self, event: Button.Click) -> None:
|
198
|
+
"""
|
199
|
+
Handle button click events.
|
200
|
+
|
201
|
+
Processes the cancel button click by either dismissing the screen if processing is complete
|
202
|
+
or showing a confirmation dialog if processing is still in progress.
|
203
|
+
|
204
|
+
Args:
|
205
|
+
event (Button.Click): The button click event
|
206
|
+
"""
|
207
|
+
if event.action != "cancel":
|
208
|
+
return
|
209
|
+
|
210
|
+
if self.files_has_been_processed:
|
211
|
+
self.dismiss()
|
212
|
+
return
|
213
|
+
|
214
|
+
self.app.push_screen(
|
215
|
+
QuestionScreen("Are you sure you want to cancel process?"),
|
216
|
+
callback=self.exit_callback,
|
217
|
+
)
|
218
|
+
|
219
|
+
def exit_callback(self, result):
|
220
|
+
"""
|
221
|
+
Callback function to handle the result of the confirmation dialog.
|
222
|
+
If the user confirms, it cancels the ongoing operation and dismisses the screen.
|
223
|
+
Args:
|
224
|
+
result (bool): The result of the confirmation dialog
|
225
|
+
|
226
|
+
"""
|
227
|
+
if not result:
|
228
|
+
return
|
229
|
+
if self.active_executor:
|
230
|
+
self.active_executor.shutdown(cancel_futures=True)
|
231
|
+
self.active_executor = None
|
232
|
+
if self.active_worker:
|
233
|
+
self.active_worker.cancel()
|
234
|
+
self.active_worker = None
|
235
|
+
self.dismiss()
|
236
|
+
|
237
|
+
async def download_files(self):
|
238
|
+
"""
|
239
|
+
Download files from storage and update progress bars.
|
240
|
+
This method handles the download of multiple files, updating progress bars
|
241
|
+
and handling various error conditions that might occur during the process.
|
242
|
+
"""
|
243
|
+
main_progress_bar = self.query_one("#progress_bar")
|
244
|
+
failed_downloads = []
|
245
|
+
|
246
|
+
with ThreadPoolExecutor(max_workers=MAX_PARALLEL_DOWNLOADS) as executor:
|
247
|
+
self.active_executor = executor
|
248
|
+
futures = [
|
249
|
+
executor.submit(
|
250
|
+
self.download_file,
|
251
|
+
os.path.join(self.path, key.path) if self.path else key.path,
|
252
|
+
key.uuid,
|
253
|
+
main_progress_bar,
|
254
|
+
)
|
255
|
+
for key in self.keys
|
256
|
+
]
|
257
|
+
|
258
|
+
for future in as_completed(futures):
|
259
|
+
if future.exception():
|
260
|
+
failed_downloads.append(future)
|
261
|
+
self.files_has_been_processed = True
|
262
|
+
|
263
|
+
self.active_executor = None
|
264
|
+
if failed_downloads:
|
265
|
+
self.notify(
|
266
|
+
f"Failed to download {len(failed_downloads)} files", severity="error"
|
267
|
+
)
|
268
|
+
|
269
|
+
def download_file(self, key, uuid, main_progress_bar):
|
270
|
+
"""
|
271
|
+
Download a file from storage and update progress bars.
|
272
|
+
|
273
|
+
This method handles the download of a single file, updating progress bars
|
274
|
+
and handling various error conditions that might occur during the process.
|
275
|
+
|
276
|
+
Args:
|
277
|
+
key (str): The key/path of the file to download
|
278
|
+
uuid (str): Unique identifier for the file
|
279
|
+
main_progress_bar (ProgressBar): The main progress bar to update
|
280
|
+
"""
|
281
|
+
|
282
|
+
if not self.provider_service:
|
283
|
+
self.notify(f"Failed to download {key}", severity="error")
|
284
|
+
return
|
285
|
+
|
286
|
+
def progress_callback(progress_bar, chunk):
|
287
|
+
progress_bar.advance(chunk)
|
288
|
+
|
289
|
+
progress_bar = self.query_one(f"#progress_bar_{uuid}")
|
290
|
+
|
291
|
+
try:
|
292
|
+
# Step 1: Get file size
|
293
|
+
try:
|
294
|
+
file_size = self.provider_service.get_file_size(self.storage_name, key)
|
295
|
+
progress_bar.total = file_size # type: ignore
|
296
|
+
except Exception as ex:
|
297
|
+
self.notify(
|
298
|
+
f"Failed to get file size for {key}: {str(ex)}", severity="error"
|
299
|
+
)
|
300
|
+
self.log.error(f"Error getting file size: {ex}")
|
301
|
+
return
|
302
|
+
|
303
|
+
# Step 2: Download the file
|
304
|
+
try:
|
305
|
+
self.provider_service.download_storage_item(
|
306
|
+
self.storage_name,
|
307
|
+
key,
|
308
|
+
lambda chunk: progress_callback(progress_bar, chunk),
|
309
|
+
)
|
310
|
+
except Exception as ex:
|
311
|
+
self.notify(f"Failed to download {key}: {str(ex)}", severity="error")
|
312
|
+
self.log.error(f"Error downloading file: {ex}")
|
313
|
+
return
|
314
|
+
|
315
|
+
# Step 3: Ensure progress bar is complete
|
316
|
+
if progress_bar.progress != progress_bar.total: # type: ignore
|
317
|
+
try:
|
318
|
+
progress_bar.progress = file_size # type: ignore
|
319
|
+
except Exception as ex:
|
320
|
+
self.log.error(f"Error updating progress bar: {ex}")
|
321
|
+
# Non-critical error, continue execution
|
322
|
+
except Exception as ex:
|
323
|
+
# Catch any unexpected exceptions
|
324
|
+
self.notify(
|
325
|
+
f"Unexpected error downloading {key}: {str(ex)}", severity="error"
|
326
|
+
)
|
327
|
+
self.log.error(f"Unexpected error: {ex}")
|
328
|
+
finally:
|
329
|
+
main_progress_bar.advance(1)
|
330
|
+
|
331
|
+
async def delete_files(self):
|
332
|
+
"""
|
333
|
+
Delete files from storage and update progress bars.
|
334
|
+
|
335
|
+
This method handles the deletion of multiple files, updating progress bars
|
336
|
+
and handling various error conditions that might occur during the process.
|
337
|
+
"""
|
338
|
+
main_progress_bar = self.query_one("#progress_bar")
|
339
|
+
failed_downloads = []
|
340
|
+
|
341
|
+
with ThreadPoolExecutor(max_workers=MAX_PARALLEL_DOWNLOADS) as executor:
|
342
|
+
self.active_executor = executor
|
343
|
+
futures = [
|
344
|
+
executor.submit(
|
345
|
+
self.delete_file,
|
346
|
+
os.path.join(self.path, key.path) if self.path else key.path,
|
347
|
+
key.uuid,
|
348
|
+
main_progress_bar,
|
349
|
+
)
|
350
|
+
for key in self.keys
|
351
|
+
]
|
352
|
+
|
353
|
+
for future in as_completed(futures):
|
354
|
+
if future.exception():
|
355
|
+
failed_downloads.append(future)
|
356
|
+
self.files_has_been_processed = True
|
357
|
+
|
358
|
+
self.active_executor = None
|
359
|
+
|
360
|
+
def delete_file(self, key, uuid, main_progress_bar):
|
361
|
+
"""
|
362
|
+
Delete a file from storage and update progress bars.
|
363
|
+
|
364
|
+
This method handles the deletion of a single file, updating progress bars
|
365
|
+
and handling various error conditions that might occur during the process.
|
366
|
+
|
367
|
+
Args:
|
368
|
+
key (str): The key/path of the file to delete
|
369
|
+
uuid (str): Unique identifier for the file
|
370
|
+
main_progress_bar (ProgressBar): The main progress bar to update
|
371
|
+
""" ""
|
372
|
+
if not self.provider_service:
|
373
|
+
self.notify(f"Failed to delete {key}", severity="error")
|
374
|
+
main_progress_bar.advance(1)
|
375
|
+
return
|
376
|
+
|
377
|
+
progress_bar = self.query_one(f"#progress_bar_{uuid}")
|
378
|
+
try:
|
379
|
+
progress_bar.total = 1 # type: ignore
|
380
|
+
self.provider_service.delete_storage_item(self.storage_name, key)
|
381
|
+
progress_bar.advance(1) # type: ignore
|
382
|
+
except Exception:
|
383
|
+
self.notify(f"Failed to delete {key}", severity="error")
|
384
|
+
raise
|
385
|
+
finally:
|
386
|
+
main_progress_bar.advance(1)
|
387
|
+
|
388
|
+
async def upload_files(self):
|
389
|
+
"""
|
390
|
+
Upload files to storage and update progress bars.
|
391
|
+
|
392
|
+
This method handles the upload of files, updating progress bars and
|
393
|
+
handling various error conditions that might occur during the process.
|
394
|
+
"""
|
395
|
+
main_progress_bar = self.query_one("#progress_bar")
|
396
|
+
failed_downloads = []
|
397
|
+
if not self.provider_service:
|
398
|
+
self.notify("Failed to upload files", severity="error")
|
399
|
+
return
|
400
|
+
|
401
|
+
for key in self.keys:
|
402
|
+
source_path = Path(key.path)
|
403
|
+
if source_path.is_file():
|
404
|
+
self.provider_service.upload_storage_item(
|
405
|
+
storage=self.storage_name,
|
406
|
+
source_path=key.path,
|
407
|
+
dest_path=str(Path(self.path) / key.dest_path) if self.path else key.dest_path, # type: ignore
|
408
|
+
)
|
409
|
+
self.files_has_been_processed = True
|
410
|
+
elif source_path.is_dir():
|
411
|
+
|
412
|
+
files_n = len([i for i in source_path.rglob("*") if i.is_file()])
|
413
|
+
progress_bar = self.query_one(f"#progress_bar_{key.uuid}")
|
414
|
+
progress_bar.total = files_n # type: ignore
|
415
|
+
with ThreadPoolExecutor(max_workers=MAX_PARALLEL_DOWNLOADS) as executor:
|
416
|
+
self.active_executor = executor
|
417
|
+
futures = [
|
418
|
+
executor.submit(
|
419
|
+
self.upload_file,
|
420
|
+
obj,
|
421
|
+
Path(obj).relative_to(source_path),
|
422
|
+
os.path.join(
|
423
|
+
source_path.name,
|
424
|
+
str(Path(obj).relative_to(source_path)),
|
425
|
+
),
|
426
|
+
key.uuid,
|
427
|
+
)
|
428
|
+
for obj in source_path.rglob("*")
|
429
|
+
if obj.is_file()
|
430
|
+
]
|
431
|
+
|
432
|
+
for future in as_completed(futures):
|
433
|
+
if future.exception():
|
434
|
+
failed_downloads.append(future)
|
435
|
+
self.files_has_been_processed = True
|
436
|
+
self.active_executor = None
|
437
|
+
try:
|
438
|
+
self.query_one(f"#progress_file_details_{key.uuid}").remove()
|
439
|
+
except Exception:
|
440
|
+
self.log(f"Failed to remove progress details for {key.uuid}")
|
441
|
+
main_progress_bar.advance(1) # type: ignore
|
442
|
+
|
443
|
+
def upload_file(self, source, rel_source, destination, uuid):
|
444
|
+
"""
|
445
|
+
Upload a file to storage and update progress bars.
|
446
|
+
|
447
|
+
This method handles the upload of a single file, updating progress bars
|
448
|
+
and handling various error conditions that might occur during the process.
|
449
|
+
Args:
|
450
|
+
source (str): The source path of the file to upload
|
451
|
+
rel_source (str): The relative path of the file in the source directory
|
452
|
+
destination (str): The destination path in the storage
|
453
|
+
uuid (str): Unique identifier for the file
|
454
|
+
"""
|
455
|
+
if not self.provider_service:
|
456
|
+
self.notify(f"Failed to upload {source}", severity="error")
|
457
|
+
return
|
458
|
+
progress_bar = self.query_one(f"#progress_bar_{uuid}")
|
459
|
+
details_container = None
|
460
|
+
try:
|
461
|
+
details_container = self.query_one(f"#progress_file_details_{uuid}")
|
462
|
+
except NoMatches:
|
463
|
+
pass
|
464
|
+
if details_container:
|
465
|
+
details_container.update(Text(f"({rel_source})", overflow="ellipsis")) # type: ignore
|
466
|
+
try:
|
467
|
+
self.provider_service.upload_storage_item(
|
468
|
+
storage=self.storage_name,
|
469
|
+
source_path=source,
|
470
|
+
dest_path=(
|
471
|
+
str(Path(self.path) / destination) if self.path else destination
|
472
|
+
),
|
473
|
+
)
|
474
|
+
progress_bar.advance(1) # type: ignore
|
475
|
+
except Exception:
|
476
|
+
self.notify(f"Failed to upload {source}", severity="error")
|
@@ -0,0 +1,43 @@
|
|
1
|
+
|
2
|
+
Container {
|
3
|
+
height: auto;
|
4
|
+
}
|
5
|
+
|
6
|
+
|
7
|
+
StorageActionProgressScreen {
|
8
|
+
align: center middle;
|
9
|
+
content-align: center top;
|
10
|
+
|
11
|
+
|
12
|
+
& > #StorageActionProgress {
|
13
|
+
padding: 1 2 0 2;
|
14
|
+
margin: 0 0;
|
15
|
+
width: 70;
|
16
|
+
height: 25;
|
17
|
+
border: solid $secondary-background;
|
18
|
+
border-title-color: $primary-lighten-2;
|
19
|
+
|
20
|
+
#progress_files {
|
21
|
+
padding-top: 1;
|
22
|
+
}
|
23
|
+
|
24
|
+
.progress_file_details {
|
25
|
+
align-horizontal: right;
|
26
|
+
color: $panel-lighten-3;
|
27
|
+
padding-right: 2;
|
28
|
+
}
|
29
|
+
|
30
|
+
.progress_file {
|
31
|
+
height: auto;
|
32
|
+
|
33
|
+
Label {
|
34
|
+
width: 30;
|
35
|
+
text-overflow: ellipsis;
|
36
|
+
text-wrap: nowrap;
|
37
|
+
padding-right: 1;
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
"""Application settings and constants.
|
2
|
+
|
3
|
+
This module contains various configuration constants and settings used throughout
|
4
|
+
the application, including UI elements and display configurations.
|
5
|
+
"""
|
6
|
+
|
7
|
+
NO_DATA_LOGO = """
|
8
|
+
### ###
|
9
|
+
### ###
|
10
|
+
#### ####
|
11
|
+
#### ####
|
12
|
+
#######################
|
13
|
+
###### ######### ######
|
14
|
+
###### ######### ######
|
15
|
+
###################################
|
16
|
+
###################################
|
17
|
+
### ####################### ###
|
18
|
+
### ####################### ###
|
19
|
+
### ### ### ###
|
20
|
+
####### #######
|
21
|
+
####### #######
|
22
|
+
|
23
|
+
|
24
|
+
### ## ## #
|
25
|
+
#### ## #### ###### #### #### #####
|
26
|
+
## ## ## ## ## ## ## #### # ####
|
27
|
+
## #### ## ## ## ## ## ## # ## ##
|
28
|
+
## ## #### ##### ###### ### ### #
|
29
|
+
"""
|
30
|
+
|
31
|
+
MIN_SECTION_DIMENSION = 10
|
File without changes
|
@@ -0,0 +1,21 @@
|
|
1
|
+
from textual.theme import Theme
|
2
|
+
|
3
|
+
github_dark_theme = Theme(
|
4
|
+
name="github-dark",
|
5
|
+
primary="#FFA656",
|
6
|
+
secondary="#81A1C1",
|
7
|
+
accent="#B48EAD",
|
8
|
+
foreground="#CDD9E5",
|
9
|
+
background="#1C2128",
|
10
|
+
success="#A3BE8C",
|
11
|
+
warning="#EBCB8B",
|
12
|
+
error="#BF616A",
|
13
|
+
surface="#22272E",
|
14
|
+
panel="#434C5E",
|
15
|
+
dark=True,
|
16
|
+
variables={
|
17
|
+
"block-cursor-text-style": "none",
|
18
|
+
"footer-key-foreground": "#88C0D0",
|
19
|
+
"input-selection-background": "#81a1c1 35%",
|
20
|
+
},
|
21
|
+
)
|
@@ -0,0 +1,69 @@
|
|
1
|
+
"""
|
2
|
+
Utility functions for the presentation layer.
|
3
|
+
|
4
|
+
This module provides helper functions for the presentation layer,
|
5
|
+
particularly for retrieving and initializing storage provider services.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from sourcerer.domain.storage_provider.services import BaseStorageProviderService
|
9
|
+
from sourcerer.infrastructure.access_credentials.registry import (
|
10
|
+
access_credential_method_registry,
|
11
|
+
)
|
12
|
+
from sourcerer.infrastructure.storage_provider.registry import storage_provider_registry
|
13
|
+
|
14
|
+
|
15
|
+
def get_provider_service_by_access_uuid(
|
16
|
+
uuid, credentials_service
|
17
|
+
) -> BaseStorageProviderService | None:
|
18
|
+
"""
|
19
|
+
Retrieves the provider service associated with the given access credentials UUID.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
uuid (str): The UUID of the access credentials.
|
23
|
+
credentials_service: Credentials service
|
24
|
+
|
25
|
+
Returns:
|
26
|
+
The provider service instance corresponding to the access credentials.
|
27
|
+
"""
|
28
|
+
access_credentials = credentials_service.get(uuid)
|
29
|
+
return get_provider_service_by_access_credentials(access_credentials)
|
30
|
+
|
31
|
+
|
32
|
+
def get_provider_service_by_access_credentials(
|
33
|
+
credentials,
|
34
|
+
) -> BaseStorageProviderService | None:
|
35
|
+
"""
|
36
|
+
Retrieves a storage provider service instance using the given access credentials.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
credentials: An object containing provider and credentials type information.
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
An instance of the storage provider service if both the credentials service
|
43
|
+
and provider service class are found; otherwise, returns None.
|
44
|
+
|
45
|
+
Flow:
|
46
|
+
1. Fetch the credentials service using the provider and credentials type.
|
47
|
+
2. If the credentials service is not found, return None.
|
48
|
+
3. Fetch the provider service class using the provider.
|
49
|
+
4. If the provider service class is not found, return None.
|
50
|
+
5. Authenticate the credentials using the credentials service.
|
51
|
+
6. Return an instance of the provider service class initialized with the
|
52
|
+
authenticated credentials.
|
53
|
+
"""
|
54
|
+
|
55
|
+
credentials_service = access_credential_method_registry.get_by_provider_and_name(
|
56
|
+
credentials.provider, credentials.credentials_type
|
57
|
+
)
|
58
|
+
|
59
|
+
if not credentials_service:
|
60
|
+
return
|
61
|
+
|
62
|
+
provider_service_class = storage_provider_registry.get_by_provider(
|
63
|
+
credentials.provider
|
64
|
+
)
|
65
|
+
if not provider_service_class:
|
66
|
+
return
|
67
|
+
|
68
|
+
auth_credentials = credentials_service().authenticate(credentials.credentials)
|
69
|
+
return provider_service_class(auth_credentials)
|