termcore 0.2.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.
- termcore/__init__.py +6 -0
- termcore/cli/__init__.py +3 -0
- termcore/cli/output.py +70 -0
- termcore/io/__init__.py +7 -0
- termcore/io/app_state_storage.py +185 -0
- termcore/io/database.py +655 -0
- termcore/io/errors.py +71 -0
- termcore/io/file.py +263 -0
- termcore/io/textfile.py +92 -0
- termcore/py.typed +0 -0
- termcore/tui/__init__.py +9 -0
- termcore/tui/custom_bindings.py +551 -0
- termcore/tui/custom_widgets/__init__.py +1 -0
- termcore/tui/custom_widgets/custom_checkbox.py +41 -0
- termcore/tui/custom_widgets/custom_data_table.py +247 -0
- termcore/tui/custom_widgets/custom_selection.py +80 -0
- termcore/tui/custom_widgets/multiline_footer.py +240 -0
- termcore/tui/question_screen.py +107 -0
- termcore/tui/theme_loader.py +495 -0
- termcore/tui/themes/__init__.py +2 -0
- termcore/tui/themes/classic-black-saturated/style.css +57 -0
- termcore/tui/themes/classic-black-saturated/theme.py +22 -0
- termcore/tui/themes/classic-black-v1/style.css +57 -0
- termcore/tui/themes/classic-black-v1/theme.py +22 -0
- termcore/tui/themes/classic-black-v2/style.css +57 -0
- termcore/tui/themes/classic-black-v2/theme.py +24 -0
- termcore/tui/themes/classic-blue/theme.py +21 -0
- termcore/tui/themes/compact-gray/style.tcss +175 -0
- termcore/tui/themes/compact-gray/theme.py +22 -0
- termcore/tui/themes/mnml-black/style.css +57 -0
- termcore/tui/themes/mnml-black/theme.py +21 -0
- termcore/tui/themes/mnml-deepblack/style.css +57 -0
- termcore/tui/themes/mnml-deepblack/theme.py +21 -0
- termcore/tui/themes/pure-amber/theme.py +18 -0
- termcore/tui/themes/pure-black/theme.py +18 -0
- termcore/tui/themes/pure-blue/theme.py +18 -0
- termcore/tui/themes/pure-green/theme.py +18 -0
- termcore/tui/themes/pure-sweet16/theme.py +20 -0
- termcore/tui/themes/xplore-black/theme.py +18 -0
- termcore/tui/themes/xplore-blue/theme.py +18 -0
- termcore/tui/themes/xplore-blue-muted/theme.py +18 -0
- termcore/tui/themes/xplore-teal/theme.py +18 -0
- termcore/util/__init__.py +10 -0
- termcore/util/datetime.py +173 -0
- termcore/util/debug.py +196 -0
- termcore/util/index.py +112 -0
- termcore/util/logger.py +36 -0
- termcore/util/singleton.py +46 -0
- termcore/util/string.py +263 -0
- termcore/util/validation.py +31 -0
- termcore/util/version.py +56 -0
- termcore-0.2.0.dist-info/METADATA +504 -0
- termcore-0.2.0.dist-info/RECORD +55 -0
- termcore-0.2.0.dist-info/WHEEL +4 -0
- termcore-0.2.0.dist-info/licenses/LICENSE +21 -0
termcore/io/errors.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Domain exceptions raised by the `termcore.io` modules."""
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"AppStateError",
|
|
5
|
+
"DatabaseError",
|
|
6
|
+
"EmptyConditionsError",
|
|
7
|
+
"StateFileError",
|
|
8
|
+
"UnknownIdentifierError",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DatabaseError(Exception):
|
|
13
|
+
"""Base class for every error raised while talking to a database."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UnknownIdentifierError(DatabaseError):
|
|
17
|
+
"""
|
|
18
|
+
Raised when a table or column name is not part of the schema.
|
|
19
|
+
|
|
20
|
+
Identifiers cannot be passed as query parameters, so they are checked
|
|
21
|
+
against the schema before they are put into a statement. Rejecting an
|
|
22
|
+
unknown name here means a typo is reported as a typo, and a name that
|
|
23
|
+
was never a column cannot become a fragment of SQL.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
name: str
|
|
27
|
+
known: tuple[str, ...]
|
|
28
|
+
|
|
29
|
+
def __init__(self, name: str, known: tuple[str, ...]) -> None:
|
|
30
|
+
"""Stores the rejected name and what would have been accepted."""
|
|
31
|
+
super().__init__(
|
|
32
|
+
f"Unknown identifier {name!r}. "
|
|
33
|
+
f"Known identifiers: {', '.join(known) or '(none)'}"
|
|
34
|
+
)
|
|
35
|
+
self.name = name
|
|
36
|
+
self.known = known
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class EmptyConditionsError(DatabaseError):
|
|
40
|
+
"""
|
|
41
|
+
Raised when an update or a delete is asked to run without conditions.
|
|
42
|
+
|
|
43
|
+
An empty condition list almost always means the caller built it from
|
|
44
|
+
something that turned out to be empty, and carrying on would rewrite or
|
|
45
|
+
delete every row in the table.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
operation: str
|
|
49
|
+
|
|
50
|
+
def __init__(self, operation: str) -> None:
|
|
51
|
+
"""Stores the operation that was refused."""
|
|
52
|
+
super().__init__(
|
|
53
|
+
f"Refusing to {operation} every row of the table: no conditions "
|
|
54
|
+
f"were given. Use query() if that is genuinely intended."
|
|
55
|
+
)
|
|
56
|
+
self.operation = operation
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AppStateError(Exception):
|
|
60
|
+
"""Base class for every error raised while handling the app state."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class StateFileError(AppStateError):
|
|
64
|
+
"""
|
|
65
|
+
Raised when the state file cannot be read, created or written.
|
|
66
|
+
|
|
67
|
+
The storage used to print a message and call `sys.exit()` here, which
|
|
68
|
+
takes the whole application down from inside a library and leaves the
|
|
69
|
+
caller no way to react. Losing the state is worth reporting, but it is
|
|
70
|
+
the application's decision what to do about it.
|
|
71
|
+
"""
|
termcore/io/file.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Reading, writing and managing files and folders.
|
|
4
|
+
|
|
5
|
+
It offers a simple and lightweight interface for handling
|
|
6
|
+
text-based file operations.
|
|
7
|
+
|
|
8
|
+
This module includes functions and classes to work with text files, such as
|
|
9
|
+
reading content, writing data, appending text and performing basic file
|
|
10
|
+
manipulations. It is designed to simplify text file handling in Python projects
|
|
11
|
+
by providing an easy-to-use API.
|
|
12
|
+
|
|
13
|
+
Features:
|
|
14
|
+
|
|
15
|
+
- Read content from text files.
|
|
16
|
+
- Write content to text files.
|
|
17
|
+
- Append data to existing files.
|
|
18
|
+
- Handle file encoding issues.
|
|
19
|
+
- Support for line-by-line reading to optimize memory usage.
|
|
20
|
+
|
|
21
|
+
This module is particularly useful for applications requiring efficient and
|
|
22
|
+
straightforward text file operations, such as logging systems, data processing
|
|
23
|
+
and configuration management.
|
|
24
|
+
"""
|
|
25
|
+
import shutil
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"File",
|
|
31
|
+
"FolderItem",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(slots=True)
|
|
36
|
+
class FolderItem:
|
|
37
|
+
"""
|
|
38
|
+
Internal class: Represents an item (file or folder) within a directory.
|
|
39
|
+
|
|
40
|
+
Attributes
|
|
41
|
+
----------
|
|
42
|
+
type : str
|
|
43
|
+
Type of the item, either 'file' or 'folder'.
|
|
44
|
+
name : str
|
|
45
|
+
Name of the file or folder (without extension for files).
|
|
46
|
+
level : int
|
|
47
|
+
Depth level of the item in the directory tree
|
|
48
|
+
(0 for root items, 1 for sub-items, etc.).
|
|
49
|
+
Items in a sub folder level 1 etc.
|
|
50
|
+
extension : str or None, optional
|
|
51
|
+
File extension without leading dot (None for folders).
|
|
52
|
+
|
|
53
|
+
Notes
|
|
54
|
+
-----
|
|
55
|
+
TODO: Support more pieces of information like size, modified, etc.
|
|
56
|
+
TODO: Method to copy/move/delete a file (one method "copy" that copies
|
|
57
|
+
folders and files)
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
type: str
|
|
61
|
+
name: str
|
|
62
|
+
level: int
|
|
63
|
+
extension: str | None = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class File:
|
|
67
|
+
"""
|
|
68
|
+
A collection of static methods for performing file and folder operations.
|
|
69
|
+
|
|
70
|
+
Provides functionalities to list folder contents, copy files and folders,
|
|
71
|
+
retrieve file extensions, modify file extensions, and extract folder paths
|
|
72
|
+
from file paths.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def folder_content(
|
|
77
|
+
path: str, extfilter: str | None = None
|
|
78
|
+
) -> dict[str, FolderItem]:
|
|
79
|
+
"""
|
|
80
|
+
Retrieves the contents of a given folder, without its subfolders.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
path : str
|
|
85
|
+
The directory path to list contents from.
|
|
86
|
+
extfilter : str or None, optional
|
|
87
|
+
A specific file extension to filter results
|
|
88
|
+
(e.g., "txt").
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
dict[str, FolderItem]
|
|
93
|
+
A dictionary where the keys are item paths and the values are
|
|
94
|
+
FolderItem instances containing metadata about each item.
|
|
95
|
+
"""
|
|
96
|
+
return File._folder_content(path, extfilter, with_subfolders=False)
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def folder_content_recursive(
|
|
100
|
+
path: str, extfilter: str | None = None
|
|
101
|
+
) -> dict[str, FolderItem]:
|
|
102
|
+
"""
|
|
103
|
+
Retrieves the contents of a given folder and of every subfolder.
|
|
104
|
+
|
|
105
|
+
Each `FolderItem` carries its depth below `path` in `level`.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
path : str
|
|
110
|
+
The directory path to list contents from.
|
|
111
|
+
extfilter : str or None, optional
|
|
112
|
+
A specific file extension to filter results
|
|
113
|
+
(e.g., "txt").
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
dict[str, FolderItem]
|
|
118
|
+
A dictionary where the keys are item paths and the values are
|
|
119
|
+
FolderItem instances containing metadata about each item.
|
|
120
|
+
"""
|
|
121
|
+
return File._folder_content(path, extfilter, with_subfolders=True)
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _folder_content( # noqa: PLR0913 - level is carried by recursion
|
|
125
|
+
path: str, extfilter: str | None = None, level: int = 0,
|
|
126
|
+
*, with_subfolders: bool
|
|
127
|
+
) -> dict[str, FolderItem]:
|
|
128
|
+
"""
|
|
129
|
+
Lists a folder, descending into subfolders when asked to.
|
|
130
|
+
|
|
131
|
+
Notes
|
|
132
|
+
-----
|
|
133
|
+
TODO: also return a sorted list of the keys of the dictionary
|
|
134
|
+
"""
|
|
135
|
+
# Get all items of the folder and create a dictionary
|
|
136
|
+
items_list: list[str] = [p.name for p in Path(path).iterdir()]
|
|
137
|
+
items_dict: dict[str, FolderItem] = {}
|
|
138
|
+
|
|
139
|
+
# Loop folder items
|
|
140
|
+
for item in items_list:
|
|
141
|
+
item_path = path + "/" + item
|
|
142
|
+
|
|
143
|
+
# Recursion if the item is a folder
|
|
144
|
+
if Path(item_path).is_dir() and with_subfolders:
|
|
145
|
+
items_dict = {**items_dict, **File._folder_content(
|
|
146
|
+
item_path + "/", extfilter, level + 1,
|
|
147
|
+
with_subfolders=with_subfolders)}
|
|
148
|
+
item_type = "folder"
|
|
149
|
+
else:
|
|
150
|
+
item_type = "file"
|
|
151
|
+
|
|
152
|
+
# Add item to dictionary
|
|
153
|
+
if File.extension(item) == extfilter or extfilter is None:
|
|
154
|
+
# Create FolderItem instance
|
|
155
|
+
items_dict[item_path] = FolderItem(
|
|
156
|
+
type=item_type,
|
|
157
|
+
name=item,
|
|
158
|
+
extension=File.extension(item),
|
|
159
|
+
level=level
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# TODO: also return a sorted list of the keys of the dictionary
|
|
163
|
+
|
|
164
|
+
return items_dict
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def copy_folder(source: str, target: str) -> None:
|
|
168
|
+
"""
|
|
169
|
+
Copies a folder and its contents to a new location.
|
|
170
|
+
|
|
171
|
+
Creates the target folder if it does not exist.
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
source : str
|
|
176
|
+
The path of the source folder to copy.
|
|
177
|
+
target : str
|
|
178
|
+
The destination path where the folder should be copied.
|
|
179
|
+
|
|
180
|
+
Notes
|
|
181
|
+
-----
|
|
182
|
+
TODO: Add functionality for moving or deleting folders.
|
|
183
|
+
"""
|
|
184
|
+
# Create target folder if it doesn't exist
|
|
185
|
+
Path(target).mkdir(parents=True, exist_ok=True)
|
|
186
|
+
|
|
187
|
+
# Copy each file
|
|
188
|
+
# TODO: Also copy sub folder (-> recursion)
|
|
189
|
+
for entry in Path(source).iterdir():
|
|
190
|
+
shutil.copyfile(entry, Path(target) / entry.name)
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def extension(file_name: str) -> str | None:
|
|
194
|
+
"""
|
|
195
|
+
Extracts and returns the file extension from a given file name.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
file_name : str
|
|
200
|
+
The name of the file (including extension).
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
-------
|
|
204
|
+
str or None
|
|
205
|
+
The file extension (without a leading dot) or None if the file has
|
|
206
|
+
no extension.
|
|
207
|
+
"""
|
|
208
|
+
# Dateiname anhand von . in Liste splitten
|
|
209
|
+
file_name_split = file_name.split(".")
|
|
210
|
+
count = len(file_name_split)
|
|
211
|
+
|
|
212
|
+
# Das letzte Element der Liste (= Dateiendung) zurückgeben
|
|
213
|
+
if count > 1:
|
|
214
|
+
return file_name.split(".")[count - 1]
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def change_extension(file_name: str, extension: str) -> str:
|
|
219
|
+
"""
|
|
220
|
+
Replaces the extension of a given file name with a new extension.
|
|
221
|
+
|
|
222
|
+
Parameters
|
|
223
|
+
----------
|
|
224
|
+
file_name : str
|
|
225
|
+
The original file name.
|
|
226
|
+
extension : str
|
|
227
|
+
The new file extension (without a leading dot).
|
|
228
|
+
|
|
229
|
+
Returns
|
|
230
|
+
-------
|
|
231
|
+
str
|
|
232
|
+
The file name with the updated extension.
|
|
233
|
+
"""
|
|
234
|
+
# Split file name into list by "."
|
|
235
|
+
file_name_split = file_name.split(".")
|
|
236
|
+
count = len(file_name_split)
|
|
237
|
+
|
|
238
|
+
# Replace the last element of the list (old extension) and replace it
|
|
239
|
+
# with the new extension
|
|
240
|
+
if count > 1:
|
|
241
|
+
file_name_split[count-1] = extension
|
|
242
|
+
file_name = ".".join(file_name_split)
|
|
243
|
+
|
|
244
|
+
return file_name
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
def path(file_path: str) -> str:
|
|
248
|
+
"""
|
|
249
|
+
Extracts and returns the folder path from a full file path.
|
|
250
|
+
|
|
251
|
+
Parameters
|
|
252
|
+
----------
|
|
253
|
+
file_path : str
|
|
254
|
+
The full file path.
|
|
255
|
+
|
|
256
|
+
Returns
|
|
257
|
+
-------
|
|
258
|
+
str
|
|
259
|
+
The folder path without the file name.
|
|
260
|
+
"""
|
|
261
|
+
file_path_list: list[str] = file_path.split("/")
|
|
262
|
+
file_path_list.remove(file_path_list[len(file_path_list) - 1])
|
|
263
|
+
return "/".join(file_path_list)
|
termcore/io/textfile.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reading and writing text files.
|
|
3
|
+
|
|
4
|
+
This module provides a convenient interface for working with text files,
|
|
5
|
+
allowing users to read entire file contents, retrieve lines as a list,
|
|
6
|
+
and write new content efficiently.
|
|
7
|
+
|
|
8
|
+
It is designed to simplify file operations by providing a class-based
|
|
9
|
+
approach for handling text files. It ensures proper file handling using context
|
|
10
|
+
managers and includes essential methods to perform common file operations.
|
|
11
|
+
|
|
12
|
+
Features:
|
|
13
|
+
|
|
14
|
+
- Read the entire content of a text file as a string.
|
|
15
|
+
- Read a text file line by line, returning a list of lines.
|
|
16
|
+
- Write new content to a text file, replacing any existing content.
|
|
17
|
+
- Uses UTF-8 encoding for compatibility with various text formats.
|
|
18
|
+
- Ensures proper file handling by automatically closing files after operations.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Textfile",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Textfile:
|
|
29
|
+
"""
|
|
30
|
+
Reads and writes text files.
|
|
31
|
+
|
|
32
|
+
This class provides methods to read the entire content of a text file,
|
|
33
|
+
read individual lines, and write new content to the file. It is designed for
|
|
34
|
+
simple file manipulation tasks and ensures that file streams are properly
|
|
35
|
+
handled using context managers.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def readlines(path: str) -> list[str]:
|
|
40
|
+
"""
|
|
41
|
+
Reads the text file and returns its lines as a list.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
path : str
|
|
46
|
+
The path to the text file that will be read.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
list[str]
|
|
51
|
+
A list containing all lines from the text file.
|
|
52
|
+
"""
|
|
53
|
+
with Path(path).open("r+", encoding="utf-8") as f:
|
|
54
|
+
return f.readlines()
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def read(path: str) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Reads the whole text file into a single string.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
path : str
|
|
64
|
+
The path to the text file that will be read.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
str
|
|
69
|
+
The complete content of the text file as a string.
|
|
70
|
+
"""
|
|
71
|
+
with Path(path).open("r+", encoding="utf-8") as f:
|
|
72
|
+
return f.read()
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def write(path: str, text: str) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Writes the given text to the file, overwriting any existing content.
|
|
78
|
+
|
|
79
|
+
The file is truncated before writing, ensuring that any previous content
|
|
80
|
+
is removed.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
path : str
|
|
85
|
+
The path to the text file that will be written.
|
|
86
|
+
text : str
|
|
87
|
+
Content of the text file.
|
|
88
|
+
"""
|
|
89
|
+
with Path(path).open("w", encoding="utf-8") as f:
|
|
90
|
+
f.seek(0) # Set stream to the beginning of the file
|
|
91
|
+
f.write("".join(text)) # Write text to file
|
|
92
|
+
f.truncate() # Remove old text
|
termcore/py.typed
ADDED
|
File without changes
|
termcore/tui/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Textual helpers: theme loading, key bindings and widgets."""
|
|
2
|
+
|
|
3
|
+
from .custom_bindings import *
|
|
4
|
+
from .custom_widgets.custom_checkbox import *
|
|
5
|
+
from .custom_widgets.custom_data_table import *
|
|
6
|
+
from .custom_widgets.custom_selection import *
|
|
7
|
+
from .custom_widgets.multiline_footer import *
|
|
8
|
+
from .question_screen import *
|
|
9
|
+
from .theme_loader import *
|