data-sourcerer 0.4.0__tar.gz → 0.5.0__tar.gz

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 (136) hide show
  1. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/PKG-INFO +9 -8
  2. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/README.md +7 -7
  3. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/pyproject.toml +5 -1
  4. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/__init__.py +3 -1
  5. data_sourcerer-0.5.0/sourcerer/domain/package_meta/entities.py +9 -0
  6. data_sourcerer-0.5.0/sourcerer/domain/package_meta/services.py +9 -0
  7. data_sourcerer-0.5.0/sourcerer/domain/settings/entities.py +11 -0
  8. data_sourcerer-0.5.0/sourcerer/domain/settings/repositories.py +20 -0
  9. data_sourcerer-0.5.0/sourcerer/domain/settings/services.py +19 -0
  10. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/db/models.py +23 -0
  11. data_sourcerer-0.5.0/sourcerer/infrastructure/package_meta/services.py +26 -0
  12. data_sourcerer-0.5.0/sourcerer/infrastructure/settings/repositories.py +59 -0
  13. data_sourcerer-0.5.0/sourcerer/infrastructure/settings/services.py +16 -0
  14. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/storage_provider/services/gcp.py +0 -1
  15. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/utils.py +1 -0
  16. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/di_container.py +13 -0
  17. data_sourcerer-0.5.0/sourcerer/presentation/screens/about/main.py +60 -0
  18. data_sourcerer-0.5.0/sourcerer/presentation/screens/about/styles.tcss +32 -0
  19. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/main/main.py +89 -6
  20. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/main/styles.tcss +13 -4
  21. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +102 -18
  22. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/provider_creds_list/main.py +14 -4
  23. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/provider_creds_list/styles.tcss +9 -0
  24. data_sourcerer-0.5.0/sourcerer/presentation/screens/settings/main.py +70 -0
  25. data_sourcerer-0.5.0/sourcerer/presentation/screens/settings/styles.tcss +44 -0
  26. data_sourcerer-0.5.0/sourcerer/presentation/screens/shared/__init__.py +0 -0
  27. data_sourcerer-0.5.0/sourcerer/presentation/screens/shared/widgets/__init__.py +0 -0
  28. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/shared/widgets/button.py +11 -0
  29. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/shared/widgets/labeled_input.py +1 -3
  30. data_sourcerer-0.5.0/sourcerer/presentation/screens/storage_action_progress/__init__.py +0 -0
  31. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/storage_action_progress/main.py +1 -2
  32. data_sourcerer-0.5.0/sourcerer/presentation/screens/storages_list/__init__.py +0 -0
  33. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/storages_list/main.py +15 -4
  34. data_sourcerer-0.5.0/sourcerer/presentation/screens/storages_list/messages/__init__.py +0 -0
  35. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/storages_list/styles.tcss +7 -0
  36. data_sourcerer-0.5.0/sourcerer/presentation/screens/storages_registration/__init__.py +0 -0
  37. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/settings.py +1 -0
  38. data_sourcerer-0.5.0/sourcerer/presentation/themes/__init__.py +0 -0
  39. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/utils.py +1 -0
  40. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/utils.py +19 -1
  41. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/.gitignore +0 -0
  42. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/LICENSE +0 -0
  43. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/__init__.py +0 -0
  44. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/access_credentials/__init__.py +0 -0
  45. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/access_credentials/entities.py +0 -0
  46. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/access_credentials/exceptions.py +0 -0
  47. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/access_credentials/repositories.py +0 -0
  48. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/access_credentials/services.py +0 -0
  49. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/file_system/__init__.py +0 -0
  50. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/file_system/entities.py +0 -0
  51. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/file_system/exceptions.py +0 -0
  52. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/file_system/services.py +0 -0
  53. {data_sourcerer-0.4.0/sourcerer/domain/storage → data_sourcerer-0.5.0/sourcerer/domain/package_meta}/__init__.py +0 -0
  54. {data_sourcerer-0.4.0/sourcerer/infrastructure/storage → data_sourcerer-0.5.0/sourcerer/domain/settings}/__init__.py +0 -0
  55. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/shared/__init__.py +0 -0
  56. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/shared/entities.py +0 -0
  57. {data_sourcerer-0.4.0/sourcerer/infrastructure/storage_provider/services → data_sourcerer-0.5.0/sourcerer/domain/storage}/__init__.py +0 -0
  58. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/storage/entities.py +0 -0
  59. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/storage/repositories.py +0 -0
  60. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/storage_provider/__init__.py +0 -0
  61. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/storage_provider/entities.py +0 -0
  62. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/storage_provider/exceptions.py +0 -0
  63. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/domain/storage_provider/services.py +0 -0
  64. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/__init__.py +0 -0
  65. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/access_credentials/__init__.py +0 -0
  66. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/access_credentials/exceptions.py +0 -0
  67. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/access_credentials/registry.py +0 -0
  68. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/access_credentials/repositories.py +0 -0
  69. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/access_credentials/services.py +0 -0
  70. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/db/__init__.py +0 -0
  71. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/db/config.py +0 -0
  72. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/file_system/__init__.py +0 -0
  73. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/file_system/exceptions.py +0 -0
  74. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/file_system/services.py +0 -0
  75. {data_sourcerer-0.4.0/sourcerer/presentation/screens → data_sourcerer-0.5.0/sourcerer/infrastructure/package_meta}/__init__.py +0 -0
  76. {data_sourcerer-0.4.0/sourcerer/presentation/screens/critical_error → data_sourcerer-0.5.0/sourcerer/infrastructure/settings}/__init__.py +0 -0
  77. {data_sourcerer-0.4.0/sourcerer/presentation/screens/file_system_finder/widgets → data_sourcerer-0.5.0/sourcerer/infrastructure/storage}/__init__.py +0 -0
  78. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/storage/repositories.py +0 -0
  79. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/storage/services.py +0 -0
  80. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/storage_provider/__init__.py +0 -0
  81. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/storage_provider/exceptions.py +0 -0
  82. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/storage_provider/registry.py +0 -0
  83. {data_sourcerer-0.4.0/sourcerer/presentation/screens/main → data_sourcerer-0.5.0/sourcerer/infrastructure/storage_provider/services}/__init__.py +0 -0
  84. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/storage_provider/services/azure.py +0 -0
  85. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/infrastructure/storage_provider/services/s3.py +0 -0
  86. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/__init__.py +0 -0
  87. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/app.py +0 -0
  88. {data_sourcerer-0.4.0/sourcerer/presentation/screens/main/messages → data_sourcerer-0.5.0/sourcerer/presentation/screens}/__init__.py +0 -0
  89. {data_sourcerer-0.4.0/sourcerer/presentation/screens/main/mixins → data_sourcerer-0.5.0/sourcerer/presentation/screens/about}/__init__.py +0 -0
  90. {data_sourcerer-0.4.0/sourcerer/presentation/screens/main/widgets → data_sourcerer-0.5.0/sourcerer/presentation/screens/critical_error}/__init__.py +0 -0
  91. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/critical_error/main.py +0 -0
  92. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/critical_error/styles.tcss +0 -0
  93. {data_sourcerer-0.4.0/sourcerer/presentation/screens/preview_content → data_sourcerer-0.5.0/sourcerer/presentation/screens/file_system_finder}/__init__.py +0 -0
  94. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/file_system_finder/main.py +0 -0
  95. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/file_system_finder/styles.tcss +0 -0
  96. {data_sourcerer-0.4.0/sourcerer/presentation/screens/provider_creds_list → data_sourcerer-0.5.0/sourcerer/presentation/screens/file_system_finder/widgets}/__init__.py +0 -0
  97. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +0 -0
  98. {data_sourcerer-0.4.0/sourcerer/presentation/screens/provider_creds_list/messages → data_sourcerer-0.5.0/sourcerer/presentation/screens/main}/__init__.py +0 -0
  99. {data_sourcerer-0.4.0/sourcerer/presentation/screens/provider_creds_registration → data_sourcerer-0.5.0/sourcerer/presentation/screens/main/messages}/__init__.py +0 -0
  100. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/main/messages/delete_request.py +0 -0
  101. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/main/messages/download_request.py +0 -0
  102. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/main/messages/preview_request.py +0 -0
  103. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/main/messages/refresh_storages_list_request.py +0 -0
  104. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/main/messages/resizing_rule.py +0 -0
  105. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/main/messages/select_storage_item.py +0 -0
  106. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/main/messages/uncheck_files_request.py +0 -0
  107. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/main/messages/upload_request.py +0 -0
  108. {data_sourcerer-0.4.0/sourcerer/presentation/screens/question → data_sourcerer-0.5.0/sourcerer/presentation/screens/main/mixins}/__init__.py +0 -0
  109. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +0 -0
  110. {data_sourcerer-0.4.0/sourcerer/presentation/screens/shared → data_sourcerer-0.5.0/sourcerer/presentation/screens/main/widgets}/__init__.py +0 -0
  111. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/main/widgets/gradient.py +0 -0
  112. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/main/widgets/resizing_rule.py +0 -0
  113. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/main/widgets/storage_content.py +0 -0
  114. {data_sourcerer-0.4.0/sourcerer/presentation/screens/shared/widgets → data_sourcerer-0.5.0/sourcerer/presentation/screens/preview_content}/__init__.py +0 -0
  115. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/preview_content/main.py +0 -0
  116. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/preview_content/styles.tcss +0 -0
  117. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/preview_content/text_area_style.py +0 -0
  118. {data_sourcerer-0.4.0/sourcerer/presentation/screens/storage_action_progress → data_sourcerer-0.5.0/sourcerer/presentation/screens/provider_creds_list}/__init__.py +0 -0
  119. {data_sourcerer-0.4.0/sourcerer/presentation/screens/storages_list → data_sourcerer-0.5.0/sourcerer/presentation/screens/provider_creds_list/messages}/__init__.py +0 -0
  120. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/provider_creds_list/messages/reload_credentials_request.py +0 -0
  121. {data_sourcerer-0.4.0/sourcerer/presentation/screens/storages_list/messages → data_sourcerer-0.5.0/sourcerer/presentation/screens/provider_creds_registration}/__init__.py +0 -0
  122. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/provider_creds_registration/main.py +0 -0
  123. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/provider_creds_registration/styles.tcss +0 -0
  124. {data_sourcerer-0.4.0/sourcerer/presentation/screens/storages_registration → data_sourcerer-0.5.0/sourcerer/presentation/screens/question}/__init__.py +0 -0
  125. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/question/main.py +0 -0
  126. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/question/styles.tcss +0 -0
  127. {data_sourcerer-0.4.0/sourcerer/presentation/themes → data_sourcerer-0.5.0/sourcerer/presentation/screens/settings}/__init__.py +0 -0
  128. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/shared/containers.py +0 -0
  129. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/shared/modal_screens.py +0 -0
  130. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/shared/widgets/spinner.py +0 -0
  131. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/storage_action_progress/styles.tcss +0 -0
  132. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/storages_list/messages/reload_storages_request.py +0 -0
  133. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/storages_registration/main.py +0 -0
  134. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/screens/storages_registration/styles.tcss +0 -0
  135. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/presentation/themes/github_dark.py +0 -0
  136. {data_sourcerer-0.4.0 → data_sourcerer-0.5.0}/sourcerer/settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-sourcerer
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Sourcerer is a terminal cloud storage navigator.
5
5
  Author-email: Bohdana Kuzmenko <bohdana.kuzmenko.dev@gmail.com>
6
6
  License: MIT
@@ -16,6 +16,7 @@ Requires-Dist: dependency-injector<5.0.0,>=4.43.0
16
16
  Requires-Dist: google-cloud-storage<4.0.0,>=3.1.0
17
17
  Requires-Dist: humanize<5.0.0,>=4.12.1
18
18
  Requires-Dist: msgspec>=0.19.0
19
+ Requires-Dist: packaging>=25.0
19
20
  Requires-Dist: pyopenssl>=22.1.0; sys_platform == 'linux'
20
21
  Requires-Dist: sqlalchemy-utils<1.0.0,>=0.41.2
21
22
  Requires-Dist: sqlalchemy<3.0.0,>=2.0.38
@@ -36,8 +37,8 @@ Description-Content-Type: text/markdown
36
37
 
37
38
  # 🧙‍♂️ Sourcerer
38
39
 
39
- **Sourcerer** is a CLI-based cloud storage explorer that provides a unified interface for developers and DevOps
40
- engineers to view and manage files across multiple cloud providers like
40
+ **Sourcerer** is a CLI-based cloud storage explorer that provides a unified interface for developers and DevOps
41
+ engineers to view and manage files across multiple cloud providers like
41
42
  **GCP Storage**, **Azure Storage**, **AWS S3**, and **S3-compatible services**.
42
43
 
43
44
  > Your terminal. Your storages. Your control.
@@ -48,11 +49,11 @@ engineers to view and manage files across multiple cloud providers like
48
49
 
49
50
  ## ✨ Features
50
51
 
51
- - 🔍 Unified file browser for GCP Storage, Azure Storage, AWS S3, and S3-compatible services
52
- - 🧭 Terminal UI (TUI) built with [Textual](https://github.com/Textualize/textual)
53
- - 🗂️ Explore buckets and objects seamlessly
54
- - 🔄 Upload, download, and delete files
55
- - 🔐 Secure credential management via local **SQLite database**
52
+ - 🔍 Unified file browser for GCP Storage, Azure Storage, AWS S3, and S3-compatible services
53
+ - 🧭 Terminal UI (TUI) built with [Textual](https://github.com/Textualize/textual)
54
+ - 🗂️ Explore buckets and objects seamlessly
55
+ - 🔄 Upload, download, and delete files
56
+ - 🔐 Secure credential management via local **SQLite database**
56
57
 
57
58
  ---
58
59
 
@@ -1,7 +1,7 @@
1
1
  # 🧙‍♂️ Sourcerer
2
2
 
3
- **Sourcerer** is a CLI-based cloud storage explorer that provides a unified interface for developers and DevOps
4
- engineers to view and manage files across multiple cloud providers like
3
+ **Sourcerer** is a CLI-based cloud storage explorer that provides a unified interface for developers and DevOps
4
+ engineers to view and manage files across multiple cloud providers like
5
5
  **GCP Storage**, **Azure Storage**, **AWS S3**, and **S3-compatible services**.
6
6
 
7
7
  > Your terminal. Your storages. Your control.
@@ -12,11 +12,11 @@ engineers to view and manage files across multiple cloud providers like
12
12
 
13
13
  ## ✨ Features
14
14
 
15
- - 🔍 Unified file browser for GCP Storage, Azure Storage, AWS S3, and S3-compatible services
16
- - 🧭 Terminal UI (TUI) built with [Textual](https://github.com/Textualize/textual)
17
- - 🗂️ Explore buckets and objects seamlessly
18
- - 🔄 Upload, download, and delete files
19
- - 🔐 Secure credential management via local **SQLite database**
15
+ - 🔍 Unified file browser for GCP Storage, Azure Storage, AWS S3, and S3-compatible services
16
+ - 🧭 Terminal UI (TUI) built with [Textual](https://github.com/Textualize/textual)
17
+ - 🗂️ Explore buckets and objects seamlessly
18
+ - 🔄 Upload, download, and delete files
19
+ - 🔐 Secure credential management via local **SQLite database**
20
20
 
21
21
  ---
22
22
 
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
 
3
3
  name = "data-sourcerer"
4
- version = "0.4.0"
4
+ dynamic = ["version"]
5
5
  description = "Sourcerer is a terminal cloud storage navigator."
6
6
  requires-python = ">=3.9"
7
7
 
@@ -28,6 +28,7 @@ dependencies = [
28
28
  "azure-identity>=1.22.0,<2.0.0",
29
29
  "azure-mgmt-storage>=22.2.0,<23.0.0",
30
30
  "msgspec>=0.19.0",
31
+ "packaging>=25.0",
31
32
  ]
32
33
 
33
34
  [project.scripts]
@@ -109,6 +110,9 @@ max-complexity = 10
109
110
  include = ["src"]
110
111
  sources = ["src"]
111
112
 
113
+ [tool.hatch.version]
114
+ path = "src/sourcerer/__init__.py"
115
+
112
116
  [tool.ty.rules]
113
117
  unresolved-import = "ignore"
114
118
  invalid-assignment = "warn"
@@ -12,4 +12,6 @@ The application is structured using a clean architecture approach with:
12
12
  - Presentation layer: User interface components
13
13
  """
14
14
 
15
- __version__ = "0.4.0"
15
+ name = "sourcerer"
16
+ package_name = "data_sourcerer"
17
+ __version__ = "0.5.0"
@@ -0,0 +1,9 @@
1
+ from msgspec._core import Struct
2
+
3
+
4
+ class PackageMeta(Struct):
5
+ version: str
6
+ latest_version: str | None
7
+ has_available_update: bool
8
+ system_version: str
9
+ platform: str
@@ -0,0 +1,9 @@
1
+ from abc import ABCMeta, abstractmethod
2
+
3
+ from sourcerer.domain.package_meta.entities import PackageMeta
4
+
5
+
6
+ class BasePackageMetaService(metaclass=ABCMeta):
7
+ @abstractmethod
8
+ def get_package_meta(self) -> PackageMeta:
9
+ raise NotImplementedError
@@ -0,0 +1,11 @@
1
+ from msgspec._core import Struct
2
+
3
+
4
+ class Settings(Struct):
5
+ theme: str = "github-dark"
6
+ group_by_access_credentials: bool = False
7
+
8
+
9
+ class SettingsFields:
10
+ theme = "theme"
11
+ group_by_access_credentials = "group_by_access_credentials"
@@ -0,0 +1,20 @@
1
+ from abc import ABCMeta, abstractmethod
2
+
3
+ from sourcerer.domain.settings.entities import Settings
4
+
5
+
6
+ class BaseSettingsRepository(metaclass=ABCMeta):
7
+ @abstractmethod
8
+ def get_settings(self) -> Settings:
9
+ """Retrieve all settings as a Settings."""
10
+ raise NotImplementedError()
11
+
12
+ @abstractmethod
13
+ def get_setting(self, key: str) -> str:
14
+ """Retrieve a setting by its key."""
15
+ raise NotImplementedError()
16
+
17
+ @abstractmethod
18
+ def set_setting(self, key: str, value: str) -> None:
19
+ """Set a setting with a given key and value."""
20
+ raise NotImplementedError()
@@ -0,0 +1,19 @@
1
+ from sourcerer.domain.settings.entities import Settings
2
+ from sourcerer.domain.settings.repositories import BaseSettingsRepository
3
+
4
+
5
+ class BaseSettingsService:
6
+ def __init__(self, repository: BaseSettingsRepository):
7
+ self.repository = repository
8
+
9
+ def load_settings(self) -> Settings:
10
+ """Load settings from the settings file."""
11
+ raise NotImplementedError("This method should be implemented by subclasses.")
12
+
13
+ def get_setting(self, key: str) -> str:
14
+ """Get the value of a setting by its key."""
15
+ raise NotImplementedError("This method should be implemented by subclasses.")
16
+
17
+ def set_setting(self, key: str, value: str) -> None:
18
+ """Set the value of a setting by its key."""
19
+ raise NotImplementedError("This method should be implemented by subclasses.")
@@ -76,3 +76,26 @@ class Storage(Base):
76
76
  cascade="save-update",
77
77
  backref=backref("storages", passive_deletes=True),
78
78
  )
79
+
80
+
81
+ class Settings(Base):
82
+ """
83
+ SQLAlchemy model for storing application settings.
84
+
85
+ This model represents the settings table in the database,
86
+ storing key-value pairs for application configuration.
87
+
88
+ Attributes:
89
+ id (int): Primary key
90
+ key (str): Setting key
91
+ value (str): Setting value
92
+ created_at (datetime): Timestamp when the setting was created
93
+ updated_at (datetime): Timestamp when the setting was last updated
94
+ """
95
+
96
+ __tablename__ = "settings"
97
+ id = Column(Integer, primary_key=True)
98
+ key = Column(String, unique=True, nullable=False)
99
+ value = Column(String, nullable=False)
100
+ created_at = Column(DateTime, default=datetime.utcnow)
101
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
@@ -0,0 +1,26 @@
1
+ import platform
2
+
3
+ from packaging import version
4
+
5
+ from sourcerer import __version__, package_name
6
+ from sourcerer.domain.package_meta.entities import PackageMeta
7
+ from sourcerer.domain.package_meta.services import BasePackageMetaService
8
+ from sourcerer.utils import get_last_package_version
9
+
10
+
11
+ class PackageMetaService(BasePackageMetaService):
12
+ def get_package_meta(self) -> PackageMeta:
13
+ latest_version = get_last_package_version(package_name)
14
+ has_available_update = (
15
+ version.parse(latest_version) > version.parse(__version__)
16
+ if latest_version
17
+ else False
18
+ )
19
+
20
+ return PackageMeta(
21
+ version=__version__,
22
+ latest_version=latest_version,
23
+ has_available_update=has_available_update,
24
+ system_version=platform.release(),
25
+ platform=platform.system(),
26
+ )
@@ -0,0 +1,59 @@
1
+ from typing import get_type_hints
2
+
3
+ from sourcerer.domain.settings.entities import Settings
4
+ from sourcerer.domain.settings.repositories import BaseSettingsRepository
5
+ from sourcerer.infrastructure.db.models import Settings as DBSettings
6
+
7
+
8
+ class SQLAlchemySettingsRepository(BaseSettingsRepository):
9
+ def __init__(self, db):
10
+ """
11
+ Initialize the repository with a database session factory.
12
+
13
+ Args:
14
+ db: Database session factory
15
+ """
16
+ self.db = db
17
+
18
+ def get_settings(self) -> Settings:
19
+ """Retrieve all settings as a Settings."""
20
+
21
+ with self.db() as session:
22
+ settings = session.query(DBSettings).all()
23
+ type_hints = get_type_hints(Settings)
24
+ return Settings(
25
+ **{
26
+ setting.key: self.parse_value(
27
+ setting.key, setting.value, type_hints.get(setting.key, str)
28
+ )
29
+ for setting in settings
30
+ }
31
+ )
32
+
33
+ def get_setting(self, key: str) -> str:
34
+ """Retrieve a setting by its key."""
35
+ with self.db() as session:
36
+ setting = session.query(DBSettings).filter(DBSettings.key == key).first()
37
+ if setting is None:
38
+ raise KeyError(f"Setting '{key}' not found.")
39
+ return setting.value
40
+
41
+ def set_setting(self, key: str, value: str) -> None:
42
+ """Set a setting with a given key and value."""
43
+ with self.db() as session:
44
+ setting = session.query(DBSettings).filter(DBSettings.key == key).first()
45
+ if setting is None:
46
+ setting = DBSettings(key=key, value=value)
47
+ session.add(setting)
48
+ else:
49
+ setting.value = value
50
+ session.commit()
51
+
52
+ def parse_value(self, key: str, value: str, expected_type):
53
+ if expected_type is bool:
54
+ return value.lower() in ("true", "1", "yes", "on")
55
+ if expected_type is int:
56
+ return int(value)
57
+ if expected_type is float:
58
+ return float(value)
59
+ return value # Assume string or leave as-is
@@ -0,0 +1,16 @@
1
+ from sourcerer.domain.settings.entities import Settings
2
+ from sourcerer.domain.settings.services import BaseSettingsService
3
+
4
+
5
+ class SettingsService(BaseSettingsService):
6
+ def load_settings(self) -> Settings:
7
+ """Load settings from the settings file."""
8
+ return self.repository.get_settings()
9
+
10
+ def get_setting(self, key: str) -> str:
11
+ """Get the value of a setting by its key."""
12
+ return self.repository.get_setting(key)
13
+
14
+ def set_setting(self, key: str, value: str) -> None:
15
+ """Set the value of a setting by its key."""
16
+ self.repository.set_setting(key, value)
@@ -118,7 +118,6 @@ class GCPStorageProviderService(BaseStorageProviderService):
118
118
  ListStorageItemsError: If an error occurs while listing items
119
119
  """
120
120
  try:
121
-
122
121
  files = []
123
122
  folders = []
124
123
  if path and not path.endswith("/"):
@@ -4,6 +4,7 @@ Utility functions for the Sourcerer application.
4
4
  This module provides various utility functions used throughout the application,
5
5
  including UUID generation, MIME type detection, and file type checking.
6
6
  """
7
+
7
8
  import mimetypes
8
9
  import secrets
9
10
  import uuid
@@ -16,6 +16,9 @@ from sourcerer.infrastructure.access_credentials.repositories import (
16
16
  from sourcerer.infrastructure.access_credentials.services import CredentialsService
17
17
  from sourcerer.infrastructure.db.config import Database
18
18
  from sourcerer.infrastructure.file_system.services import FileSystemService
19
+ from sourcerer.infrastructure.package_meta.services import PackageMetaService
20
+ from sourcerer.infrastructure.settings.repositories import SQLAlchemySettingsRepository
21
+ from sourcerer.infrastructure.settings.services import SettingsService
19
22
  from sourcerer.infrastructure.storage.repositories import SQLAlchemyStoragesRepository
20
23
  from sourcerer.infrastructure.storage.services import StoragesService
21
24
  from sourcerer.settings import APP_DIR, DB_NAME
@@ -50,6 +53,10 @@ class DiContainer(containers.DeclarativeContainer):
50
53
  SQLAlchemyStoragesRepository, session_factory
51
54
  )
52
55
 
56
+ settings_repository = providers.Factory(
57
+ SQLAlchemySettingsRepository, session_factory
58
+ )
59
+
53
60
  credentials_service = providers.Factory(
54
61
  CredentialsService, repository=credentials_repository
55
62
  )
@@ -59,3 +66,9 @@ class DiContainer(containers.DeclarativeContainer):
59
66
  )
60
67
 
61
68
  file_system_service = providers.Factory(FileSystemService, Path.home())
69
+
70
+ package_meta_service = providers.Factory(PackageMetaService)
71
+ settings_service = providers.Factory(
72
+ SettingsService,
73
+ repository=settings_repository,
74
+ )
@@ -0,0 +1,60 @@
1
+ from dependency_injector.wiring import Provide
2
+ from rich.text import Text
3
+ from textual import on
4
+ from textual.app import ComposeResult
5
+ from textual.containers import Container, Horizontal
6
+ from textual.widgets import Static
7
+
8
+ from sourcerer.domain.package_meta.services import BasePackageMetaService
9
+ from sourcerer.presentation.di_container import DiContainer
10
+ from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
11
+ from sourcerer.presentation.screens.shared.widgets.button import Button
12
+
13
+
14
+ class AboutScreen(ExitBoundModalScreen):
15
+ """Screen with a parameter."""
16
+
17
+ CSS_PATH = "styles.tcss"
18
+
19
+ def __init__(
20
+ self,
21
+ package_meta_service: BasePackageMetaService = Provide[
22
+ DiContainer.package_meta_service
23
+ ],
24
+ ) -> None:
25
+ super().__init__()
26
+ self.package_meta_service = package_meta_service
27
+
28
+ def compose(self) -> ComposeResult:
29
+ package_meta = self.package_meta_service.get_package_meta()
30
+
31
+ with Container():
32
+ yield Static(Text("Sourcerer", style="bold cyan", justify="center"))
33
+ yield Static(
34
+ Text(
35
+ f"Version: {package_meta.version}"
36
+ f"{' (newer version is available)' if package_meta.has_available_update else ''}",
37
+ style="dim",
38
+ justify="center",
39
+ )
40
+ )
41
+ yield Static(
42
+ Text(
43
+ f"Platform: {package_meta.platform}", style="dim", justify="center"
44
+ )
45
+ )
46
+ yield Static(
47
+ Text(
48
+ f"System Version: {package_meta.system_version}",
49
+ style="dim",
50
+ justify="center",
51
+ )
52
+ )
53
+ with Horizontal(id="controls"):
54
+ yield Button("Close", name="close")
55
+
56
+ @on(Button.Click)
57
+ def on_button_clicked(self, event: Button.Click) -> None:
58
+ """Handle button clicked events."""
59
+ if event.action == "close":
60
+ self.action_cancel_screen()
@@ -0,0 +1,32 @@
1
+ AboutScreen {
2
+ align: center middle;
3
+ content-align: center top;
4
+
5
+ & > Container {
6
+ padding: 1 2 0 2;
7
+ margin: 0 0;
8
+ width: 50;
9
+ height: 10;
10
+ border: solid $border;
11
+
12
+ & > Static {
13
+ text-align: center;
14
+ text-wrap: wrap;
15
+ }
16
+
17
+ & > Horizontal#controls {
18
+ padding-top: 1;
19
+ align: center bottom;
20
+
21
+ & > Button {
22
+ color: $border;
23
+ border: none;
24
+
25
+ & > :focus {
26
+ color: $border;
27
+ }
28
+ }
29
+ }
30
+ }
31
+
32
+ }
@@ -1,26 +1,32 @@
1
1
  import contextlib
2
2
  import time
3
3
  import traceback
4
+ from collections.abc import Iterable
4
5
  from concurrent.futures import ThreadPoolExecutor
5
6
  from pathlib import Path
6
7
  from typing import ClassVar
7
8
 
8
9
  from dependency_injector.wiring import Provide
9
10
  from textual import on, work
10
- from textual.app import App, ComposeResult
11
+ from textual.app import App, ComposeResult, SystemCommand
11
12
  from textual.binding import Binding, BindingType
12
13
  from textual.containers import Horizontal
13
14
  from textual.css.query import NoMatches
14
15
  from textual.reactive import reactive
16
+ from textual.screen import Screen
15
17
  from textual.widgets import Footer
16
18
 
19
+ from sourcerer.domain.package_meta.services import BasePackageMetaService
20
+ from sourcerer.domain.settings.entities import SettingsFields
17
21
  from sourcerer.domain.storage_provider.entities import Storage
18
22
  from sourcerer.infrastructure.access_credentials.services import CredentialsService
23
+ from sourcerer.infrastructure.settings.services import SettingsService
19
24
  from sourcerer.infrastructure.storage_provider.exceptions import (
20
25
  ListStorageItemsError,
21
26
  )
22
27
  from sourcerer.infrastructure.utils import generate_uuid
23
28
  from sourcerer.presentation.di_container import DiContainer
29
+ from sourcerer.presentation.screens.about.main import AboutScreen
24
30
  from sourcerer.presentation.screens.critical_error.main import CriticalErrorScreen
25
31
  from sourcerer.presentation.screens.file_system_finder.main import (
26
32
  FileSystemNavigationModal,
@@ -55,6 +61,7 @@ from sourcerer.presentation.screens.preview_content.main import PreviewContentSc
55
61
  from sourcerer.presentation.screens.provider_creds_list.main import (
56
62
  ProviderCredsListScreen,
57
63
  )
64
+ from sourcerer.presentation.screens.settings.main import SettingsScreen
58
65
  from sourcerer.presentation.screens.storage_action_progress.main import (
59
66
  DeleteKey,
60
67
  DownloadKey,
@@ -103,8 +110,10 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
103
110
  CSS_PATH = "styles.tcss"
104
111
  BINDINGS: ClassVar[list[BindingType]] = [
105
112
  Binding("ctrl+r", "registrations", "Registrations list"),
106
- Binding("ctrl+s", "storages", "Storages list"),
113
+ Binding("ctrl+l", "storages", "Storages list"),
107
114
  Binding("ctrl+f", "find", show=False),
115
+ Binding("ctrl+s", "settings", "Settings"),
116
+ Binding("ctrl+a", "about", "About"),
108
117
  Binding(
109
118
  KeyBindings.ARROW_LEFT.value, "focus_sidebar", "Focus sidebar", show=False
110
119
  ),
@@ -116,15 +125,24 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
116
125
 
117
126
  def __init__(
118
127
  self,
128
+ settings_service: SettingsService = Provide[DiContainer.settings_service],
119
129
  credentials_service: CredentialsService = Provide[
120
130
  DiContainer.credentials_service
121
131
  ],
132
+ package_meta_service: BasePackageMetaService = Provide[
133
+ DiContainer.package_meta_service
134
+ ],
122
135
  *args,
123
136
  **kwargs,
124
137
  ):
125
138
  super().__init__(*args, **kwargs)
139
+ self.settings_service = settings_service
126
140
  self.credentials_service = credentials_service
127
- self.storage_list_sidebar = StorageListSidebar(id="storage_list_sidebar")
141
+ self.package_meta_service = package_meta_service
142
+ self.settings = self.settings_service.load_settings()
143
+ self.storage_list_sidebar = StorageListSidebar(
144
+ self.settings.group_by_access_credentials, id="storage_list_sidebar"
145
+ )
128
146
  self.storage_content = StorageContentContainer(id="storage_content_container")
129
147
  self.load_percentage = 0
130
148
  self.active_resizing_rule: ResizingRule | None = None
@@ -145,14 +163,49 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
145
163
  def _handle_exception(self, error: Exception) -> None:
146
164
  self.push_screen(CriticalErrorScreen(str(error), traceback.format_exc()))
147
165
 
166
+ def get_system_commands(self, screen: Screen) -> Iterable[SystemCommand]:
167
+ yield SystemCommand(
168
+ "Quit the application",
169
+ "Quit the application as soon as possible",
170
+ self.action_quit,
171
+ )
172
+
173
+ if screen.query("HelpPanel"):
174
+ yield SystemCommand(
175
+ "Hide keys and help panel",
176
+ "Hide the keys and widget help panel",
177
+ self.action_hide_help_panel,
178
+ )
179
+ else:
180
+ yield SystemCommand(
181
+ "Show keys and help panel",
182
+ "Show help for the focused widget and a summary of available keys",
183
+ self.action_show_help_panel,
184
+ )
185
+ yield SystemCommand(
186
+ "Save screenshot",
187
+ "Save an SVG 'screenshot' of the current screen",
188
+ self.deliver_screenshot,
189
+ )
190
+ yield SystemCommand("About", "About sourcerer", self.action_about)
191
+ yield SystemCommand("Settings", "Sourcerer settings", self.action_settings)
192
+
148
193
  def on_mount(self):
149
194
  """
150
195
  Initializes the application theme and storage list on mount.
151
196
  """
152
197
 
153
198
  self.register_theme(github_dark_theme) # pyright: ignore [reportArgumentType]
154
-
155
- self.theme = "github-dark"
199
+ if self.settings.theme in self._registered_themes:
200
+ self.theme = self.settings.theme
201
+
202
+ package_meta = self.package_meta_service.get_package_meta()
203
+ if package_meta.has_available_update:
204
+ self.notify(
205
+ f"Sourcerer {package_meta.version} "
206
+ f"is running while {package_meta.latest_version} is available",
207
+ severity="warning",
208
+ )
156
209
  self.init_storages_list()
157
210
 
158
211
  def action_find(self):
@@ -188,9 +241,39 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
188
241
  ProviderCredsListScreen(), callback=self.modal_screen_callback
189
242
  )
190
243
 
244
+ def action_settings(self):
245
+ """
246
+ Opens the settings screen.
247
+ This method is triggered by the key binding "ctrl+s" and allows the user
248
+ to modify application settings such as theme and grouping of storage items.
249
+ It retrieves the current settings from the settings service and pushes
250
+ the SettingsScreen to the application stack. A callback is set to handle
251
+ the settings changes when the screen is closed.
252
+ """
253
+ settings = self.settings_service.load_settings()
254
+ self.app.push_screen(SettingsScreen(settings), callback=self.settings_callback)
255
+
191
256
  def action_storages(self):
192
257
  self.app.push_screen(StoragesListScreen(), callback=self.modal_screen_callback)
193
258
 
259
+ def action_about(self):
260
+ self.push_screen(AboutScreen())
261
+
262
+ def settings_callback(self, settings: dict | None):
263
+ default_settings = self.settings_service.load_settings()
264
+ if settings is None:
265
+ self.app.theme = default_settings.theme
266
+ return
267
+ self.app.theme = settings[SettingsFields.theme]
268
+ if (theme := settings.get(SettingsFields.theme)) != default_settings.theme:
269
+ self.settings_service.set_setting(SettingsFields.theme, theme) # type: ignore
270
+
271
+ if (
272
+ group_by := settings.get(SettingsFields.group_by_access_credentials)
273
+ ) != default_settings.group_by_access_credentials:
274
+ self.settings_service.set_setting(SettingsFields.group_by_access_credentials, group_by) # type: ignore
275
+ self.storage_list_sidebar.groupby_access_credentials = group_by # type: ignore
276
+
194
277
  def modal_screen_callback(self, requires_storage_refresh: bool | None = True):
195
278
  """
196
279
  Callback for modal screens to refresh the storage list if required.
@@ -587,7 +670,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
587
670
  for storage in credentials.storages
588
671
  if storage.name not in storage_names
589
672
  ]
590
- self.storage_list_sidebar.storages[credentials.uuid] = (
673
+ self.storage_list_sidebar.storages[(credentials.uuid, credentials.name)] = (
591
674
  storages + registered_storages
592
675
  )
593
676
  self.storage_list_sidebar.last_update_timestamp = time.time()
@@ -12,9 +12,18 @@ StorageContent {
12
12
  align-horizontal: center;
13
13
 
14
14
  & > Button {
15
+ color: $primary;
15
16
  border: solid $primary;
16
- width: 10;
17
- content-align-horizontal: center;
17
+ padding-top: 0;
18
+ width: 15;
19
+ text-align: center;
20
+
21
+ &:hover {
22
+ color: $text;
23
+ }
24
+ &:focus {
25
+ color: $text;
26
+ }
18
27
  }
19
28
  }
20
29
 
@@ -25,8 +34,8 @@ StorageContent {
25
34
  padding: 1 1 0 1;
26
35
  }
27
36
 
28
- Rule {
37
+ ResizingRule {
29
38
  color: $background-lighten-3;
30
39
  padding: 0 0;
31
40
  margin: 0 0;
32
- }
41
+ }