invesyservertools 0.1.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.
@@ -0,0 +1,26 @@
1
+ import importlib
2
+ from pathlib import Path
3
+
4
+ __all__ = []
5
+
6
+
7
+ def _load_tools():
8
+ """Re-export the public names of every ``*_tools.py`` submodule."""
9
+ package_dir = Path(__file__).parent
10
+ for file in sorted(package_dir.glob('*_tools.py')):
11
+ module = importlib.import_module(f'.{file.stem}', __package__)
12
+ names = getattr(
13
+ module,
14
+ '__all__',
15
+ [name for name in dir(module) if not name.startswith('_')]
16
+ )
17
+ for name in names:
18
+ if name in __all__:
19
+ raise RuntimeError(
20
+ f"duplicate export {name!r} from {file.stem}"
21
+ )
22
+ globals()[name] = getattr(module, name)
23
+ __all__.append(name)
24
+
25
+
26
+ _load_tools()
@@ -0,0 +1,74 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ =================
4
+ interactive_tools
5
+ =================
6
+ """
7
+
8
+ import sys
9
+ import warnings
10
+ from typing import Optional, Union
11
+
12
+ try:
13
+ import termios
14
+ except ImportError: # termios is POSIX-only (absent on Windows)
15
+ termios = None
16
+
17
+ from .print_tools import print_
18
+
19
+ __all__ = ['wait_for_key']
20
+
21
+
22
+ def wait_for_key(
23
+ text: str = 'continue: press any button',
24
+ list_bullet: Union[bool, str] = False,
25
+ color: str = None,
26
+ indent: int = 0
27
+ ) -> Optional[str]:
28
+ ''' Wait for a key press on the console and return it.'''
29
+
30
+ if termios is None:
31
+ raise RuntimeError(
32
+ 'wait_for_key requires a POSIX terminal '
33
+ '(the termios module is unavailable on this platform)'
34
+ )
35
+
36
+ if text:
37
+ print_(
38
+ text,
39
+ color=color,
40
+ list_bullet=list_bullet,
41
+ indent=indent
42
+ )
43
+
44
+ # without a real terminal we cannot switch to raw mode; read a single
45
+ # character (or return None at EOF) instead of crashing in termios
46
+ if not sys.stdin.isatty():
47
+ try:
48
+ return sys.stdin.read(1) or None
49
+ except OSError:
50
+ warnings.warn("wait_for_key: failed to read from stdin")
51
+ return None
52
+
53
+ fd = sys.stdin.fileno()
54
+ result = None
55
+
56
+ try:
57
+ oldterm = termios.tcgetattr(fd)
58
+ newattr = termios.tcgetattr(fd)
59
+ newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
60
+ termios.tcsetattr(fd, termios.TCSANOW, newattr)
61
+ except termios.error:
62
+ warnings.warn("wait_for_key: failed to configure the terminal")
63
+ return None
64
+
65
+ try:
66
+ result = sys.stdin.read(1) or None
67
+ except OSError:
68
+ warnings.warn("wait_for_key: failed to read from stdin")
69
+ finally:
70
+ # only reached when the terminal was reconfigured successfully,
71
+ # so oldterm is always bound here
72
+ termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm)
73
+
74
+ return result
@@ -0,0 +1,260 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ ===========
4
+ print_tools
5
+ ===========
6
+ """
7
+
8
+ import logging as _logging
9
+ import os
10
+ import sys
11
+ import textwrap
12
+ from typing import Dict, Optional, Union
13
+
14
+ from colorama import Fore, Style
15
+
16
+ LIST_BULLET = '-'
17
+ COLOR_RESET = getattr(Style, 'RESET_ALL')
18
+
19
+ INDENT_MULT = 4
20
+
21
+ UNIT_PREFIXES = {
22
+ 'binary': ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'],
23
+ 'decimal': ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
24
+ }
25
+
26
+ __all__ = ['and_list', 'indent', 'print_', 'format_filesize']
27
+
28
+
29
+ def and_list(
30
+ elements: list,
31
+ et: str = 'and'
32
+ ) -> str:
33
+ """Create a human-readable list, joining elements with commas and a final conjunction.
34
+
35
+ :param elements: items to join
36
+ :param et: conjunction before the last element (e.g. ``'and'``, ``'und'``)
37
+ :return: the formatted string
38
+ """
39
+ elements = [str(el) for el in elements]
40
+ if len(elements) <= 1:
41
+ return ''.join(elements)
42
+ return ', '.join(elements[:-1]) + f' {et} ' + elements[-1]
43
+
44
+
45
+ def indent(
46
+ level: int = 1,
47
+ raw: bool = False,
48
+ base: int = INDENT_MULT,
49
+ base_str: str = ' '
50
+ ) -> str:
51
+ """Build an indentation string of *level* steps.
52
+
53
+ :param level: number of indentation steps
54
+ :param raw: if ``True`` return the full width; otherwise trim one
55
+ character (see below)
56
+ :param base: width of one indentation step
57
+ :param base_str: character the indentation is filled with
58
+ :return: the indentation string
59
+ """
60
+ base = base * base_str
61
+ if raw:
62
+ return base * level
63
+ else:
64
+ # Trim the last character so that appending a single-character marker
65
+ # — e.g. the bullet from print_(..., list_bullet=True) — places that
66
+ # marker exactly on the base-column grid (columns base, 2*base, …;
67
+ # with the defaults: 4, 8, …). Use raw=True when you add no marker.
68
+ return (base * level)[:-1]
69
+
70
+
71
+ def print_(
72
+ *texts: str,
73
+ logging: Optional[_logging.Logger] = None,
74
+ indent: int = 0,
75
+ list_bullet: Union[bool, str] = '',
76
+ color: Union[str, Dict[int, str]] = '',
77
+ style: str = '',
78
+ sep: str = ' ',
79
+ end: str = '\n',
80
+ ignore: bool = False,
81
+ flush: bool = None,
82
+ nowrapper: bool = None
83
+ ) -> None:
84
+ """Enhanced print function.
85
+
86
+ The text arguments can contain ``'\\n'``, either as a
87
+ singleton or as part of a string argument.
88
+
89
+ :param texts: the texts to print
90
+ :param logging: provide a logging object to log the output
91
+ :param indent: set the indentation level
92
+ :param list_bullet: ``True`` for the default bullet or provide a string
93
+ :param color: a color name or a dict mapping 1-based text indices to
94
+ color names, e.g. ``{1: 'red', 4: 'blue'}``.
95
+ Strings containing only line endings don't count for the index.
96
+ :param style: a colorama style name
97
+ :param sep: separator between texts (as in :func:`print`)
98
+ :param end: end string (as in :func:`print`)
99
+ :param ignore: don't print (but possibly log)
100
+ :param flush: flush the output (as in :func:`print`)
101
+ :param nowrapper: disable textwrap
102
+ """
103
+
104
+ # are we outputting to a tty?
105
+ output_to_tty = sys.stdout.isatty()
106
+
107
+ if nowrapper is None:
108
+ nowrapper = not output_to_tty
109
+ if flush is None:
110
+ flush = not output_to_tty
111
+
112
+ # wrapping needs a known terminal width, available only on a tty
113
+ if not output_to_tty:
114
+ nowrapper = True
115
+
116
+ texts = [str(t) for t in texts]
117
+
118
+ # we have line endings
119
+ if any(
120
+ ['\n' in t for t in texts]
121
+ ):
122
+ for i, t in enumerate(texts):
123
+ if t.endswith('\n'):
124
+ try:
125
+ texts[i + 1] = '\n' + texts[i + 1] # update next string by adding line end
126
+ texts[i] = t[:-1] # remove line end from current string
127
+ except IndexError:
128
+ pass
129
+
130
+ nowrapper = True
131
+
132
+ texts = list(filter(None, texts)) # remove empty strings
133
+
134
+ if logging:
135
+ logging.info(sep.join(texts))
136
+
137
+ if ignore:
138
+ return
139
+
140
+ colors = None
141
+
142
+ if isinstance(color, dict):
143
+ colors = color
144
+ color = None
145
+
146
+ # only use colors/styles if stdout is a tty
147
+ if not output_to_tty:
148
+ colors = {}
149
+ color = None
150
+ style = ''
151
+
152
+ if len(texts):
153
+ try:
154
+ prefix = ' ' * indent
155
+ except TypeError:
156
+ prefix = indent or ''
157
+
158
+ # visible width of the prefix, ignoring invisible color escapes
159
+ visible_prefix_len = len(prefix)
160
+
161
+ suffix = ''
162
+ if color or style:
163
+ prefix += getattr(
164
+ Fore,
165
+ (color or '').upper(), ''
166
+ ) + getattr(
167
+ Style, (style or '').upper(),
168
+ ''
169
+ )
170
+ suffix = COLOR_RESET
171
+
172
+ if colors:
173
+ texts_colored = []
174
+ # line-ending-only arguments don't consume a color index
175
+ color_index = 0
176
+ for t in texts:
177
+ p = s = ''
178
+ if t.strip('\n'):
179
+ color_index += 1
180
+ if color_index in colors:
181
+ p = getattr(Fore, colors[color_index].upper(), '')
182
+ s = COLOR_RESET if p else ''
183
+
184
+ texts_colored.append(p + t + s)
185
+
186
+ texts = texts_colored
187
+
188
+ if isinstance(list_bullet, bool):
189
+ if list_bullet:
190
+ list_bullet = LIST_BULLET
191
+ else:
192
+ list_bullet = ''
193
+
194
+ bullet_sep = ' '
195
+ if list_bullet:
196
+ prefix += list_bullet + bullet_sep
197
+ visible_prefix_len += len(list_bullet) + len(bullet_sep)
198
+
199
+ if output_to_tty:
200
+ try:
201
+ term_columns = os.get_terminal_size().columns
202
+ wrapper = textwrap.TextWrapper(
203
+ initial_indent=prefix,
204
+ width=term_columns,
205
+ subsequent_indent=' ' * visible_prefix_len
206
+ )
207
+ except OSError:
208
+ nowrapper = True
209
+
210
+ if flush or nowrapper:
211
+ texts[-1] += suffix
212
+ print(
213
+ prefix + texts[0],
214
+ *texts[1:],
215
+ sep=sep,
216
+ end=end,
217
+ flush=flush
218
+ )
219
+ else:
220
+ print(wrapper.fill(sep.join(texts) + suffix), end=end)
221
+
222
+ else:
223
+ print()
224
+
225
+
226
+ def format_filesize(
227
+ num: Union[int, float],
228
+ suffix: str = "B",
229
+ prefix_type: str = 'binary'
230
+ ) -> str:
231
+ """Format a file size as a human-readable string.
232
+
233
+ Inspired by `Fred Cirera <https://stackoverflow.com/a/1094933/1690805>`_.
234
+
235
+ :param num: size in bytes
236
+ :param suffix: unit suffix (default ``'B'``)
237
+ :param prefix_type: ``'binary'`` for IEC prefixes (Ki, Mi, …) or
238
+ ``'decimal'`` for SI prefixes (K, M, …)
239
+ :raises ValueError: if *prefix_type* is not ``'binary'`` or ``'decimal'``
240
+ :return: the formatted size string
241
+ """
242
+ if prefix_type not in UNIT_PREFIXES:
243
+ raise ValueError(
244
+ f"prefix_type must be one of {sorted(UNIT_PREFIXES)}, "
245
+ f"got {prefix_type!r}"
246
+ )
247
+
248
+ prefixes = UNIT_PREFIXES[prefix_type]
249
+ divider = 1024.0 if prefix_type == 'binary' else 1000.0
250
+
251
+ # stop before the largest prefix so it can serve as the overflow cap;
252
+ # dividing past it would underreport values above the largest unit
253
+ for unit in prefixes[:-1]:
254
+ if abs(num) < divider:
255
+ return f"{num:3.1f} {unit}{suffix}"
256
+
257
+ num /= divider
258
+
259
+ # value reaches or exceeds the largest defined prefix
260
+ return f"{num:3.1f} {prefixes[-1]}{suffix}"
@@ -0,0 +1,78 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ ============
4
+ system_tools
5
+ ============
6
+ """
7
+
8
+ import os
9
+ import warnings
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ try:
14
+ import pwd
15
+ except ImportError: # pwd is POSIX-only (absent on Windows)
16
+ pwd = None
17
+
18
+ from .print_tools import print_
19
+
20
+ __all__ = ['get_username', 'get_home_dir', 'find_file_path']
21
+
22
+
23
+ def get_username() -> str:
24
+ """Return the login name of the current user."""
25
+ if pwd is None:
26
+ raise RuntimeError(
27
+ 'get_username requires a POSIX platform '
28
+ '(the pwd module is unavailable on this platform)'
29
+ )
30
+ return pwd.getpwuid(os.getuid())[0]
31
+
32
+
33
+ def get_home_dir() -> str:
34
+ """Return the current user's home directory."""
35
+ return str(Path.home())
36
+
37
+
38
+ def find_file_path(
39
+ filename: str,
40
+ rootpath: str,
41
+ path_only: bool = False,
42
+ verbose=False
43
+ ) -> Optional[str]:
44
+ """Find the complete path for a file.
45
+
46
+ Walks the directory tree starting at *rootpath* and returns the
47
+ full path to the first file matching *filename*.
48
+
49
+ Unreadable subdirectories are skipped with a warning rather than
50
+ aborting the search, so a ``None`` result does not guarantee the file
51
+ is absent from an inaccessible subtree.
52
+
53
+ :param filename: name of the file to search for
54
+ :param rootpath: root directory to start the search (``~`` is expanded)
55
+ :param path_only: if ``True``, return only the containing directory
56
+ :param verbose: if ``True``, print each directory as it is visited
57
+ :return: the file path, the containing directory, or ``None`` if the
58
+ file is not found
59
+ """
60
+
61
+ # expand ~ and ~user to the corresponding home directory
62
+ rootpath = os.path.expanduser(rootpath)
63
+
64
+ def _on_error(error):
65
+ warnings.warn(
66
+ f"find_file_path: cannot access {error.filename!r}: {error}"
67
+ )
68
+
69
+ for root, dirs, files in os.walk(rootpath, onerror=_on_error):
70
+ if verbose:
71
+ print_(root)
72
+ for name in files:
73
+ if name == filename:
74
+ if path_only:
75
+ return root
76
+ return os.path.join(root, filename)
77
+
78
+ return None
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: invesyservertools
3
+ Version: 0.1.0
4
+ Summary: Tools for Python scripts or terminal
5
+ Author-email: Georg Pfolz <georg.pfolz@invesy.at>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://gitlab.com/Rastaf/invesyservertools
8
+ Project-URL: Bug Tracker, https://gitlab.com/Rastaf/invesyservertools/-/issues
9
+ Project-URL: Documentation, https://rastaf.gitlab.io/invesyservertools/
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: POSIX
12
+ Classifier: Operating System :: MacOS
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE.txt
16
+ Requires-Dist: colorama
17
+ Provides-Extra: test
18
+ Requires-Dist: pytest; extra == "test"
19
+ Dynamic: license-file
20
+
21
+ # invesyservertools
22
+
23
+ A small set of useful tools on a server, especially for terminal scripts.
24
+
25
+ Why "invesy"? Invesy (from German **In**halts**ve**rwaltungs**sy**stem == content management system) is a closed source cms I created with Thomas Macher. It's only used in-house, that's why we didn't bother making it open source.
26
+
27
+ ## Installation
28
+
29
+ ```sh
30
+ pip install invesyservertools
31
+ ```
32
+
33
+ Requires Python >= 3.9 on a POSIX platform (Linux, *BSD, macOS — uses the Unix-only modules `termios` and `pwd`).
34
+
35
+ ## Quick example
36
+
37
+ ```python
38
+ from invesyservertools import print_, format_filesize, wait_for_key
39
+
40
+ print_('Hello', 'World', color={1: 'green'}, sep=' ')
41
+ print(format_filesize(234567890)) # '223.7 MiB'
42
+ wait_for_key()
43
+ ```
44
+
45
+ ## What's in the box?
46
+
47
+ ### interactive_tools
48
+ - **wait_for_key**: Wait for a pressed key to continue.
49
+
50
+ ### print_tools
51
+ - **and_list**: human-readable list like `a, 1, 2 and something`
52
+ - **indent**: get an indentation string
53
+ - **print_**: Enhanced print function, specially enhanced for printing in the terminal and into log files.
54
+ - **format_filesize**: return a human-readable filesize, like `31.9 GiB`.
55
+
56
+ ### system_tools
57
+ - **get_username**: get the username in the operating system
58
+ - **get_home_dir**: get the home directory
59
+ - **find_file_path**: provide a filename and a start path to begin searching recursively, get the complete path including the filename
60
+
61
+ ## Documentation
62
+
63
+ Full API docs: https://rastaf.gitlab.io/invesyservertools/
64
+
65
+ ## License
66
+
67
+ MIT
68
+
69
+ # History
70
+ ## 0.1.0
71
+ - Packaging modernized: built from `pyproject.toml`, tests run under `pytest`
72
+ - **Breaking:** requires Python >= 3.9 (was 3.7)
73
+ - **Breaking:** `find_file_path()` returns `None` when nothing is found (was a message string); the `not_found` argument was removed
74
+ - Targets POSIX (Linux, *BSD, macOS) but now imports cleanly everywhere — the POSIX-only `wait_for_key` and `get_username` raise a clear error on non-POSIX instead of breaking `import invesyservertools`
75
+ - **and_list**: keeps the conjunction for multi-word or punctuated final elements
76
+ - **print_**: several correctness fixes (terminal-size detection, `end`/separator handling, colour indexing and wrapped-line indentation, `style=` off a tty)
77
+ - **format_filesize**: correct SI (decimal) thresholds and large-value handling; clear error on an invalid `prefix_type`
78
+ - **wait_for_key**: robust on non-tty stdin (no crash; returns `None` at EOF)
79
+
80
+ ## 0.0.10 (2023-07-13)
81
+ - bumped version to 0.0.10
82
+ - replaced dynamic package imports using `exec()` and `sys.path` mutation with `importlib`
83
+
84
+ ## 0.0.9 (2023-06-10)
85
+ - fixed IndexError in **print_** (for line ending in last string)
86
+ - **print_** checks for terminal (-> no colors, no wrapper)
87
+
88
+ ## 0.0.5 (2022-12-09)
89
+ - fixed import of submodules
90
+
91
+ ## 0.0.4 (2022-12-09)
92
+ - *wait_for_key* gets a "indent" argument
93
+ - Sphinx documentation
94
+
95
+ ## 0.0.3 (2022-06-16)
96
+ Added support for line breaks in *print_*:
97
+
98
+ - can be added as singletons or as part of strings
99
+ - line break singletons are not counted in the color argument
100
+
101
+ ## 0.0.2 (2022-06-13)
102
+ * splitted modules (now: *interactive_tools*, *print_tools*, *system_tools*)
103
+ * added new functions:
104
+ * system_tools
105
+ * get_username
106
+ * get\_home\_dir
107
+ * find\_file\_path
108
+ * print_tools
109
+ * and_list
110
+ * format_filesize
111
+
112
+ ## 0.0.1 (2022-06-10)
113
+ * first version
@@ -0,0 +1,9 @@
1
+ invesyservertools/__init__.py,sha256=aeEZB3l4AhPmCS1dbDbS27N8_5g7r9bc6iB0zdmYht8,745
2
+ invesyservertools/interactive_tools.py,sha256=w5Vx7wj9lrSQpmWIPGfJ_Ya_Z9rXNZ78QqOqYzwoiXw,1954
3
+ invesyservertools/print_tools.py,sha256=HF8vFZhrT0LH2PAKhdeHFglGQrqsAW9-kosXGROFoYI,7598
4
+ invesyservertools/system_tools.py,sha256=0EiWqlTtotSziXr9SmGETePNnSR8vtp1NMCmNVxeG3g,2150
5
+ invesyservertools-0.1.0.dist-info/licenses/LICENSE.txt,sha256=-vEbVpKDEALxD3QtS7yMJ4T9uRB_AbN9aQey6X941Ws,1050
6
+ invesyservertools-0.1.0.dist-info/METADATA,sha256=3Ga32YQdPNdvJ2K5uFaUm2CEQIzOfya_mtb7Ye8BCB8,4015
7
+ invesyservertools-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ invesyservertools-0.1.0.dist-info/top_level.txt,sha256=goITXYiS_8Th-GRPKmgp9P0erFrNVyKbTFTluN0bL9Y,18
9
+ invesyservertools-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,7 @@
1
+ Copyright 2022 Georg Pfolz
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
1
+ invesyservertools