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.
Files changed (95) hide show
  1. data_sourcerer-0.1.0.dist-info/METADATA +52 -0
  2. data_sourcerer-0.1.0.dist-info/RECORD +95 -0
  3. data_sourcerer-0.1.0.dist-info/WHEEL +5 -0
  4. data_sourcerer-0.1.0.dist-info/entry_points.txt +2 -0
  5. data_sourcerer-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. data_sourcerer-0.1.0.dist-info/top_level.txt +1 -0
  7. sourcerer/__init__.py +15 -0
  8. sourcerer/domain/__init__.py +13 -0
  9. sourcerer/domain/access_credentials/__init__.py +7 -0
  10. sourcerer/domain/access_credentials/entities.py +53 -0
  11. sourcerer/domain/access_credentials/exceptions.py +17 -0
  12. sourcerer/domain/access_credentials/repositories.py +84 -0
  13. sourcerer/domain/access_credentials/services.py +91 -0
  14. sourcerer/domain/file_system/__init__.py +7 -0
  15. sourcerer/domain/file_system/entities.py +70 -0
  16. sourcerer/domain/file_system/exceptions.py +17 -0
  17. sourcerer/domain/file_system/services.py +64 -0
  18. sourcerer/domain/shared/__init__.py +6 -0
  19. sourcerer/domain/shared/entities.py +18 -0
  20. sourcerer/domain/storage_provider/__init__.py +7 -0
  21. sourcerer/domain/storage_provider/entities.py +86 -0
  22. sourcerer/domain/storage_provider/exceptions.py +17 -0
  23. sourcerer/domain/storage_provider/services.py +130 -0
  24. sourcerer/infrastructure/__init__.py +13 -0
  25. sourcerer/infrastructure/access_credentials/__init__.py +7 -0
  26. sourcerer/infrastructure/access_credentials/exceptions.py +16 -0
  27. sourcerer/infrastructure/access_credentials/registry.py +120 -0
  28. sourcerer/infrastructure/access_credentials/repositories.py +119 -0
  29. sourcerer/infrastructure/access_credentials/services.py +396 -0
  30. sourcerer/infrastructure/db/__init__.py +6 -0
  31. sourcerer/infrastructure/db/config.py +73 -0
  32. sourcerer/infrastructure/db/models.py +47 -0
  33. sourcerer/infrastructure/file_system/__init__.py +7 -0
  34. sourcerer/infrastructure/file_system/exceptions.py +89 -0
  35. sourcerer/infrastructure/file_system/services.py +147 -0
  36. sourcerer/infrastructure/storage_provider/__init__.py +7 -0
  37. sourcerer/infrastructure/storage_provider/exceptions.py +78 -0
  38. sourcerer/infrastructure/storage_provider/registry.py +84 -0
  39. sourcerer/infrastructure/storage_provider/services.py +509 -0
  40. sourcerer/infrastructure/utils.py +106 -0
  41. sourcerer/presentation/__init__.py +12 -0
  42. sourcerer/presentation/app.py +36 -0
  43. sourcerer/presentation/di_container.py +46 -0
  44. sourcerer/presentation/screens/__init__.py +0 -0
  45. sourcerer/presentation/screens/critical_error/__init__.py +0 -0
  46. sourcerer/presentation/screens/critical_error/main.py +78 -0
  47. sourcerer/presentation/screens/critical_error/styles.tcss +41 -0
  48. sourcerer/presentation/screens/file_system_finder/main.py +248 -0
  49. sourcerer/presentation/screens/file_system_finder/styles.tcss +44 -0
  50. sourcerer/presentation/screens/file_system_finder/widgets/__init__.py +0 -0
  51. sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +810 -0
  52. sourcerer/presentation/screens/main/__init__.py +0 -0
  53. sourcerer/presentation/screens/main/main.py +469 -0
  54. sourcerer/presentation/screens/main/messages/__init__.py +0 -0
  55. sourcerer/presentation/screens/main/messages/delete_request.py +12 -0
  56. sourcerer/presentation/screens/main/messages/download_request.py +12 -0
  57. sourcerer/presentation/screens/main/messages/preview_request.py +10 -0
  58. sourcerer/presentation/screens/main/messages/resizing_rule.py +21 -0
  59. sourcerer/presentation/screens/main/messages/select_storage_item.py +11 -0
  60. sourcerer/presentation/screens/main/messages/uncheck_files_request.py +8 -0
  61. sourcerer/presentation/screens/main/messages/upload_request.py +10 -0
  62. sourcerer/presentation/screens/main/mixins/__init__.py +0 -0
  63. sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +144 -0
  64. sourcerer/presentation/screens/main/styles.tcss +32 -0
  65. sourcerer/presentation/screens/main/widgets/__init__.py +0 -0
  66. sourcerer/presentation/screens/main/widgets/gradient.py +45 -0
  67. sourcerer/presentation/screens/main/widgets/resizing_rule.py +67 -0
  68. sourcerer/presentation/screens/main/widgets/storage_content.py +691 -0
  69. sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +143 -0
  70. sourcerer/presentation/screens/preview_content/__init__.py +0 -0
  71. sourcerer/presentation/screens/preview_content/main.py +59 -0
  72. sourcerer/presentation/screens/preview_content/styles.tcss +26 -0
  73. sourcerer/presentation/screens/provider_creds_list/__init__.py +0 -0
  74. sourcerer/presentation/screens/provider_creds_list/main.py +164 -0
  75. sourcerer/presentation/screens/provider_creds_list/styles.tcss +65 -0
  76. sourcerer/presentation/screens/provider_creds_registration/__init__.py +0 -0
  77. sourcerer/presentation/screens/provider_creds_registration/main.py +264 -0
  78. sourcerer/presentation/screens/provider_creds_registration/styles.tcss +42 -0
  79. sourcerer/presentation/screens/question/__init__.py +0 -0
  80. sourcerer/presentation/screens/question/main.py +31 -0
  81. sourcerer/presentation/screens/question/styles.tcss +33 -0
  82. sourcerer/presentation/screens/shared/__init__.py +0 -0
  83. sourcerer/presentation/screens/shared/containers.py +13 -0
  84. sourcerer/presentation/screens/shared/widgets/__init__.py +0 -0
  85. sourcerer/presentation/screens/shared/widgets/button.py +54 -0
  86. sourcerer/presentation/screens/shared/widgets/labeled_input.py +80 -0
  87. sourcerer/presentation/screens/storage_action_progress/__init__.py +0 -0
  88. sourcerer/presentation/screens/storage_action_progress/main.py +476 -0
  89. sourcerer/presentation/screens/storage_action_progress/styles.tcss +43 -0
  90. sourcerer/presentation/settings.py +31 -0
  91. sourcerer/presentation/themes/__init__.py +0 -0
  92. sourcerer/presentation/themes/github_dark.py +21 -0
  93. sourcerer/presentation/utils.py +69 -0
  94. sourcerer/settings.py +72 -0
  95. sourcerer/utils.py +32 -0
@@ -0,0 +1,509 @@
1
+ """
2
+ Implementation of storage provider services.
3
+
4
+ This module provides concrete implementations of the BaseStorageProviderService
5
+ interface for various cloud storage providers.
6
+ """
7
+
8
+ from itertools import groupby
9
+ from pathlib import Path
10
+ from typing import List, Any, Callable
11
+
12
+ import humanize
13
+ from platformdirs import user_downloads_dir
14
+
15
+ from sourcerer.domain.shared.entities import StorageProvider
16
+ from sourcerer.domain.storage_provider.entities import (
17
+ StoragePermissions,
18
+ StorageContent,
19
+ Folder,
20
+ File,
21
+ Storage,
22
+ )
23
+ from sourcerer.domain.storage_provider.services import BaseStorageProviderService
24
+ from sourcerer.infrastructure.storage_provider.exceptions import (
25
+ ListStoragesException,
26
+ StoragePermissionException,
27
+ ListStorageItemsException,
28
+ ReadStorageItemsException,
29
+ DeleteStorageItemsException,
30
+ UploadStorageItemsException,
31
+ CredentialsNotFoundException,
32
+ )
33
+ from sourcerer.infrastructure.storage_provider.registry import storage_provider
34
+ from sourcerer.infrastructure.utils import generate_uuid, is_text_file
35
+ from sourcerer.settings import PATH_DELIMITER, PAGE_SIZE
36
+
37
+
38
+ @storage_provider(StorageProvider.S3)
39
+ class S3ProviderService(BaseStorageProviderService):
40
+ """
41
+ AWS S3 storage provider service implementation.
42
+
43
+ This class provides methods for interacting with AWS S3 storage,
44
+ implementing the BaseStorageProviderService interface.
45
+ """
46
+
47
+ def __init__(self, credentials: Any):
48
+ """
49
+ Initialize the service with AWS credentials.
50
+
51
+ Args:
52
+ credentials (Any): AWS session or credentials object
53
+ """
54
+ self.credentials = credentials
55
+
56
+ @property
57
+ def client(self):
58
+ """
59
+ Get the S3 client.
60
+
61
+ Returns:
62
+ boto3.client: S3 client object
63
+ """
64
+ if not self.credentials:
65
+ raise CredentialsNotFoundException()
66
+
67
+ session = self.credentials.session
68
+
69
+ client_args = {}
70
+ if self.credentials.endpoint_url:
71
+ client_args["endpoint_url"] = self.credentials.endpoint_url
72
+
73
+ return session.client("s3", **client_args)
74
+
75
+ @property
76
+ def resource(self):
77
+ """
78
+ Get the S3 resource.
79
+
80
+ Returns:
81
+ boto3.resource: S3 resource object
82
+ """
83
+ if not self.credentials:
84
+ raise CredentialsNotFoundException()
85
+
86
+ session = self.credentials.session
87
+
88
+ client_args = {}
89
+ if self.credentials.endpoint_url:
90
+ client_args["endpoint_url"] = self.credentials.endpoint_url
91
+ return session.resource("s3", **client_args)
92
+
93
+ def list_storages(self) -> List[Storage]:
94
+ """
95
+ Return a list of available S3 buckets.
96
+
97
+ Returns:
98
+ List[Storage]: List of storage objects representing S3 buckets
99
+
100
+ Raises:
101
+ ListStoragesException: If an error occurs while listing buckets
102
+ """
103
+ try:
104
+ response = self.client.list_buckets()
105
+ except Exception as ex:
106
+ raise ListStoragesException(str(ex)) from ex
107
+ return [
108
+ Storage(StorageProvider.S3, i.get("Name"), i.get("CreationDate"))
109
+ for i in response.get("Buckets")
110
+ ]
111
+
112
+ def get_storage_permissions(self, storage: str) -> List[StoragePermissions]:
113
+ """
114
+ Return the permissions for the specified S3 bucket.
115
+
116
+ Args:
117
+ storage (str): The bucket name
118
+
119
+ Returns:
120
+ List[StoragePermissions]: List of permission objects for the bucket
121
+
122
+ Raises:
123
+ StoragePermissionException: If an error occurs while getting permissions
124
+ """
125
+ try:
126
+ permissions = self.client.get_bucket_acl(Bucket=storage)
127
+ except Exception as ex:
128
+ raise StoragePermissionException(str(ex)) from ex
129
+ return [
130
+ StoragePermissions(name, [i["Permission"] for i in items])
131
+ for name, items in groupby(
132
+ permissions["Grants"],
133
+ key=lambda x: x["Grantee"]["DisplayName"] or x["Grantee"]["ID"],
134
+ )
135
+ ]
136
+
137
+ def list_storage_items(
138
+ self, storage: str, path: str = "", prefix: str = ""
139
+ ) -> StorageContent:
140
+ """
141
+ List items in the specified S3 bucket path with the given prefix.
142
+
143
+ Args:
144
+ storage (str): The bucket name
145
+ path (str, optional): The path within the bucket. Defaults to ''.
146
+ prefix (str, optional): Filter items by this prefix. Defaults to ''.
147
+
148
+ Returns:
149
+ StorageContent: Object containing files and folders at the specified location
150
+
151
+ Raises:
152
+ ListStorageItemsException: If an error occurs while listing items
153
+ """
154
+ if path and not path.endswith("/"):
155
+ path += "/"
156
+ try:
157
+ result = self.client.list_objects_v2(
158
+ Bucket=storage,
159
+ Prefix=path + prefix,
160
+ Delimiter=PATH_DELIMITER,
161
+ MaxKeys=PAGE_SIZE,
162
+ )
163
+ except Exception as ex:
164
+ raise ListStorageItemsException(str(ex)) from ex
165
+
166
+ folders = [
167
+ Folder(i.get("Prefix").replace(path, ""))
168
+ for i in result.get("CommonPrefixes", [])
169
+ if i.get("Prefix")
170
+ ]
171
+ files = [
172
+ File(
173
+ generate_uuid(),
174
+ i.get("Key").replace(path, ""),
175
+ humanize.naturalsize(i.get("Size")),
176
+ is_text_file(i.get("Key")),
177
+ i.get("LastModified"),
178
+ )
179
+ for i in result.get("Contents", [])
180
+ if i.get("Key").replace(path, "")
181
+ ]
182
+ return StorageContent(files=files, folders=folders)
183
+
184
+ def read_storage_item(self, storage: str, key: str) -> str:
185
+ """
186
+ Read and return the content of the specified S3 object.
187
+
188
+ Args:
189
+ storage (str): The bucket name
190
+ key (str): The key/path of the item to read
191
+
192
+ Returns:
193
+ bytes: The content of the S3 object
194
+
195
+ Raises:
196
+ ReadStorageItemsException: If an error occurs while reading the item
197
+ """
198
+ try:
199
+ content_object = self.resource.Object(storage, key)
200
+ return content_object.get()["Body"].read().decode("utf-8")
201
+ except Exception as ex:
202
+ raise ReadStorageItemsException(str(ex)) from ex
203
+
204
+ def delete_storage_item(self, storage: str, key: str) -> None:
205
+ """
206
+ Delete the specified S3 object.
207
+
208
+ Args:
209
+ storage (str): The bucket name
210
+ key (str): The key/path of the item to delete
211
+
212
+ Raises:
213
+ DeleteStorageItemsException: If an error occurs while deleting the item
214
+ """
215
+ try:
216
+ return self.resource.Object(storage, key).delete()
217
+ except Exception as ex:
218
+ raise DeleteStorageItemsException(str(ex)) from ex
219
+
220
+ def upload_storage_item(
221
+ self, storage: str, source_path: Path, dest_path: str | None = None
222
+ ) -> None:
223
+ """
224
+ Upload a file to the specified S3 bucket path.
225
+
226
+ Args:
227
+ storage (str): The bucket name
228
+ source_path (Path): Local file path to upload
229
+ dest_path (str, optional): Destination path in S3. Defaults to None.
230
+
231
+ Raises:
232
+ UploadStorageItemsException: If an error occurs while uploading the item
233
+ """
234
+ try:
235
+ self.client.upload_file(source_path, storage, dest_path or source_path.name)
236
+ except Exception as ex:
237
+ raise UploadStorageItemsException(str(ex)) from ex
238
+
239
+ def download_storage_item(
240
+ self, storage: str, key: str, progress_callback: Callable | None = None
241
+ ) -> str:
242
+ """
243
+ Download a file from S3 to local filesystem.
244
+
245
+ Args:
246
+ storage (str): The bucket name
247
+ key (str): The key/path of the item to download
248
+ progress_callback (Callable, optional): Callback function for progress updates. Defaults to None.
249
+
250
+ Returns:
251
+ str: Path to the downloaded file
252
+
253
+ Raises:
254
+ ReadStorageItemsException: If an error occurs while downloading the item
255
+ """
256
+ try:
257
+ download_path = Path(user_downloads_dir()) / Path(key).name
258
+ self.client.download_file(
259
+ storage, key, download_path, Callback=progress_callback
260
+ )
261
+ return str(download_path)
262
+ except Exception as ex:
263
+ raise ReadStorageItemsException(str(ex)) from ex
264
+
265
+ def get_file_size(self, storage: str, key: str) -> dict:
266
+ """
267
+ Get metadata for an S3 object without downloading content.
268
+
269
+ Args:
270
+ storage (str): The bucket name
271
+ key (str): The key/path of the item
272
+
273
+ Returns:
274
+ dict: Metadata for the specified S3 object
275
+
276
+ Raises:
277
+ ReadStorageItemsException: If an error occurs while getting metadata
278
+ """
279
+ try:
280
+ metadata = self.client.head_object(Bucket=storage, Key=key)
281
+ return metadata.get("ContentLength")
282
+ except Exception as ex:
283
+ raise ReadStorageItemsException(str(ex)) from ex
284
+
285
+
286
+ @storage_provider(StorageProvider.GoogleCloudStorage)
287
+ class GCPStorageProviderService(BaseStorageProviderService):
288
+ """
289
+ Google Cloud Platform storage provider service implementation.
290
+
291
+ This class provides methods for interacting with GCP Cloud Storage,
292
+ implementing the BaseStorageProviderService interface.
293
+ """
294
+
295
+ def __init__(self, credentials: Any):
296
+ """
297
+ Initialize the service with GCP credentials.
298
+
299
+ Args:
300
+ credentials (Any): GCP client or credentials object
301
+ """
302
+ self.client = credentials
303
+
304
+ def list_storages(self) -> List[Storage]:
305
+ """
306
+ Return a list of available GCP buckets.
307
+
308
+ Returns:
309
+ List[Storage]: List of storage objects representing GCP buckets
310
+
311
+ Raises:
312
+ ListStoragesException: If an error occurs while listing buckets
313
+ """
314
+ try:
315
+ return [
316
+ Storage(StorageProvider.GoogleCloudStorage, i.name, i.time_created)
317
+ for i in self.client.list_buckets()
318
+ ]
319
+ except Exception as ex:
320
+ raise ListStoragesException(str(ex)) from ex
321
+
322
+ def get_storage_permissions(self, storage: str) -> List[StoragePermissions]:
323
+ """
324
+ Return the permissions for the specified GCP bucket.
325
+
326
+ Args:
327
+ storage (str): The bucket name
328
+
329
+ Returns:
330
+ List[StoragePermissions]: List of permission objects for the bucket
331
+
332
+ Raises:
333
+ StoragePermissionException: If an error occurs while getting permissions
334
+ """
335
+ try:
336
+ bucket = self.client.get_bucket(storage)
337
+ policy = bucket.get_iam_policy()
338
+
339
+ result = {}
340
+ for role, members in policy.items():
341
+ for member in members:
342
+ member = member.split(":")[-1]
343
+ if member not in result:
344
+ result[member] = set()
345
+ result[member].add(role)
346
+ return [
347
+ StoragePermissions(member, roles) for member, roles in result.items()
348
+ ]
349
+ except Exception as ex:
350
+ raise StoragePermissionException(str(ex)) from ex
351
+
352
+ def list_storage_items(
353
+ self, storage: str, path: str = "", prefix: str = ""
354
+ ) -> StorageContent:
355
+ """
356
+ List items in the specified GCP bucket path with the given prefix.
357
+
358
+ Args:
359
+ storage (str): The bucket name
360
+ path (str, optional): The path within the bucket. Defaults to ''.
361
+ prefix (str, optional): Filter items by this prefix. Defaults to ''.
362
+
363
+ Returns:
364
+ StorageContent: Object containing files and folders at the specified location
365
+
366
+ Raises:
367
+ ListStorageItemsException: If an error occurs while listing items
368
+ """
369
+ try:
370
+
371
+ files = []
372
+ folders = []
373
+ if path and not path.endswith("/"):
374
+ path += "/"
375
+
376
+ bucket = self.client.bucket(storage)
377
+
378
+ blobs = bucket.list_blobs(
379
+ prefix=path + prefix, delimiter=PATH_DELIMITER, max_results=PAGE_SIZE
380
+ )
381
+
382
+ for blob in blobs:
383
+ files.append(
384
+ File(
385
+ generate_uuid(),
386
+ blob.name[len(path) :],
387
+ size=humanize.naturalsize(blob.size),
388
+ date_modified=blob.updated.date(),
389
+ is_text=is_text_file(blob.name),
390
+ )
391
+ )
392
+
393
+ for folder in blobs.prefixes:
394
+ relative_path = folder[len(path) :]
395
+ folders.append(Folder(relative_path))
396
+
397
+ return StorageContent(files=files, folders=folders)
398
+
399
+ except Exception as ex:
400
+ raise ListStorageItemsException(
401
+ f"Failed to list items in {storage}: {str(ex)}"
402
+ ) from ex
403
+
404
+ def read_storage_item(self, storage: str, key: str) -> str:
405
+ """
406
+ Read and return the content of the specified GCP object.
407
+
408
+ Args:
409
+ storage (str): The bucket name
410
+ key (str): The key/path of the item to read
411
+
412
+ Returns:
413
+ bytes: The content of the GCP object
414
+
415
+ Raises:
416
+ ReadStorageItemsException: If an error occurs while reading the item
417
+ """
418
+ try:
419
+ bucket = self.client.bucket(storage)
420
+ blob = bucket.get_blob(key)
421
+ content = blob.download_as_bytes()
422
+ return content.decode("utf-8")
423
+ except Exception as ex:
424
+ raise ReadStorageItemsException(str(ex)) from ex
425
+
426
+ def delete_storage_item(self, storage: str, key: str) -> None:
427
+ """
428
+ Delete the specified GCP object.
429
+
430
+ Args:
431
+ storage (str): The bucket name
432
+ key (str): The key/path of the item to delete
433
+
434
+ Raises:
435
+ DeleteStorageItemsException: If an error occurs while deleting the item
436
+ """
437
+ try:
438
+ bucket = self.client.bucket(storage)
439
+ blob = bucket.get_blob(key)
440
+ blob.delete()
441
+ except Exception as ex:
442
+ raise DeleteStorageItemsException(str(ex)) from ex
443
+
444
+ def upload_storage_item(
445
+ self, storage: str, source_path: Path, dest_path: str | None = None
446
+ ) -> None:
447
+ """
448
+ Upload a file to the specified GCP bucket path.
449
+
450
+ Args:
451
+ storage (str): The bucket name
452
+ source_path (Path): Local file path to upload
453
+ dest_path (str, optional): Destination path in GCP. Defaults to None.
454
+
455
+ Raises:
456
+ UploadStorageItemsException: If an error occurs while uploading the item
457
+ """
458
+ try:
459
+ bucket = self.client.bucket(storage)
460
+ bucket.blob(dest_path or source_path.name).upload_from_filename(source_path)
461
+ except Exception as ex:
462
+ raise UploadStorageItemsException(str(ex)) from ex
463
+
464
+ def download_storage_item(
465
+ self, storage: str, key: str, progress_callback: Callable | None = None
466
+ ) -> str:
467
+ """
468
+ Download a file from GCP to the local filesystem.
469
+
470
+ Args:
471
+ storage (str): The bucket name
472
+ key (str): The key/path of the item to download
473
+ progress_callback (Callable, optional): Callback function for progress updates. Defaults to None.
474
+
475
+ Returns:
476
+ str: Path to the downloaded file
477
+
478
+ Raises:
479
+ ReadStorageItemsException: If an error occurs while downloading the item
480
+ """
481
+ try:
482
+ bucket = self.client.bucket(storage)
483
+ blob = bucket.get_blob(key)
484
+ download_path = Path(user_downloads_dir()) / Path(key).name
485
+ blob.download_to_filename(str(download_path))
486
+ return str(download_path)
487
+ except Exception as ex:
488
+ raise ReadStorageItemsException(str(ex)) from ex
489
+
490
+ def get_file_size(self, storage: str, key: str) -> dict:
491
+ """
492
+ Get metadata for a GCP object without downloading content.
493
+
494
+ Args:
495
+ storage (str): The bucket name
496
+ key (str): The key/path of the item
497
+
498
+ Returns:
499
+ dict: Metadata for the specified GCP object
500
+
501
+ Raises:
502
+ ReadStorageItemsException: If an error occurs while getting metadata
503
+ """
504
+ try:
505
+ bucket = self.client.bucket(storage)
506
+ blob = bucket.get_blob(key)
507
+ return blob.size
508
+ except Exception as ex:
509
+ raise ReadStorageItemsException(str(ex)) from ex
@@ -0,0 +1,106 @@
1
+ """
2
+ Utility functions for the Sourcerer application.
3
+
4
+ This module provides various utility functions used throughout the application,
5
+ including UUID generation, MIME type detection, and file type checking.
6
+ """
7
+
8
+ import mimetypes
9
+ import uuid
10
+ from pathlib import Path
11
+
12
+ from sourcerer.settings import TEXT_EXTENSIONS
13
+
14
+
15
+ def is_text_mime(filename):
16
+ """
17
+ Determine if a file has a text MIME type.
18
+
19
+ This function uses the mimetypes module to guess the MIME type of a file
20
+ based on its filename, then checks if the MIME type starts with "text/".
21
+
22
+ Args:
23
+ filename (str): The name of the file to check
24
+
25
+ Returns:
26
+ bool: True if the file has a text MIME type, False otherwise
27
+ """
28
+ mime, _ = mimetypes.guess_type(filename)
29
+ return mime is not None and mime.startswith("text/")
30
+
31
+
32
+ def generate_uuid():
33
+ """
34
+ Generate a unique identifier (UUID).
35
+
36
+ This function creates a UUID using the uuid4() function from the uuid module.
37
+ The UUID is prefixed with 'a' to ensure compatibility for ID usage in various
38
+ contexts.
39
+
40
+ Returns:
41
+ str: A unique identifier string prefixed with 'a'
42
+ """
43
+ return f"a{uuid.uuid4()}"
44
+
45
+
46
+ def is_text_file(file_name):
47
+ """
48
+ Check if the given file is a text file based on its extension or MIME type.
49
+
50
+ This function determines if a file is a text file by checking if its extension
51
+ is in the predefined TEXT_EXTENSIONS list or if its MIME type indicates it's
52
+ a text file.
53
+
54
+ Args:
55
+ file_name (str): The name of the file to check
56
+
57
+ Returns:
58
+ bool: True if the file is a text file, False otherwise
59
+ """
60
+ ext = Path(file_name).suffix.lower()
61
+ if ext in TEXT_EXTENSIONS:
62
+ return True
63
+ if is_text_mime(file_name):
64
+ return True
65
+ return False
66
+
67
+
68
+ def custom_sort_key(s: str | Path):
69
+ """
70
+ Converts a string by replacing '.' with a character '{' (ASCII 123)
71
+ to ensure that strings are sorted in a specific order where '.'
72
+ is considered after all letters in ASCII comparison.
73
+
74
+ Args:
75
+ s (str|Path): The string to be transformed for custom sorting.
76
+
77
+ Returns:
78
+ str: A string transformed to facilitate the desired sorting order.
79
+ """
80
+ return str(s).replace(".", "{")
81
+
82
+
83
+ class Singleton(type):
84
+ """
85
+ Metaclass that implements the singleton pattern, ensuring only one instance of a class exists.
86
+ """
87
+
88
+ _instances = {}
89
+
90
+ def __call__(cls, *args, **kwargs):
91
+ """
92
+ Create or return the singleton instance of the class.
93
+
94
+ If an instance does not exist, instantiate the class with the given arguments.
95
+ Otherwise, return the existing instance.
96
+
97
+ Args:
98
+ *args: Positional arguments for class instantiation.
99
+ **kwargs: Keyword arguments for class instantiation.
100
+
101
+ Returns:
102
+ object: The singleton instance of the class.
103
+ """
104
+ if cls not in cls._instances:
105
+ cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
106
+ return cls._instances[cls]
@@ -0,0 +1,12 @@
1
+ """
2
+ Presentation layer for the Sourcerer application.
3
+
4
+ This package contains the user interface components and controllers that
5
+ handle user interactions. It uses the Textual library to create a terminal-based
6
+ user interface for interacting with cloud storage providers.
7
+
8
+ The presentation layer is organized into several subpackages:
9
+ - screens: Different screens and views of the application
10
+ - themes: Visual themes and styling
11
+ - utils: Presentation-specific utilities
12
+ """
@@ -0,0 +1,36 @@
1
+ """Main application module for the Sourcerer application.
2
+
3
+ This module initializes the dependency injection container, sets up the database,
4
+ and runs the main application window.
5
+ """
6
+
7
+ from sourcerer.infrastructure.access_credentials.registry import (
8
+ access_credential_method_registry,
9
+ )
10
+ from sourcerer.presentation.di_container import DiContainer
11
+ from sourcerer.presentation.screens.main.main import Sourcerer
12
+
13
+
14
+ def main():
15
+ """Initialize and run the Sourcerer application.
16
+
17
+ This function:
18
+ 1. Creates and configures the dependency injection container
19
+ 2. Sets up the access credential method registry
20
+ 3. Prepares the database
21
+ 4. Creates and runs the main application window
22
+ """
23
+ di_container = DiContainer()
24
+ di_container.config.access_credential_method_registry.from_value(
25
+ access_credential_method_registry
26
+ )
27
+ di_container.wire(packages=["sourcerer"])
28
+
29
+ DiContainer.db().prepare_db()
30
+
31
+ app = Sourcerer()
32
+ app.run()
33
+
34
+
35
+ if __name__ == "__main__":
36
+ main()