data-sourcerer 0.3.0__py3-none-any.whl → 0.4.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 (23) hide show
  1. {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.4.0.dist-info}/METADATA +3 -1
  2. {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.4.0.dist-info}/RECORD +23 -21
  3. sourcerer/__init__.py +1 -1
  4. sourcerer/domain/storage_provider/entities.py +1 -1
  5. sourcerer/infrastructure/storage_provider/services/azure.py +1 -3
  6. sourcerer/infrastructure/storage_provider/services/gcp.py +1 -2
  7. sourcerer/infrastructure/storage_provider/services/s3.py +1 -2
  8. sourcerer/presentation/screens/file_system_finder/main.py +3 -9
  9. sourcerer/presentation/screens/main/main.py +27 -2
  10. sourcerer/presentation/screens/main/messages/preview_request.py +1 -0
  11. sourcerer/presentation/screens/main/widgets/storage_content.py +10 -3
  12. sourcerer/presentation/screens/preview_content/main.py +202 -15
  13. sourcerer/presentation/screens/preview_content/styles.tcss +39 -4
  14. sourcerer/presentation/screens/preview_content/text_area_style.py +60 -0
  15. sourcerer/presentation/screens/provider_creds_list/main.py +9 -5
  16. sourcerer/presentation/screens/provider_creds_registration/main.py +3 -3
  17. sourcerer/presentation/screens/shared/modal_screens.py +37 -0
  18. sourcerer/presentation/screens/storages_list/main.py +9 -5
  19. sourcerer/presentation/screens/storages_registration/main.py +3 -3
  20. sourcerer/settings.py +2 -0
  21. {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.4.0.dist-info}/WHEEL +0 -0
  22. {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.4.0.dist-info}/entry_points.txt +0 -0
  23. {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-sourcerer
3
- Version: 0.3.0
3
+ Version: 0.4.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
@@ -42,6 +42,8 @@ engineers to view and manage files across multiple cloud providers like
42
42
 
43
43
  > Your terminal. Your storages. Your control.
44
44
 
45
+ [Demo page](https://the-impact-craft.github.io/sourcerer/)
46
+
45
47
  ---
46
48
 
47
49
  ## ✨ Features
@@ -1,5 +1,5 @@
1
- sourcerer/__init__.py,sha256=3SFTxE-wK6g4MigwpFa7z88woGrTeoJTPs8F1qg8bII,585
2
- sourcerer/settings.py,sha256=dwY2GoaWD1CsAoVtWzaAhP7_e3KUugmBr2E624JSedc,1388
1
+ sourcerer/__init__.py,sha256=QELNZ0_i0crBFBzWe24aLrXu6z8WyDxq3mevvARdn2Q,585
2
+ sourcerer/settings.py,sha256=jIUcq9-_yYPxLS_8m1iKlkGCOg_pbhIjHXkR1LQmHQI,1463
3
3
  sourcerer/utils.py,sha256=4jAlcofepAQMcD1cYDsC1ryGwBLxE9m7ckPS6CzDsCI,879
4
4
  sourcerer/domain/__init__.py,sha256=rV21d-dD-e0q4EQ2LfWDSDLlrUOjnHnWBtWPujoue0o,556
5
5
  sourcerer/domain/access_credentials/__init__.py,sha256=pFAwnr74uy09e7kubYsuaqzkVPkTA66dwjKzpIGQkAY,217
@@ -17,7 +17,7 @@ sourcerer/domain/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
17
17
  sourcerer/domain/storage/entities.py,sha256=bSWVFx4Hh7_RLCHVLN_U2Wg6XxeYUyzDGLUpbFuWTLo,687
18
18
  sourcerer/domain/storage/repositories.py,sha256=v7RU8_X_HgFT3wn7Yc5m07aN7DEbsBMh8sRZSRSCTxU,1003
19
19
  sourcerer/domain/storage_provider/__init__.py,sha256=P3RUH9LFkWez3ehCczgVbnbp0tZZepPeOQf9spsC8FQ,192
20
- sourcerer/domain/storage_provider/entities.py,sha256=EgHkVFizMzzpzN161DlmWKPV9QhoRo8X7iiLtcN3_Wk,1931
20
+ sourcerer/domain/storage_provider/entities.py,sha256=koaY4M4nWeie9kXJrHLJ85fEvaLxOQJEq8Zl3vmGwMM,1931
21
21
  sourcerer/domain/storage_provider/exceptions.py,sha256=6xK5r62Bhedx3vV0_i7Eu5ZG5IExxeiuaGHG5sX17i4,508
22
22
  sourcerer/domain/storage_provider/services.py,sha256=Zm6nrKqQJW-9ZaqTp9wQaQuwMeH6hkLJZCkhQ_sTRF0,3997
23
23
  sourcerer/infrastructure/__init__.py,sha256=HQoqA8S9Vx2dr1Eua86wu_YxwXyY6jqa4IfEoZJcXcQ,616
@@ -40,9 +40,9 @@ sourcerer/infrastructure/storage_provider/__init__.py,sha256=GONjDCsTmd6f_fF3lzx
40
40
  sourcerer/infrastructure/storage_provider/exceptions.py,sha256=acx3IIXD2yWlzLvD2asJBEpKEa6eJW31uKkzzr8MrR4,3336
41
41
  sourcerer/infrastructure/storage_provider/registry.py,sha256=8dbRLOx1jLK_i18uuh_JnKvId9NJBECKg4nG9F_dFH4,2249
42
42
  sourcerer/infrastructure/storage_provider/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
- sourcerer/infrastructure/storage_provider/services/azure.py,sha256=A7k8Zs6rK5nBsUr5ZNBzkDQWRxpU0qnfWAcrz3eZzAk,9796
44
- sourcerer/infrastructure/storage_provider/services/gcp.py,sha256=nohx-WE5STYRkeZd4nk8blBZxmWSWSEz5bdFOXN1QYc,9097
45
- sourcerer/infrastructure/storage_provider/services/s3.py,sha256=lSpf5SPyTZGl19ChgYBtf3IR6uUWULuAvLBEdD-oarM,9277
43
+ sourcerer/infrastructure/storage_provider/services/azure.py,sha256=ZCglIQ0DQX1mYimsT4rS1tUvZtLbvpR_NZZzjGw7sB4,9757
44
+ sourcerer/infrastructure/storage_provider/services/gcp.py,sha256=vGqD7rrDZ4fepxIiY1owI4YTSCZWbRMmyrsuEssR24U,9059
45
+ sourcerer/infrastructure/storage_provider/services/s3.py,sha256=avRAtgZTobKnovfCG_lnW9xZOG77cnpDnZGazn85XHc,9239
46
46
  sourcerer/presentation/__init__.py,sha256=kzOeaTpy9hm61MLl_nybdooRrawFUd1uEX4f3Y-84ZU,472
47
47
  sourcerer/presentation/app.py,sha256=ROu3vSWzo6d8W30A9Zqi5zdLcVeHJsGLDJMLTKrthHE,1018
48
48
  sourcerer/presentation/di_container.py,sha256=KTDSWEtR_YTqJTOsyhwBBHqB9gqIkX-nu5a8Ks9UlDc,2215
@@ -52,17 +52,17 @@ sourcerer/presentation/screens/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
52
52
  sourcerer/presentation/screens/critical_error/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
53
  sourcerer/presentation/screens/critical_error/main.py,sha256=10Ip1InBzktwwM2ijKXBOkhvBXGorw1X8EGxdh75WZE,2208
54
54
  sourcerer/presentation/screens/critical_error/styles.tcss,sha256=mURvbf0_npkRtzVBs2bVBybbyNK9cO_6Ar2Muk1Mpv8,604
55
- sourcerer/presentation/screens/file_system_finder/main.py,sha256=AC_G6VYMTO0NOYmGmOZ3be5isTGFjebR0IHFHU-yw3o,10343
55
+ sourcerer/presentation/screens/file_system_finder/main.py,sha256=8V603I74iKoyMMChbl8XjpQAu614qpW0sbRlkCAxSjg,10221
56
56
  sourcerer/presentation/screens/file_system_finder/styles.tcss,sha256=fZkdwXFsDkjXkaIskLxoQ_YHsLWKjgrn6hYseugg_68,718
57
57
  sourcerer/presentation/screens/file_system_finder/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
58
  sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py,sha256=giQXiNEsSAmBV-YWc1eXcJDFUWxpCppv0oNxgvrLZxg,31070
59
59
  sourcerer/presentation/screens/main/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
- sourcerer/presentation/screens/main/main.py,sha256=vIsrhj0rb1OR0ITASDvIl-yf1_7n8oNi494s7QRRBz8,23127
60
+ sourcerer/presentation/screens/main/main.py,sha256=4eCKA7ZCy7DPfpK-uFnwSdY7HIELQSHr2TrmGdPX0u4,24038
61
61
  sourcerer/presentation/screens/main/styles.tcss,sha256=Ruv2vBKzM8njH7OS2TCpZqCmRVEp7XQLeBN4XhVB5AU,381
62
62
  sourcerer/presentation/screens/main/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
63
  sourcerer/presentation/screens/main/messages/delete_request.py,sha256=puaU1UtbDErfYi8ViPEawhSbycnUpwdE81zZfzlslNE,203
64
64
  sourcerer/presentation/screens/main/messages/download_request.py,sha256=3urSTrvNbod1FrXfu-C1UDZqOu5D0OC-NJsKJhhprXE,205
65
- sourcerer/presentation/screens/main/messages/preview_request.py,sha256=px8N3E2VEexKmpZESb9nYDQvhXUThPxM0dU41B533tM,184
65
+ sourcerer/presentation/screens/main/messages/preview_request.py,sha256=-UVWFVr4vsB0LGj97MTT8pLbJ3eNJClMWvT6vQ4lqw8,198
66
66
  sourcerer/presentation/screens/main/messages/refresh_storages_list_request.py,sha256=NxOBcUf5oKF1cMCcHZny8qG2Jv6zTdPmYeFV9qORHh8,136
67
67
  sourcerer/presentation/screens/main/messages/resizing_rule.py,sha256=ws7lzS08h6qqeihF66XV5FsX26YkjQOje_4vgCw2mqI,332
68
68
  sourcerer/presentation/screens/main/messages/select_storage_item.py,sha256=iZOwzOwFLhsr58WV01-NtTFr4LXKykz1i76nayLkCLw,269
@@ -73,24 +73,26 @@ sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py,sh
73
73
  sourcerer/presentation/screens/main/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
74
  sourcerer/presentation/screens/main/widgets/gradient.py,sha256=Ow6oaX0tsFNbUKRh_e9ITph20_tKoQgqzM7rujwKmxs,1263
75
75
  sourcerer/presentation/screens/main/widgets/resizing_rule.py,sha256=W4tAbSZlmAIytP1BNlSvBeiMGA7cD6l13IpxG2JYzxk,2088
76
- sourcerer/presentation/screens/main/widgets/storage_content.py,sha256=TnPG4uN9-7oZj4JbZC_Xj60BGj6jbm3IxygK-TsVpQg,26097
76
+ sourcerer/presentation/screens/main/widgets/storage_content.py,sha256=7ngNzPSagFi2GzzYNzFQqq6DCppLil2taghJdkSube0,26278
77
77
  sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py,sha256=83R7HximPBPIj-3J9oWnISv10RcCabTo5EVWmhO5lSk,7748
78
78
  sourcerer/presentation/screens/preview_content/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
- sourcerer/presentation/screens/preview_content/main.py,sha256=QX3eyoLkgMhRLneUz3rIHkzFGmxHqD1vvok9LcxU8cE,2785
80
- sourcerer/presentation/screens/preview_content/styles.tcss,sha256=Qk0nZtUfp-mCFaUXtkjwimAnshXnhEgh719aT4_lDPQ,374
79
+ sourcerer/presentation/screens/preview_content/main.py,sha256=UejOzmE6cl6HKTH_N0-spoByyTJRxZwI8-0nm3S8hi0,9111
80
+ sourcerer/presentation/screens/preview_content/styles.tcss,sha256=ESFpZgwZlehkrbCVhA45ODiXTpFuwqQPvQypsSjGxpQ,887
81
+ sourcerer/presentation/screens/preview_content/text_area_style.py,sha256=AOX8CG9A77gQIhF8GHTW45clPWrQcRKqu0KJcUzXrFU,2699
81
82
  sourcerer/presentation/screens/provider_creds_list/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
- sourcerer/presentation/screens/provider_creds_list/main.py,sha256=ybnaT6GROnNfIcwhfa5zrtEL4JaP80qRiL4YjqMocyk,8595
83
+ sourcerer/presentation/screens/provider_creds_list/main.py,sha256=96BCZUi7ZLhZSCGeg8KaSeMdaQbh_BxwEZ1lW-9ZUp4,8822
83
84
  sourcerer/presentation/screens/provider_creds_list/styles.tcss,sha256=_BXTWQw_LjPjbAG-V4YxxOeHn4GFShVjc4K1by-uVR0,956
84
85
  sourcerer/presentation/screens/provider_creds_list/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
85
86
  sourcerer/presentation/screens/provider_creds_list/messages/reload_credentials_request.py,sha256=zkyLFkXZHQMj5OsDU_IiInUnezHNWyvYn8yZLoJmJn0,134
86
87
  sourcerer/presentation/screens/provider_creds_registration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
- sourcerer/presentation/screens/provider_creds_registration/main.py,sha256=sIkdS1cvjzT4-0Y-z1R2bLiNx8chinGQvr5v9VWcoBg,11354
88
+ sourcerer/presentation/screens/provider_creds_registration/main.py,sha256=CkGFef_JdMvcH0ve-XPGA_I8ZRTdzJNurvjUjZ0ZjLo,11422
88
89
  sourcerer/presentation/screens/provider_creds_registration/styles.tcss,sha256=gd1SNeRoHTYwNzdGxK-2aDqNPeY5b2wFWajtoNn5--Y,612
89
90
  sourcerer/presentation/screens/question/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
90
91
  sourcerer/presentation/screens/question/main.py,sha256=7ogFbKrqP9deBYfgq7Bs6NkiSdy3whEu51mW_--TCIY,980
91
92
  sourcerer/presentation/screens/question/styles.tcss,sha256=NT8Ty4opqYLG1-sal3m1us4M4LNhtTcQywlU-Al-v7o,396
92
93
  sourcerer/presentation/screens/shared/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
93
94
  sourcerer/presentation/screens/shared/containers.py,sha256=9Tkl5SbPNgycZlfp5Pq50pHnJqbP0EckmxayXPPuhFs,378
95
+ sourcerer/presentation/screens/shared/modal_screens.py,sha256=ycpqEy9M3Dh_q-EO44_YbIyDAb9lkF8ZCUUje5fh42c,1128
94
96
  sourcerer/presentation/screens/shared/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
95
97
  sourcerer/presentation/screens/shared/widgets/button.py,sha256=aGsJTwK04nTXarafkl8TgdeB9EGfRwn4gffoPvFFwiU,1580
96
98
  sourcerer/presentation/screens/shared/widgets/labeled_input.py,sha256=OmaZjRJSg8LABvBvbN3LQ0s9M3-jp9X2i3yCVNbRhEk,2798
@@ -99,17 +101,17 @@ sourcerer/presentation/screens/storage_action_progress/__init__.py,sha256=47DEQp
99
101
  sourcerer/presentation/screens/storage_action_progress/main.py,sha256=0nNSxKwZv-ly5uA1cpdZf6x-UO8xoF0poHwc9VcGMj4,17899
100
102
  sourcerer/presentation/screens/storage_action_progress/styles.tcss,sha256=ffvDxRWhckE5tjEG4jwlhQNqeQsYpdF71104StWCGWU,818
101
103
  sourcerer/presentation/screens/storages_list/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
102
- sourcerer/presentation/screens/storages_list/main.py,sha256=trlNfWIe3VE318ksy803TC1AH9duYN3ZEfzOAv1a9eM,6254
104
+ sourcerer/presentation/screens/storages_list/main.py,sha256=tzgCH-nJODW4wX1TO8Z7Zj5EwFXl5IX5JjEjxy6_Ky0,6481
103
105
  sourcerer/presentation/screens/storages_list/styles.tcss,sha256=BlvlL5M-WGtMZuyqxT37gGlAIi1AFfILMaLiL-ZwvIc,766
104
106
  sourcerer/presentation/screens/storages_list/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
105
107
  sourcerer/presentation/screens/storages_list/messages/reload_storages_request.py,sha256=PtwlWgPUP_ZcAid535fd4Py6AvKUVhaXbc0WMka4QD0,131
106
108
  sourcerer/presentation/screens/storages_registration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
107
- sourcerer/presentation/screens/storages_registration/main.py,sha256=_MlyeTbI-zoNWegGY6jnZPR9aoeTeeXDpjXQL8hEFIM,3734
109
+ sourcerer/presentation/screens/storages_registration/main.py,sha256=xam1qrUgmh7kA1y7EEbUAO1znZRpd1FQ7igN7Q_4j50,3802
108
110
  sourcerer/presentation/screens/storages_registration/styles.tcss,sha256=Yd78pkiaaShb30r5s6qlYlLxyB62DJNeAQqs_gMcP6k,601
109
111
  sourcerer/presentation/themes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
110
112
  sourcerer/presentation/themes/github_dark.py,sha256=9E1mEOr701nU-ZDSKBccMl3GYchroCEsxEVelm5oI-E,497
111
- data_sourcerer-0.3.0.dist-info/METADATA,sha256=JWKRdTOJMClCyOZR6alR2Ew2aFZtvN0fxAYxAnM52Ew,2535
112
- data_sourcerer-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
113
- data_sourcerer-0.3.0.dist-info/entry_points.txt,sha256=CyD02GehPW6QuhR5oDY5tLLRHQ9qbPXe0v3aT1pK3N8,62
114
- data_sourcerer-0.3.0.dist-info/licenses/LICENSE,sha256=HjZ7RAG3i6izxvitGfY4feHfvW5F8DPj5eF0YBSf2rI,1073
115
- data_sourcerer-0.3.0.dist-info/RECORD,,
113
+ data_sourcerer-0.4.0.dist-info/METADATA,sha256=xanVKbREZlNIs0m1ecxFpYZS-OzCqb9S0BQ7YdE94jw,2595
114
+ data_sourcerer-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
115
+ data_sourcerer-0.4.0.dist-info/entry_points.txt,sha256=CyD02GehPW6QuhR5oDY5tLLRHQ9qbPXe0v3aT1pK3N8,62
116
+ data_sourcerer-0.4.0.dist-info/licenses/LICENSE,sha256=HjZ7RAG3i6izxvitGfY4feHfvW5F8DPj5eF0YBSf2rI,1073
117
+ data_sourcerer-0.4.0.dist-info/RECORD,,
sourcerer/__init__.py CHANGED
@@ -12,4 +12,4 @@ The application is structured using a clean architecture approach with:
12
12
  - Presentation layer: User interface components
13
13
  """
14
14
 
15
- __version__ = "0.3.0"
15
+ __version__ = "0.4.0"
@@ -63,7 +63,7 @@ class File(Struct):
63
63
 
64
64
  uuid: str
65
65
  key: str
66
- size: str
66
+ size: int
67
67
  is_text: bool
68
68
  date_modified: datetime | None = None
69
69
 
@@ -10,7 +10,6 @@ from collections.abc import Callable
10
10
  from pathlib import Path
11
11
  from typing import Any
12
12
 
13
- import humanize
14
13
  from azure.mgmt.storage import StorageManagementClient
15
14
  from azure.storage.blob import BlobServiceClient
16
15
  from platformdirs import user_downloads_dir
@@ -38,7 +37,6 @@ from sourcerer.infrastructure.utils import generate_uuid, is_text_file
38
37
 
39
38
  @storage_provider(StorageProvider.AzureStorage)
40
39
  class AzureStorageProviderService(BaseStorageProviderService):
41
-
42
40
  def __init__(self, credentials: Any):
43
41
  """
44
42
  Initialize the service with Azure credentials.
@@ -137,7 +135,7 @@ class AzureStorageProviderService(BaseStorageProviderService):
137
135
  File(
138
136
  generate_uuid(),
139
137
  remaining_path,
140
- size=humanize.naturalsize(blob.size),
138
+ size=blob.size,
141
139
  date_modified=blob.last_modified,
142
140
  is_text=is_text_file(blob.name),
143
141
  )
@@ -9,7 +9,6 @@ from collections.abc import Callable
9
9
  from pathlib import Path
10
10
  from typing import Any
11
11
 
12
- import humanize
13
12
  from platformdirs import user_downloads_dir
14
13
 
15
14
  from sourcerer.domain.shared.entities import StorageProvider
@@ -136,7 +135,7 @@ class GCPStorageProviderService(BaseStorageProviderService):
136
135
  File(
137
136
  generate_uuid(),
138
137
  blob.name[len(path) :],
139
- size=humanize.naturalsize(blob.size),
138
+ size=blob.size,
140
139
  date_modified=blob.updated.date(),
141
140
  is_text=is_text_file(blob.name),
142
141
  )
@@ -10,7 +10,6 @@ from itertools import groupby
10
10
  from pathlib import Path
11
11
  from typing import Any
12
12
 
13
- import humanize
14
13
  from platformdirs import user_downloads_dir
15
14
 
16
15
  from sourcerer.domain.shared.entities import StorageProvider
@@ -173,7 +172,7 @@ class S3ProviderService(BaseStorageProviderService):
173
172
  File(
174
173
  generate_uuid(),
175
174
  i.get("Key").replace(path, ""),
176
- humanize.naturalsize(i.get("Size")),
175
+ i.get("Size"),
177
176
  is_text_file(i.get("Key")),
178
177
  i.get("LastModified"),
179
178
  )
@@ -1,16 +1,13 @@
1
1
  from collections.abc import Callable
2
2
  from dataclasses import dataclass
3
3
  from pathlib import Path
4
- from typing import ClassVar
5
4
 
6
5
  from dependency_injector.wiring import Provide
7
6
  from textual import on
8
7
  from textual.app import ComposeResult
9
- from textual.binding import Binding, BindingType
10
8
  from textual.containers import Container, Horizontal
11
9
  from textual.css.query import NoMatches
12
10
  from textual.reactive import reactive
13
- from textual.screen import ModalScreen
14
11
  from textual.widgets import Static
15
12
 
16
13
  from sourcerer.infrastructure.file_system.services import FileSystemService
@@ -18,6 +15,7 @@ from sourcerer.presentation.di_container import DiContainer
18
15
  from sourcerer.presentation.screens.file_system_finder.widgets.file_system_navigator import (
19
16
  FileSystemNavigator,
20
17
  )
18
+ from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
21
19
  from sourcerer.presentation.screens.shared.widgets.button import Button
22
20
 
23
21
 
@@ -27,14 +25,10 @@ class FileSystemSelectionValidationRule:
27
25
  error_message: str
28
26
 
29
27
 
30
- class FileSystemNavigationModal(ModalScreen):
28
+ class FileSystemNavigationModal(ExitBoundModalScreen):
31
29
  CONTAINER_ID = "file_system_view_container"
32
30
  CSS_PATH = "styles.tcss"
33
31
 
34
- BINDINGS: ClassVar[list[BindingType]] = [
35
- Binding("escape", "app.pop_screen", "Pop screen"),
36
- ]
37
-
38
32
  active_path: reactive[Path] = reactive(Path())
39
33
 
40
34
  def __init__(
@@ -126,7 +120,7 @@ class FileSystemNavigationModal(ModalScreen):
126
120
  event (Button.Click): The event containing the button that was clicked.
127
121
  """
128
122
  if event.action == "close":
129
- self.on_close()
123
+ self.action_cancel_screen()
130
124
  else:
131
125
  self.on_apply()
132
126
 
@@ -1,3 +1,4 @@
1
+ import contextlib
1
2
  import time
2
3
  import traceback
3
4
  from concurrent.futures import ThreadPoolExecutor
@@ -9,6 +10,7 @@ from textual import on, work
9
10
  from textual.app import App, ComposeResult
10
11
  from textual.binding import Binding, BindingType
11
12
  from textual.containers import Horizontal
13
+ from textual.css.query import NoMatches
12
14
  from textual.reactive import reactive
13
15
  from textual.widgets import Footer
14
16
 
@@ -102,6 +104,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
102
104
  BINDINGS: ClassVar[list[BindingType]] = [
103
105
  Binding("ctrl+r", "registrations", "Registrations list"),
104
106
  Binding("ctrl+s", "storages", "Storages list"),
107
+ Binding("ctrl+f", "find", show=False),
105
108
  Binding(
106
109
  KeyBindings.ARROW_LEFT.value, "focus_sidebar", "Focus sidebar", show=False
107
110
  ),
@@ -152,6 +155,13 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
152
155
  self.theme = "github-dark"
153
156
  self.init_storages_list()
154
157
 
158
+ def action_find(self):
159
+ """
160
+ Focus search input.
161
+ """
162
+ with contextlib.suppress(NoMatches):
163
+ self.query_one(f"#{self.storage_content.search_input_id}").focus()
164
+
155
165
  def action_focus_content(self):
156
166
  """
157
167
  Focuses the storage content container.
@@ -174,10 +184,23 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
174
184
  This method is typically used to allow users to add their
175
185
  cloud storage credentials, which will then be reflected in the storage
176
186
  """
177
- self.app.push_screen(ProviderCredsListScreen(), callback=self.refresh_storages)
187
+ self.app.push_screen(
188
+ ProviderCredsListScreen(), callback=self.modal_screen_callback
189
+ )
178
190
 
179
191
  def action_storages(self):
180
- self.app.push_screen(StoragesListScreen(), callback=self.refresh_storages)
192
+ self.app.push_screen(StoragesListScreen(), callback=self.modal_screen_callback)
193
+
194
+ def modal_screen_callback(self, requires_storage_refresh: bool | None = True):
195
+ """
196
+ Callback for modal screens to refresh the storage list if required.
197
+
198
+ This method is called when a modal screen is closed. If the
199
+ `requires_storage_refresh` flag is set to True, it refreshes the
200
+ storage list by calling the `refresh_storages` method.
201
+ """
202
+ if requires_storage_refresh:
203
+ self.refresh_storages()
181
204
 
182
205
  def refresh_storages(self, *args, **kwargs):
183
206
  """
@@ -187,6 +210,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
187
210
  configurations.
188
211
  """
189
212
  self.storage_list_sidebar.storages = {}
213
+ self.storage_list_sidebar.last_update_timestamp = time.time()
190
214
  self.init_storages_list()
191
215
 
192
216
  @work(thread=True)
@@ -384,6 +408,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
384
408
  PreviewContentScreen(
385
409
  storage_name=event.storage_name,
386
410
  key=event.path,
411
+ file_size=event.size,
387
412
  access_credentials_uuid=event.access_credentials_uuid,
388
413
  )
389
414
  )
@@ -8,3 +8,4 @@ class PreviewRequest(Message):
8
8
  storage_name: str
9
9
  access_credentials_uuid: str
10
10
  path: str
11
+ size: int
@@ -12,6 +12,7 @@ from dataclasses import dataclass
12
12
  from enum import Enum, auto
13
13
  from typing import ClassVar, Self
14
14
 
15
+ import humanize
15
16
  from textual import events, on
16
17
  from textual.app import ComposeResult
17
18
  from textual.binding import Binding, BindingType
@@ -308,6 +309,7 @@ class FileItem(StorageContentItem):
308
309
  """Message sent when a file preview is selected."""
309
310
 
310
311
  name: str
312
+ size: int
311
313
 
312
314
  @dataclass
313
315
  class Unselect(Message):
@@ -333,7 +335,9 @@ class FileItem(StorageContentItem):
333
335
  yield FileMetaLabel(
334
336
  f"{FILE_ICON} {self.file.key}", classes="file_name", markup=False
335
337
  )
336
- yield FileMetaLabel(f"{self.file.size}", classes="file_size", markup=False)
338
+ yield FileMetaLabel(
339
+ f"{humanize.naturalsize(self.file.size)}", classes="file_size", markup=False
340
+ )
337
341
  yield FileMetaLabel(
338
342
  str(self.file.date_modified), classes="file_date", markup=False
339
343
  )
@@ -360,7 +364,7 @@ class FileItem(StorageContentItem):
360
364
  preview_button = self.query_one(Button)
361
365
 
362
366
  if widget is preview_button:
363
- self.post_message(self.Preview(self.file.key))
367
+ self.post_message(self.Preview(self.file.key, self.file.size))
364
368
  return
365
369
 
366
370
  checkbox = self.query_one(UnfocusableCheckbox)
@@ -561,6 +565,8 @@ class StorageContentContainer(Vertical):
561
565
  }
562
566
  """
563
567
 
568
+ search_input_id: ClassVar[str] = "search_input"
569
+
564
570
  def compose(self) -> ComposeResult:
565
571
  if not self.storage:
566
572
  return
@@ -584,7 +590,7 @@ class StorageContentContainer(Vertical):
584
590
  with Horizontal():
585
591
  yield Label("Search:")
586
592
  yield Input(
587
- id="search_input",
593
+ id=self.search_input_id,
588
594
  placeholder="input path prefix here...",
589
595
  value=self.search_prefix,
590
596
  )
@@ -663,6 +669,7 @@ class StorageContentContainer(Vertical):
663
669
  self.storage,
664
670
  self.access_credentials_uuid,
665
671
  os.path.join(self.path, event.name) if self.path else event.name,
672
+ event.size,
666
673
  )
667
674
  )
668
675
 
@@ -1,52 +1,194 @@
1
+ import contextlib
2
+ import re
3
+ from dataclasses import dataclass
1
4
  from pathlib import Path
5
+ from typing import ClassVar
2
6
 
7
+ import humanize
3
8
  from dependency_injector.wiring import Provide
4
9
  from rich.syntax import Syntax
5
- from textual import on
10
+ from textual import events, on
6
11
  from textual.app import ComposeResult
12
+ from textual.binding import Binding, BindingType
7
13
  from textual.containers import Container, Horizontal
8
- from textual.screen import ModalScreen
9
- from textual.widgets import LoadingIndicator, RichLog
14
+ from textual.css.query import NoMatches
15
+ from textual.document._document import Selection
16
+ from textual.message import Message
17
+ from textual.reactive import reactive
18
+ from textual.widgets import Input, Label, LoadingIndicator, Rule, TextArea
10
19
 
11
20
  from sourcerer.infrastructure.access_credentials.services import CredentialsService
12
21
  from sourcerer.infrastructure.storage_provider.exceptions import (
13
22
  ReadStorageItemsError,
14
23
  )
15
24
  from sourcerer.presentation.di_container import DiContainer
25
+ from sourcerer.presentation.screens.preview_content.text_area_style import (
26
+ SOURCERER_THEME_NAME,
27
+ sourcerer_text_area_theme,
28
+ )
29
+ from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
16
30
  from sourcerer.presentation.screens.shared.widgets.button import Button
17
31
  from sourcerer.presentation.utils import get_provider_service_by_access_uuid
32
+ from sourcerer.settings import PREVIEW_LENGTH_LIMIT, PREVIEW_LIMIT_SIZE
33
+
34
+
35
+ @dataclass
36
+ class HighlightResult(Message):
37
+ line: int
38
+ start: int
39
+ end: int
40
+
41
+
42
+ @dataclass
43
+ class HideSearchBar(Message):
44
+ pass
45
+
46
+
47
+ class ClickableLabel(Label):
48
+ @dataclass
49
+ class Click(Message):
50
+ name: str
51
+
52
+ def __init__(self, *args, **kwargs):
53
+ super().__init__(*args, **kwargs)
54
+
55
+ def on_click(self, _: events.Click) -> None:
56
+ self.post_message(self.Click(name=self.name)) # type: ignore
57
+
58
+
59
+ class Search(Container):
60
+ total = reactive(0, recompose=False)
61
+ current = reactive(0, recompose=False)
62
+ content = reactive("", recompose=False)
63
+
64
+ def __init__(self, *args, **kwargs):
65
+ super().__init__(*args, **kwargs)
66
+ self.search_result_lines = []
67
+ self.search_value = ""
68
+
69
+ def compose(self) -> ComposeResult:
70
+ with Horizontal():
71
+ with Horizontal(id="left"):
72
+ yield Label("Search:")
73
+ yield Input(placeholder="...")
74
+
75
+ with Horizontal(id="right"):
76
+ yield ClickableLabel(
77
+ "◀", id="previous", name="previous", classes="search-button"
78
+ )
79
+ yield Label(f"{self.current}/{self.total}", id="search-result")
80
+ yield ClickableLabel(
81
+ "▶", id="next", name="next", classes="search-button"
82
+ )
83
+ yield ClickableLabel(
84
+ "❌", id="hide", name="hide", classes="search-button"
85
+ )
86
+ yield Rule()
87
+
88
+ @on(Input.Submitted)
89
+ def on_input_submitted(self, event: Input.Submitted) -> None:
90
+ """Handle input submitted events."""
91
+ if not event.value or not self.content:
92
+ self.total = 0
93
+ self.current = 0
94
+ self.search_value = ""
95
+ self.search_result_lines = []
96
+ return
97
+ if event.value == self.search_value:
98
+ self._increment_current()
99
+ return
100
+
101
+ self.search_value = event.value
102
+ lines = self.content.split("\n")
103
+ search_pattern = event.value.lower()
18
104
 
105
+ self.search_result_lines = [
106
+ (line_n, index)
107
+ for line_n, line in enumerate(lines)
108
+ if search_pattern in line.lower()
109
+ for index in [
110
+ match.start()
111
+ for match in re.finditer(rf"(?i){re.escape(search_pattern)}", line)
112
+ ]
113
+ ]
19
114
 
20
- class PreviewContentScreen(ModalScreen):
115
+ if not self.search_result_lines:
116
+ self.notify("No matches found", severity="warning")
117
+ self.total, self.current = 0, 0
118
+ return
119
+
120
+ self.total = len(self.search_result_lines)
121
+ self.current = 1
122
+
123
+ @on(ClickableLabel.Click)
124
+ def on_click(self, event: ClickableLabel.Click) -> None:
125
+ if event.name == "next":
126
+ self._increment_current()
127
+ elif event.name == "previous":
128
+ self._decrement_current()
129
+ elif event.name == "hide":
130
+ self.post_message(HideSearchBar())
131
+
132
+ def _increment_current(self):
133
+ self.current = self.current + 1 if self.current < self.total else 1
134
+
135
+ def _decrement_current(self):
136
+ self.current = self.current - 1 if self.current > 1 else self.total
137
+
138
+ def watch_current(self):
139
+ with contextlib.suppress(NoMatches):
140
+ search_result = self.query_one("#search-result", Label)
141
+ search_result.update(f"{self.current}/{self.total}")
142
+ if not self.search_result_lines:
143
+ return
144
+ line, start = self.search_result_lines[self.current - 1]
145
+ self.post_message(
146
+ HighlightResult(line, start=start, end=start + len(self.search_value))
147
+ )
148
+
149
+
150
+ class PreviewContentScreen(ExitBoundModalScreen):
21
151
  CSS_PATH = "styles.tcss"
22
152
 
153
+ BINDINGS: ClassVar[list[BindingType]] = [
154
+ Binding("escape", "cancel", "Close the screen"),
155
+ ]
156
+
23
157
  def __init__(
24
158
  self,
25
159
  storage_name,
26
160
  key,
161
+ file_size,
27
162
  access_credentials_uuid,
28
163
  *args,
29
164
  credentials_service: CredentialsService = Provide[
30
165
  DiContainer.credentials_repository
31
166
  ],
32
- **kwargs
167
+ **kwargs,
33
168
  ):
34
169
  super().__init__(*args, **kwargs)
35
170
 
36
171
  self.storage_name = storage_name
37
172
  self.key = key
173
+ self.file_size = file_size
38
174
  self.access_credentials_uuid = access_credentials_uuid
39
175
  self.credentials_service = credentials_service
176
+ self.content = None
40
177
 
41
178
  def compose(self) -> ComposeResult:
42
179
  with Container(id="PreviewContentScreen"):
180
+ yield Search(id="search-bar")
43
181
  yield LoadingIndicator(id="loading")
44
- yield RichLog(highlight=True, markup=True, auto_scroll=False)
182
+ yield TextArea(read_only=True, show_line_numbers=True)
45
183
  with Horizontal(id="controls"):
46
184
  yield Button("Close", name="cancel")
47
185
 
48
186
  def on_mount(self) -> None:
49
187
  """Called when the DOM is ready."""
188
+ search = self.query_one(Search)
189
+ text_log = self.query_one(TextArea)
190
+ text_log.register_theme(sourcerer_text_area_theme)
191
+ text_log.theme = SOURCERER_THEME_NAME
50
192
 
51
193
  provider_service = get_provider_service_by_access_uuid(
52
194
  self.access_credentials_uuid, self.credentials_service
@@ -55,28 +197,73 @@ class PreviewContentScreen(ModalScreen):
55
197
  self.notify("Could not read file :(", severity="error")
56
198
  return
57
199
  try:
58
- content = provider_service.read_storage_item(self.storage_name, self.key)
200
+ self.content = provider_service.read_storage_item(
201
+ self.storage_name, self.key
202
+ )
203
+ if self.file_size > PREVIEW_LIMIT_SIZE:
204
+ self.content = self.content[:PREVIEW_LENGTH_LIMIT]
205
+ self.notify(
206
+ f"The file size {humanize.naturalsize(self.file_size)} "
207
+ f"exceeds {humanize.naturalsize(PREVIEW_LIMIT_SIZE)} preview limit. "
208
+ f"The content is truncated to {PREVIEW_LENGTH_LIMIT} characters.",
209
+ severity="warning",
210
+ )
211
+ search.content = self.content
59
212
  except ReadStorageItemsError:
60
213
  self.notify("Could not read file :(", severity="error")
61
214
  return
62
215
  self.query_one("#loading").remove()
63
- if content is None:
216
+ if self.content is None:
64
217
  self.notify("Empty file", severity="warning")
65
218
  return
66
219
 
67
- text_log = self.query_one(RichLog)
68
-
69
220
  extension = Path(self.key).suffix
70
221
 
71
222
  lexer = (
72
- "json" if extension == ".tfstate" else Syntax.guess_lexer(self.key, content)
223
+ "json"
224
+ if extension == ".tfstate"
225
+ else Syntax.guess_lexer(self.key, self.content)
73
226
  )
74
-
75
- content = Syntax(content, lexer, line_numbers=True, theme="ansi_dark")
76
- text_log.write(content)
227
+ if lexer in text_log.available_languages:
228
+ text_log.language = lexer
229
+ else:
230
+ text_log.language = "python"
231
+ text_log.blur()
232
+ text_log.load_text(self.content)
77
233
 
78
234
  @on(Button.Click)
79
235
  def on_button_click(self, event: Button.Click) -> None:
80
236
  """Handle button click events."""
81
237
  if event.action == "cancel":
82
- self.dismiss()
238
+ self.action_cancel_screen()
239
+
240
+ @on(HideSearchBar)
241
+ def on_hide_search_bar(self, _: HideSearchBar) -> None:
242
+ """Handle hide search bar events."""
243
+ search_bar = self.query_one("#search-bar", Search)
244
+ search_bar.remove_class("-visible")
245
+ search_bar.query_one(Input).value = ""
246
+ search_bar.total = 0
247
+ search_bar.current = 0
248
+ search_bar.search_result_lines = []
249
+ search_bar.search_value = ""
250
+
251
+ @on(HighlightResult)
252
+ def on_highlight_result(self, event: HighlightResult) -> None:
253
+ """Handle highlight result events."""
254
+
255
+ text_area = self.query_one(TextArea)
256
+ text_area.selection = Selection(
257
+ start=(event.line, event.start), end=(event.line, event.end)
258
+ )
259
+
260
+ def action_find(self):
261
+ self.query_one("#search-bar").add_class("-visible")
262
+ self.query_one(Input).focus()
263
+
264
+ def action_cancel(self):
265
+ self.action_cancel_screen()
266
+
267
+ def on_key(self, event: events.Key) -> None:
268
+ if event.key in ("ctrl+f", "super+f"):
269
+ self.action_find()
@@ -12,16 +12,51 @@ PreviewContentScreen {
12
12
  & > #PreviewContentScreen {
13
13
  padding: 1 2 0 2;
14
14
  margin: 0 0;
15
- width: 100;
15
+ width: 70%;
16
16
  height: 40;
17
17
  border: solid $secondary-background;
18
18
  border-title-color: $primary-lighten-2;
19
19
 
20
- RichLog {
20
+
21
+ #search-bar {
22
+ height: auto;
23
+ display: none;
24
+
25
+ &.-visible {
26
+ display: block;
27
+ }
28
+ }
29
+
30
+ Horizontal{
31
+ height: auto;
32
+
33
+ & > Static {
34
+ width: auto;
35
+ }
36
+
37
+ #left {
38
+ align: left middle;
39
+ width: 90%;
40
+ }
41
+ #right {
42
+ align: right middle;
43
+ width: 10%;
44
+ }
45
+ }
46
+
47
+ Input {
48
+ height: 1;
49
+ border: none;
21
50
  background: transparent;
22
- width: 95;
51
+ width: 78%;
23
52
  }
24
53
 
54
+ TextArea {
55
+ background-tint: $background;
56
+
57
+ &:focus {
58
+ border: tall $border-blurred;
59
+ }
60
+ }
25
61
  }
26
62
  }
27
-
@@ -0,0 +1,60 @@
1
+ from rich.style import Style
2
+ from textual._text_area_theme import TextAreaTheme
3
+
4
+ SOURCERER_THEME_NAME = "sourcerer"
5
+ # Terraform theme for the text area.
6
+ sourcerer_text_area_theme = TextAreaTheme(
7
+ name=SOURCERER_THEME_NAME,
8
+ cursor_style=Style(color="#1e1e1e", bgcolor="#f0f0f0"),
9
+ cursor_line_style=Style(bgcolor="#2b2b2b"),
10
+ bracket_matching_style=Style(bgcolor="#3a3a3a", bold=True),
11
+ cursor_line_gutter_style=Style(color="#CCCCCC", bgcolor="#2b2b2b"),
12
+ selection_style=Style(bgcolor="#FFA656"),
13
+ syntax_styles={
14
+ "string": Style(color="#79C0FF"),
15
+ "string.documentation": Style(color="#79C0FF"),
16
+ "comment": Style(color="#6A9955"),
17
+ "heading.marker": Style(color="#6E7681"),
18
+ "keyword": Style(color="#C586C0"),
19
+ "operator": Style(color="#CCCCCC"),
20
+ "conditional": Style(color="#569cd6"),
21
+ "keyword.function": Style(color="#F97970"),
22
+ "keyword.return": Style(color="#569cd6"),
23
+ "keyword.operator": Style(color="#569cd6"),
24
+ "repeat": Style(color="#569cd6"),
25
+ "exception": Style(color="#569cd6"),
26
+ "include": Style(color="#569cd6"),
27
+ "number": Style(color="#b5cea8"),
28
+ "float": Style(color="#b5cea8"),
29
+ "class": Style(color="#4EC9B0"),
30
+ "type": Style(color="#EFCB43"),
31
+ "type.class": Style(color="#FFA656"),
32
+ "type.builtin": Style(color="#CDD9E5"),
33
+ "function": Style(color="#CCB0EA"),
34
+ "function.call": Style(color="#CCB0EA"),
35
+ "method": Style(color="#CCB0EA"),
36
+ "method.call": Style(color="#CCB0EA"),
37
+ "constructor": Style(color="#DCBDFB"),
38
+ "boolean": Style(color="#7DAF9C"),
39
+ "constant.builtin": Style(color="#7DAF9C"),
40
+ "json.null": Style(color="#FDA556"),
41
+ "tag": Style(color="#EFCB43"),
42
+ "yaml.field": Style(color="#569cd6", bold=True),
43
+ "json.label": Style(color="#8EDB8C", bold=True),
44
+ "toml.type": Style(color="#569cd6"),
45
+ "toml.datetime": Style(color="#C586C0", italic=True),
46
+ "css.property": Style(color="#569cd6"),
47
+ "heading": Style(color="#569cd6", bold=True),
48
+ "bold": Style(bold=True),
49
+ "italic": Style(italic=True),
50
+ "strikethrough": Style(strike=True),
51
+ "link.uri": Style(color="#40A6FF", underline=True),
52
+ "link.label": Style(color="#569cd6"),
53
+ "list.marker": Style(color="#6E7681"),
54
+ "inline_code": Style(color="#ce9178"),
55
+ "info_string": Style(color="#ce9178", bold=True, italic=True),
56
+ "punctuation.bracket": Style(color="#CCCCCC"),
57
+ "punctuation.delimiter": Style(color="#CCCCCC"),
58
+ "punctuation.special": Style(color="#CCCCCC"),
59
+ },
60
+ )
@@ -7,7 +7,6 @@ from textual.app import ComposeResult
7
7
  from textual.containers import Container, Horizontal, VerticalScroll
8
8
  from textual.message import Message
9
9
  from textual.reactive import reactive
10
- from textual.screen import ModalScreen
11
10
  from textual.widgets import Checkbox, Label
12
11
 
13
12
  from sourcerer.domain.access_credentials.entities import Credentials
@@ -22,6 +21,9 @@ from sourcerer.presentation.screens.provider_creds_registration.main import (
22
21
  ProviderCredsRegistrationScreen,
23
22
  )
24
23
  from sourcerer.presentation.screens.question.main import QuestionScreen
24
+ from sourcerer.presentation.screens.shared.modal_screens import (
25
+ RefreshTriggerableModalScreen,
26
+ )
25
27
  from sourcerer.presentation.screens.shared.widgets.button import Button
26
28
 
27
29
 
@@ -98,7 +100,7 @@ class ProviderCredentialsRow(Horizontal):
98
100
  self.post_message(ReloadCredentialsRequest())
99
101
 
100
102
 
101
- class ProviderCredsListScreen(ModalScreen):
103
+ class ProviderCredsListScreen(RefreshTriggerableModalScreen):
102
104
  CSS_PATH = "styles.tcss"
103
105
 
104
106
  MAIN_CONTAINER_ID = "ProviderCredsListScreen"
@@ -151,13 +153,15 @@ class ProviderCredsListScreen(ModalScreen):
151
153
  """
152
154
  Initialize the screen by refreshing the credentials list when the screen is composed.
153
155
  """
154
- self.refresh_credentials_list()
156
+ self.refresh_credentials_list(set_refresh_flag=False)
155
157
 
156
- def refresh_credentials_list(self):
158
+ def refresh_credentials_list(self, set_refresh_flag: bool = True):
157
159
  """
158
160
  Refresh the credentials list by retrieving the latest credentials from the credentials service.
159
161
  """
160
162
  self.credentials_list = self.credentials_service.list()
163
+ if set_refresh_flag:
164
+ self._requires_storage_refresh = True
161
165
 
162
166
  def create_provider_creds_registration(
163
167
  self,
@@ -195,7 +199,7 @@ class ProviderCredsListScreen(ModalScreen):
195
199
  event (Button.Click): The button click event.
196
200
  """
197
201
  if event.action == ControlsEnum.CANCEL.name:
198
- self.dismiss()
202
+ self.action_cancel_screen()
199
203
  if event.action == "add_registration":
200
204
  self.app.push_screen(
201
205
  ProviderCredsRegistrationScreen(),
@@ -5,7 +5,6 @@ from dependency_injector.wiring import Provide
5
5
  from textual import on
6
6
  from textual.app import ComposeResult
7
7
  from textual.containers import Container, Horizontal, VerticalScroll
8
- from textual.screen import ModalScreen
9
8
  from textual.widgets import Label, Select
10
9
 
11
10
  from sourcerer.domain.access_credentials.services import (
@@ -20,6 +19,7 @@ from sourcerer.infrastructure.access_credentials.registry import (
20
19
  )
21
20
  from sourcerer.infrastructure.utils import generate_unique_name
22
21
  from sourcerer.presentation.di_container import DiContainer
22
+ from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
23
23
  from sourcerer.presentation.screens.shared.widgets.button import Button
24
24
  from sourcerer.presentation.screens.shared.widgets.labeled_input import LabeledInput
25
25
 
@@ -36,7 +36,7 @@ class ProviderCredentialsEntry:
36
36
  fields: dict[str, str]
37
37
 
38
38
 
39
- class ProviderCredsRegistrationScreen(ModalScreen):
39
+ class ProviderCredsRegistrationScreen(ExitBoundModalScreen):
40
40
  CSS_PATH = "styles.tcss"
41
41
 
42
42
  MAIN_CONTAINER_ID = "ProviderCredsRegistrationScreen"
@@ -182,7 +182,7 @@ class ProviderCredsRegistrationScreen(ModalScreen):
182
182
  collected authentication fields.
183
183
  """
184
184
  if event.action == ControlsEnum.CANCEL.name:
185
- self.dismiss()
185
+ self.action_cancel_screen()
186
186
  elif event.action == ControlsEnum.CREATE.name:
187
187
  if not self.auth_method:
188
188
  self.notify("Please select provider and auth method", severity="error")
@@ -0,0 +1,37 @@
1
+ from typing import ClassVar
2
+
3
+ from textual.binding import Binding, BindingType
4
+ from textual.screen import ModalScreen
5
+
6
+
7
+ class ExitBoundModalScreen(ModalScreen):
8
+ """
9
+ A base class for modal screens that can be exited.
10
+ It provides a method to exit the screen and a flag to indicate if the screen should be exited.
11
+ """
12
+
13
+ BINDINGS: ClassVar[list[BindingType]] = [
14
+ Binding("escape", "cancel_screen", "Pop screen"),
15
+ ]
16
+
17
+ def action_cancel_screen(self):
18
+ """
19
+ Action to exit the screen.
20
+ """
21
+ self.dismiss()
22
+
23
+
24
+ class RefreshTriggerableModalScreen(ExitBoundModalScreen):
25
+ """
26
+ A base class for modal screens that can be refreshed.
27
+ It provides a method to refresh the screen and a flag to indicate if the screen should be refreshed.
28
+ """
29
+
30
+ def __init__(self, *args, **kwargs):
31
+ super().__init__(*args, **kwargs)
32
+ self._requires_storage_refresh = False
33
+
34
+ def action_cancel_screen(self):
35
+ requires_storage_refresh = self._requires_storage_refresh
36
+ self._requires_storage_refresh = False
37
+ self.dismiss(requires_storage_refresh)
@@ -7,7 +7,6 @@ from textual import on
7
7
  from textual.app import ComposeResult
8
8
  from textual.containers import Container, Horizontal, VerticalScroll
9
9
  from textual.reactive import reactive
10
- from textual.screen import ModalScreen
11
10
  from textual.widgets import Label
12
11
 
13
12
  from sourcerer.domain.storage.entities import Storage
@@ -15,6 +14,9 @@ from sourcerer.infrastructure.access_credentials.services import CredentialsServ
15
14
  from sourcerer.infrastructure.storage.services import StoragesService
16
15
  from sourcerer.presentation.di_container import DiContainer
17
16
  from sourcerer.presentation.screens.question.main import QuestionScreen
17
+ from sourcerer.presentation.screens.shared.modal_screens import (
18
+ RefreshTriggerableModalScreen,
19
+ )
18
20
  from sourcerer.presentation.screens.shared.widgets.button import Button
19
21
  from sourcerer.presentation.screens.storages_list.messages.reload_storages_request import (
20
22
  ReloadStoragesRequest,
@@ -74,7 +76,7 @@ class StorageRow(Horizontal):
74
76
  self.post_message(ReloadStoragesRequest())
75
77
 
76
78
 
77
- class StoragesListScreen(ModalScreen):
79
+ class StoragesListScreen(RefreshTriggerableModalScreen):
78
80
  CSS_PATH = "styles.tcss"
79
81
 
80
82
  MAIN_CONTAINER_ID = "StoragesListScreen"
@@ -119,13 +121,15 @@ class StoragesListScreen(ModalScreen):
119
121
  """
120
122
  Initialize the screen by refreshing the credentials list when the screen is composed.
121
123
  """
122
- self.refresh_storages_list()
124
+ self.refresh_storages_list(set_refresh_flag=False)
123
125
 
124
- def refresh_storages_list(self):
126
+ def refresh_storages_list(self, set_refresh_flag: bool = True):
125
127
  """
126
128
  Refresh the storages list by retrieving the latest storages from the storage service.
127
129
  """
128
130
  self.storages_list = self.storage_service.list()
131
+ if set_refresh_flag:
132
+ self._requires_storage_refresh = True
129
133
 
130
134
  @on(ReloadStoragesRequest)
131
135
  def on_reload_storages_request(self, _: ReloadStoragesRequest):
@@ -149,7 +153,7 @@ class StoragesListScreen(ModalScreen):
149
153
  event (Button.Click): The button click event.
150
154
  """
151
155
  if event.action == ControlsEnum.CANCEL.name:
152
- self.dismiss()
156
+ self.action_cancel_screen()
153
157
  if event.action == ControlsEnum.ADD_STORAGE.name:
154
158
  self.app.push_screen(
155
159
  StoragesRegistrationScreen(),
@@ -5,11 +5,11 @@ from dependency_injector.wiring import Provide
5
5
  from textual import on
6
6
  from textual.app import ComposeResult
7
7
  from textual.containers import Container, Horizontal, VerticalScroll
8
- from textual.screen import ModalScreen
9
8
  from textual.widgets import Label, Select
10
9
 
11
10
  from sourcerer.infrastructure.access_credentials.services import CredentialsService
12
11
  from sourcerer.presentation.di_container import DiContainer
12
+ from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
13
13
  from sourcerer.presentation.screens.shared.widgets.button import Button
14
14
  from sourcerer.presentation.screens.shared.widgets.labeled_input import LabeledInput
15
15
 
@@ -25,7 +25,7 @@ class StorageEntry:
25
25
  credentials_uuid: str
26
26
 
27
27
 
28
- class StoragesRegistrationScreen(ModalScreen):
28
+ class StoragesRegistrationScreen(ExitBoundModalScreen):
29
29
  CSS_PATH = "styles.tcss"
30
30
 
31
31
  MAIN_CONTAINER_ID = "StoragesRegistrationScreen"
@@ -84,7 +84,7 @@ class StoragesRegistrationScreen(ModalScreen):
84
84
  collected authentication fields.
85
85
  """
86
86
  if event.action == ControlsEnum.CANCEL.name:
87
- self.dismiss()
87
+ self.action_cancel_screen()
88
88
  elif event.action == ControlsEnum.CREATE.name:
89
89
  storage_name = self.query_one("#storage_name", LabeledInput).get().value
90
90
  if not storage_name:
sourcerer/settings.py CHANGED
@@ -74,3 +74,5 @@ TEXT_EXTENSIONS = {
74
74
 
75
75
 
76
76
  PAGE_SIZE = 100
77
+ PREVIEW_LENGTH_LIMIT = 10_000
78
+ PREVIEW_LIMIT_SIZE = 2 * 1024 * 1024 # 2 MB