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.
Files changed (55) hide show
  1. termcore/__init__.py +6 -0
  2. termcore/cli/__init__.py +3 -0
  3. termcore/cli/output.py +70 -0
  4. termcore/io/__init__.py +7 -0
  5. termcore/io/app_state_storage.py +185 -0
  6. termcore/io/database.py +655 -0
  7. termcore/io/errors.py +71 -0
  8. termcore/io/file.py +263 -0
  9. termcore/io/textfile.py +92 -0
  10. termcore/py.typed +0 -0
  11. termcore/tui/__init__.py +9 -0
  12. termcore/tui/custom_bindings.py +551 -0
  13. termcore/tui/custom_widgets/__init__.py +1 -0
  14. termcore/tui/custom_widgets/custom_checkbox.py +41 -0
  15. termcore/tui/custom_widgets/custom_data_table.py +247 -0
  16. termcore/tui/custom_widgets/custom_selection.py +80 -0
  17. termcore/tui/custom_widgets/multiline_footer.py +240 -0
  18. termcore/tui/question_screen.py +107 -0
  19. termcore/tui/theme_loader.py +495 -0
  20. termcore/tui/themes/__init__.py +2 -0
  21. termcore/tui/themes/classic-black-saturated/style.css +57 -0
  22. termcore/tui/themes/classic-black-saturated/theme.py +22 -0
  23. termcore/tui/themes/classic-black-v1/style.css +57 -0
  24. termcore/tui/themes/classic-black-v1/theme.py +22 -0
  25. termcore/tui/themes/classic-black-v2/style.css +57 -0
  26. termcore/tui/themes/classic-black-v2/theme.py +24 -0
  27. termcore/tui/themes/classic-blue/theme.py +21 -0
  28. termcore/tui/themes/compact-gray/style.tcss +175 -0
  29. termcore/tui/themes/compact-gray/theme.py +22 -0
  30. termcore/tui/themes/mnml-black/style.css +57 -0
  31. termcore/tui/themes/mnml-black/theme.py +21 -0
  32. termcore/tui/themes/mnml-deepblack/style.css +57 -0
  33. termcore/tui/themes/mnml-deepblack/theme.py +21 -0
  34. termcore/tui/themes/pure-amber/theme.py +18 -0
  35. termcore/tui/themes/pure-black/theme.py +18 -0
  36. termcore/tui/themes/pure-blue/theme.py +18 -0
  37. termcore/tui/themes/pure-green/theme.py +18 -0
  38. termcore/tui/themes/pure-sweet16/theme.py +20 -0
  39. termcore/tui/themes/xplore-black/theme.py +18 -0
  40. termcore/tui/themes/xplore-blue/theme.py +18 -0
  41. termcore/tui/themes/xplore-blue-muted/theme.py +18 -0
  42. termcore/tui/themes/xplore-teal/theme.py +18 -0
  43. termcore/util/__init__.py +10 -0
  44. termcore/util/datetime.py +173 -0
  45. termcore/util/debug.py +196 -0
  46. termcore/util/index.py +112 -0
  47. termcore/util/logger.py +36 -0
  48. termcore/util/singleton.py +46 -0
  49. termcore/util/string.py +263 -0
  50. termcore/util/validation.py +31 -0
  51. termcore/util/version.py +56 -0
  52. termcore-0.2.0.dist-info/METADATA +504 -0
  53. termcore-0.2.0.dist-info/RECORD +55 -0
  54. termcore-0.2.0.dist-info/WHEEL +4 -0
  55. 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)
@@ -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
@@ -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 *