data-sourcerer 0.3.0__tar.gz → 0.4.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.
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/PKG-INFO +3 -1
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/README.md +2 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/pyproject.toml +1 -1
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/__init__.py +1 -1
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/storage_provider/entities.py +1 -1
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage_provider/services/azure.py +1 -3
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage_provider/services/gcp.py +1 -2
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage_provider/services/s3.py +1 -2
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/file_system_finder/main.py +3 -9
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/main.py +27 -2
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/preview_request.py +1 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/widgets/storage_content.py +10 -3
- data_sourcerer-0.4.0/sourcerer/presentation/screens/preview_content/main.py +269 -0
- data_sourcerer-0.4.0/sourcerer/presentation/screens/preview_content/styles.tcss +62 -0
- data_sourcerer-0.4.0/sourcerer/presentation/screens/preview_content/text_area_style.py +60 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/provider_creds_list/main.py +9 -5
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/provider_creds_registration/main.py +3 -3
- data_sourcerer-0.4.0/sourcerer/presentation/screens/shared/modal_screens.py +37 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storages_list/main.py +9 -5
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storages_registration/main.py +3 -3
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/settings.py +2 -0
- data_sourcerer-0.3.0/sourcerer/presentation/screens/preview_content/main.py +0 -82
- data_sourcerer-0.3.0/sourcerer/presentation/screens/preview_content/styles.tcss +0 -27
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/.gitignore +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/LICENSE +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/access_credentials/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/access_credentials/entities.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/access_credentials/exceptions.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/access_credentials/repositories.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/access_credentials/services.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/file_system/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/file_system/entities.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/file_system/exceptions.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/file_system/services.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/shared/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/shared/entities.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/storage/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/storage/entities.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/storage/repositories.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/storage_provider/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/storage_provider/exceptions.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/storage_provider/services.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/access_credentials/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/access_credentials/exceptions.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/access_credentials/registry.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/access_credentials/repositories.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/access_credentials/services.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/db/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/db/config.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/db/models.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/file_system/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/file_system/exceptions.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/file_system/services.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage/repositories.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage/services.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage_provider/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage_provider/exceptions.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage_provider/registry.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage_provider/services/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/utils.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/app.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/di_container.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/critical_error/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/critical_error/main.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/critical_error/styles.tcss +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/file_system_finder/styles.tcss +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/file_system_finder/widgets/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/delete_request.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/download_request.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/refresh_storages_list_request.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/resizing_rule.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/select_storage_item.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/uncheck_files_request.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/upload_request.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/mixins/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/styles.tcss +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/widgets/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/widgets/gradient.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/widgets/resizing_rule.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/preview_content/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/provider_creds_list/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/provider_creds_list/messages/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/provider_creds_list/messages/reload_credentials_request.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/provider_creds_list/styles.tcss +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/provider_creds_registration/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/provider_creds_registration/styles.tcss +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/question/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/question/main.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/question/styles.tcss +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/shared/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/shared/containers.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/shared/widgets/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/shared/widgets/button.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/shared/widgets/labeled_input.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/shared/widgets/spinner.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storage_action_progress/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storage_action_progress/main.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storage_action_progress/styles.tcss +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storages_list/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storages_list/messages/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storages_list/messages/reload_storages_request.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storages_list/styles.tcss +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storages_registration/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storages_registration/styles.tcss +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/settings.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/themes/__init__.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/themes/github_dark.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/utils.py +0 -0
- {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/utils.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: data-sourcerer
|
3
|
-
Version: 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
|
@@ -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=
|
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=
|
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
|
-
|
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(
|
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.
|
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(
|
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.
|
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
|
)
|
@@ -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(
|
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=
|
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
|
|
@@ -0,0 +1,269 @@
|
|
1
|
+
import contextlib
|
2
|
+
import re
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import ClassVar
|
6
|
+
|
7
|
+
import humanize
|
8
|
+
from dependency_injector.wiring import Provide
|
9
|
+
from rich.syntax import Syntax
|
10
|
+
from textual import events, on
|
11
|
+
from textual.app import ComposeResult
|
12
|
+
from textual.binding import Binding, BindingType
|
13
|
+
from textual.containers import Container, Horizontal
|
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
|
19
|
+
|
20
|
+
from sourcerer.infrastructure.access_credentials.services import CredentialsService
|
21
|
+
from sourcerer.infrastructure.storage_provider.exceptions import (
|
22
|
+
ReadStorageItemsError,
|
23
|
+
)
|
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
|
30
|
+
from sourcerer.presentation.screens.shared.widgets.button import Button
|
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()
|
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
|
+
]
|
114
|
+
|
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):
|
151
|
+
CSS_PATH = "styles.tcss"
|
152
|
+
|
153
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
154
|
+
Binding("escape", "cancel", "Close the screen"),
|
155
|
+
]
|
156
|
+
|
157
|
+
def __init__(
|
158
|
+
self,
|
159
|
+
storage_name,
|
160
|
+
key,
|
161
|
+
file_size,
|
162
|
+
access_credentials_uuid,
|
163
|
+
*args,
|
164
|
+
credentials_service: CredentialsService = Provide[
|
165
|
+
DiContainer.credentials_repository
|
166
|
+
],
|
167
|
+
**kwargs,
|
168
|
+
):
|
169
|
+
super().__init__(*args, **kwargs)
|
170
|
+
|
171
|
+
self.storage_name = storage_name
|
172
|
+
self.key = key
|
173
|
+
self.file_size = file_size
|
174
|
+
self.access_credentials_uuid = access_credentials_uuid
|
175
|
+
self.credentials_service = credentials_service
|
176
|
+
self.content = None
|
177
|
+
|
178
|
+
def compose(self) -> ComposeResult:
|
179
|
+
with Container(id="PreviewContentScreen"):
|
180
|
+
yield Search(id="search-bar")
|
181
|
+
yield LoadingIndicator(id="loading")
|
182
|
+
yield TextArea(read_only=True, show_line_numbers=True)
|
183
|
+
with Horizontal(id="controls"):
|
184
|
+
yield Button("Close", name="cancel")
|
185
|
+
|
186
|
+
def on_mount(self) -> None:
|
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
|
192
|
+
|
193
|
+
provider_service = get_provider_service_by_access_uuid(
|
194
|
+
self.access_credentials_uuid, self.credentials_service
|
195
|
+
)
|
196
|
+
if not provider_service:
|
197
|
+
self.notify("Could not read file :(", severity="error")
|
198
|
+
return
|
199
|
+
try:
|
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
|
212
|
+
except ReadStorageItemsError:
|
213
|
+
self.notify("Could not read file :(", severity="error")
|
214
|
+
return
|
215
|
+
self.query_one("#loading").remove()
|
216
|
+
if self.content is None:
|
217
|
+
self.notify("Empty file", severity="warning")
|
218
|
+
return
|
219
|
+
|
220
|
+
extension = Path(self.key).suffix
|
221
|
+
|
222
|
+
lexer = (
|
223
|
+
"json"
|
224
|
+
if extension == ".tfstate"
|
225
|
+
else Syntax.guess_lexer(self.key, self.content)
|
226
|
+
)
|
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)
|
233
|
+
|
234
|
+
@on(Button.Click)
|
235
|
+
def on_button_click(self, event: Button.Click) -> None:
|
236
|
+
"""Handle button click events."""
|
237
|
+
if event.action == "cancel":
|
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()
|
@@ -0,0 +1,62 @@
|
|
1
|
+
|
2
|
+
Container {
|
3
|
+
height: auto;
|
4
|
+
}
|
5
|
+
|
6
|
+
|
7
|
+
PreviewContentScreen {
|
8
|
+
align: center middle;
|
9
|
+
content-align: center top;
|
10
|
+
|
11
|
+
|
12
|
+
& > #PreviewContentScreen {
|
13
|
+
padding: 1 2 0 2;
|
14
|
+
margin: 0 0;
|
15
|
+
width: 70%;
|
16
|
+
height: 40;
|
17
|
+
border: solid $secondary-background;
|
18
|
+
border-title-color: $primary-lighten-2;
|
19
|
+
|
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;
|
50
|
+
background: transparent;
|
51
|
+
width: 78%;
|
52
|
+
}
|
53
|
+
|
54
|
+
TextArea {
|
55
|
+
background-tint: $background;
|
56
|
+
|
57
|
+
&:focus {
|
58
|
+
border: tall $border-blurred;
|
59
|
+
}
|
60
|
+
}
|
61
|
+
}
|
62
|
+
}
|