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
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Converting between UNIX timestamps and date strings.
|
|
3
|
+
|
|
4
|
+
This module offers convenient static methods for working with dates and times,
|
|
5
|
+
including:
|
|
6
|
+
|
|
7
|
+
- Converting UNIX timestamps to formatted date strings and vice versa.
|
|
8
|
+
- Calculating the number of days between two timestamps.
|
|
9
|
+
- Retrieving today's date or timestamp at midnight.
|
|
10
|
+
|
|
11
|
+
It supports both German-style date formatting ("DD.MM.YYYY") and ISO-style
|
|
12
|
+
formatting ("YYYY-MM-DD") for flexibility in various locales, selected through
|
|
13
|
+
the `DateFormat` enum.
|
|
14
|
+
|
|
15
|
+
Every conversion is anchored in the machine's local time zone, and says so:
|
|
16
|
+
the datetimes built here carry their offset instead of leaving it implied. A
|
|
17
|
+
naive timestamp changes meaning with the daylight saving switch, with the
|
|
18
|
+
machine and on serialisation, and the error surfaces months later in wrongly
|
|
19
|
+
sorted or wrongly calculated data rather than at the call site.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from datetime import UTC, datetime, time
|
|
24
|
+
from enum import Enum
|
|
25
|
+
|
|
26
|
+
# Without this, `from .datetime import *` in the package __init__ re-exports
|
|
27
|
+
# the imported `datetime` class, which then shadows this very module: after
|
|
28
|
+
# importing termcore, `termcore.util.datetime` is the class, not the module.
|
|
29
|
+
__all__ = [
|
|
30
|
+
"DateFormat",
|
|
31
|
+
"date_diff",
|
|
32
|
+
"date_to_timestamp",
|
|
33
|
+
"timestamp_to_date",
|
|
34
|
+
"today_date",
|
|
35
|
+
"today_timestamp",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DateFormat(Enum):
|
|
40
|
+
"""
|
|
41
|
+
The date layouts these functions read and write.
|
|
42
|
+
|
|
43
|
+
Values are `strftime` patterns, so they can be handed to `strftime` and
|
|
44
|
+
`strptime` directly.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
GERMAN = "%d.%m.%Y"
|
|
48
|
+
ISO = "%Y-%m-%d"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def timestamp_to_date(
|
|
52
|
+
timestamp: int | None, date_format: DateFormat = DateFormat.GERMAN
|
|
53
|
+
) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Converts a UNIX timestamp into a date string.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
timestamp : int or None
|
|
60
|
+
UNIX timestamp.
|
|
61
|
+
date_format : DateFormat, optional
|
|
62
|
+
Layout of the returned string, German by default.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
str
|
|
67
|
+
The date as seen in the local time zone. Empty string if the given
|
|
68
|
+
timestamp is None.
|
|
69
|
+
"""
|
|
70
|
+
# Return empty string if the given timestamp is None
|
|
71
|
+
if timestamp is None:
|
|
72
|
+
return ""
|
|
73
|
+
|
|
74
|
+
# Convert timestamp to a date in the local zone
|
|
75
|
+
date_obj = datetime.fromtimestamp(timestamp, tz=UTC).astimezone()
|
|
76
|
+
|
|
77
|
+
return date_obj.strftime(date_format.value)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def date_to_timestamp(
|
|
81
|
+
date_str: str, date_format: DateFormat = DateFormat.GERMAN
|
|
82
|
+
) -> int | None:
|
|
83
|
+
"""
|
|
84
|
+
Converts a date string into a UNIX timestamp.
|
|
85
|
+
|
|
86
|
+
The date is read as local midnight.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
date_str : str
|
|
91
|
+
The date, laid out as `date_format` describes.
|
|
92
|
+
date_format : DateFormat, optional
|
|
93
|
+
Layout the string is expected in, German by default.
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
int or None
|
|
98
|
+
Unix timestamp (number of seconds since 1970-01-01), or None if the
|
|
99
|
+
string does not match the expected layout.
|
|
100
|
+
"""
|
|
101
|
+
# Check if the date string matches the expected format
|
|
102
|
+
try:
|
|
103
|
+
date_obj = datetime.strptime(date_str, date_format.value) # noqa: DTZ007
|
|
104
|
+
except ValueError:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
# astimezone() reads a naive value as local time and attaches the matching
|
|
108
|
+
# offset - the assumption this module makes explicit rather than inherits
|
|
109
|
+
return int(date_obj.astimezone().timestamp())
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def date_diff(timestamp1: int, timestamp2: int) -> int:
|
|
113
|
+
"""
|
|
114
|
+
Calculates the number of calendar days between two timestamps.
|
|
115
|
+
|
|
116
|
+
Counted as local calendar dates rather than as 86400-second blocks, so
|
|
117
|
+
the time of day does not matter and a span crossing a daylight saving
|
|
118
|
+
switch is not off by one. Two moments on the same local day are 0 days
|
|
119
|
+
apart, whether they are a second or twenty hours apart.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
timestamp1 : int
|
|
124
|
+
UNIX time stamp 1.
|
|
125
|
+
timestamp2 : int
|
|
126
|
+
UNIX time stamp 2.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
int
|
|
131
|
+
Number of days between the given timestamps, negative when the
|
|
132
|
+
second one is the later of the two.
|
|
133
|
+
"""
|
|
134
|
+
first = datetime.fromtimestamp(timestamp1, tz=UTC).astimezone().date()
|
|
135
|
+
second = datetime.fromtimestamp(timestamp2, tz=UTC).astimezone().date()
|
|
136
|
+
|
|
137
|
+
return (first - second).days
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def today_timestamp() -> int:
|
|
141
|
+
"""
|
|
142
|
+
Returns the time stamp of today for the time 00:00 h.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
int
|
|
147
|
+
UNIX timestamp of local midnight.
|
|
148
|
+
"""
|
|
149
|
+
# Date of today in the local zone
|
|
150
|
+
today = datetime.now(UTC).astimezone().date()
|
|
151
|
+
|
|
152
|
+
# Combine with time 00:00 and anchor it in the local zone
|
|
153
|
+
midnight = datetime.combine(today, time.min).astimezone()
|
|
154
|
+
|
|
155
|
+
# Return unix timestamp
|
|
156
|
+
return int(midnight.timestamp())
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def today_date(date_format: DateFormat = DateFormat.GERMAN) -> str:
|
|
160
|
+
"""
|
|
161
|
+
Returns the date of today as a string.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
date_format : DateFormat, optional
|
|
166
|
+
Layout of the returned string, German by default.
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
str
|
|
171
|
+
Today's date in the local time zone.
|
|
172
|
+
"""
|
|
173
|
+
return timestamp_to_date(today_timestamp(), date_format)
|
termcore/util/debug.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decorators for inspecting calls and measuring execution time.
|
|
3
|
+
|
|
4
|
+
This module offers decorators that can be used to:
|
|
5
|
+
|
|
6
|
+
- Log function calls, including arguments, keyword arguments and return values.
|
|
7
|
+
- Measure and display execution time of functions, with optional nanosecond
|
|
8
|
+
precision.
|
|
9
|
+
|
|
10
|
+
These utilities are useful for tracking function behavior and identifying
|
|
11
|
+
performance bottlenecks during runtime, without requiring changes to the
|
|
12
|
+
function logic itself.
|
|
13
|
+
|
|
14
|
+
Examples
|
|
15
|
+
--------
|
|
16
|
+
Printing arguments
|
|
17
|
+
~~~~~~~~~~~~~~~~~~
|
|
18
|
+
|
|
19
|
+
>>> @print_arguments
|
|
20
|
+
... def do_something(t: str, n: int) -> None:
|
|
21
|
+
... print(f'I am not doing anything with string "{t}" and number {str(n)}.')
|
|
22
|
+
...
|
|
23
|
+
>>> do_something('ABC', 11)
|
|
24
|
+
|
|
25
|
+
Measuring execution time of a function
|
|
26
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
27
|
+
|
|
28
|
+
>>> @timing()
|
|
29
|
+
... def iterate_something() -> None:
|
|
30
|
+
... v: int = 0
|
|
31
|
+
... for i in range(10**7):
|
|
32
|
+
... v += 1
|
|
33
|
+
...
|
|
34
|
+
>>> iterate_something()
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import logging
|
|
39
|
+
import time
|
|
40
|
+
from collections.abc import Callable
|
|
41
|
+
from functools import wraps
|
|
42
|
+
from typing import ParamSpec, TypeVar
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"print_arguments",
|
|
46
|
+
"timing",
|
|
47
|
+
"timing_ns",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
_logger = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
P = ParamSpec("P")
|
|
53
|
+
R = TypeVar("R")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def print_arguments[**P, R](fn: Callable[P, R]) -> Callable[P, R]:
|
|
57
|
+
"""
|
|
58
|
+
Logs name, arguments and return value on every call.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
fn : Callable
|
|
63
|
+
The function to be decorated.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
Callable
|
|
68
|
+
A wrapper function that adds debugging output.
|
|
69
|
+
"""
|
|
70
|
+
@wraps(fn) # Without this fn.__name__ would be empty
|
|
71
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
72
|
+
"""
|
|
73
|
+
Wrapper function that prints the function's arguments and return value.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
*args : P.args
|
|
78
|
+
Positional arguments.
|
|
79
|
+
**kwargs : P.kwargs
|
|
80
|
+
Keyword arguments.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
R
|
|
85
|
+
The original return value of the decorated function.
|
|
86
|
+
"""
|
|
87
|
+
# Report the call through the logger, so a consumer can filter
|
|
88
|
+
# or silence it like any other diagnostic
|
|
89
|
+
_logger.debug("Function %s called", fn.__name__)
|
|
90
|
+
_logger.debug("Args: %r", args)
|
|
91
|
+
_logger.debug("Kwargs: %r", kwargs)
|
|
92
|
+
|
|
93
|
+
# Call function
|
|
94
|
+
fn_result = fn(*args, **kwargs)
|
|
95
|
+
|
|
96
|
+
_logger.debug(
|
|
97
|
+
"Function %s returns: %r", fn.__name__, fn_result
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return fn_result
|
|
101
|
+
|
|
102
|
+
return wrapper
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def timing() -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
106
|
+
"""
|
|
107
|
+
A decorator factory that measures the execution time in seconds.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
Callable
|
|
112
|
+
A decorator that wraps the target function with timing logic.
|
|
113
|
+
"""
|
|
114
|
+
return _timing(time.perf_counter, "s")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def timing_ns() -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
118
|
+
"""
|
|
119
|
+
A decorator factory that measures the execution time in nanoseconds.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
Callable
|
|
124
|
+
A decorator that wraps the target function with timing logic.
|
|
125
|
+
"""
|
|
126
|
+
return _timing(time.perf_counter_ns, "ns")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _timing(
|
|
130
|
+
time_fn: Callable[[], float], time_scale: str
|
|
131
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
132
|
+
"""
|
|
133
|
+
Builds the timing decorator around the given clock.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
time_fn : Callable
|
|
138
|
+
The clock to read, in the unit `time_scale` names.
|
|
139
|
+
time_scale : str
|
|
140
|
+
Unit suffix written to the log.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
Callable
|
|
145
|
+
A decorator that wraps the target function with timing logic.
|
|
146
|
+
"""
|
|
147
|
+
def wrap_with_timing(fn: Callable[P, R]) -> Callable[P, R]:
|
|
148
|
+
"""
|
|
149
|
+
Wraps the function so its execution time is measured.
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
fn : Callable
|
|
154
|
+
Reference to the function.
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
Callable
|
|
159
|
+
A wrapped version of the function with timing logic.
|
|
160
|
+
"""
|
|
161
|
+
@wraps(fn)
|
|
162
|
+
def timer(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
163
|
+
"""
|
|
164
|
+
Measures the execution time of the function call.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
*args : P.args
|
|
169
|
+
Positional arguments.
|
|
170
|
+
**kwargs : P.kwargs
|
|
171
|
+
Keyword arguments.
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
R
|
|
176
|
+
The original return value of the decorated function.
|
|
177
|
+
"""
|
|
178
|
+
# Store start time
|
|
179
|
+
start_time = time_fn()
|
|
180
|
+
|
|
181
|
+
# Call function
|
|
182
|
+
fn_result = fn(*args, **kwargs)
|
|
183
|
+
|
|
184
|
+
# Store end time + calculate and print execution time
|
|
185
|
+
end_time = time_fn()
|
|
186
|
+
duration = end_time - start_time
|
|
187
|
+
_logger.debug(
|
|
188
|
+
"Function %s took: %s %s",
|
|
189
|
+
fn.__name__, duration, time_scale
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return fn_result
|
|
193
|
+
|
|
194
|
+
return timer
|
|
195
|
+
|
|
196
|
+
return wrap_with_timing
|
termcore/util/index.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Index arithmetic for navigating lists.
|
|
3
|
+
|
|
4
|
+
In particular the cyclic movement selection lists in a terminal user
|
|
5
|
+
interface are expected to have.
|
|
6
|
+
|
|
7
|
+
Two functions rather than one with a switch: `next_index` wraps around at
|
|
8
|
+
both ends, `clamped_index` stops at them. Which one a view wants is a
|
|
9
|
+
property of that view, and naming it at the call site says so.
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"clamped_index",
|
|
17
|
+
"next_index",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def next_index(current_index: int, length: int, direction: int = 1) -> int:
|
|
22
|
+
"""
|
|
23
|
+
Calculates the next index in a list, wrapping around at both ends.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
current_index : int
|
|
28
|
+
The current index in the list.
|
|
29
|
+
length : int
|
|
30
|
+
The number of items in the list.
|
|
31
|
+
direction : int, optional
|
|
32
|
+
The direction and size of the movement (1 for one step forward,
|
|
33
|
+
-1 for one step backward).
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
int
|
|
38
|
+
The calculated next index. An empty list always yields 0.
|
|
39
|
+
|
|
40
|
+
Notes
|
|
41
|
+
-----
|
|
42
|
+
The expression `(current_index + direction) % length` keeps the index
|
|
43
|
+
within the valid range from `0` to `length - 1`. The modulo operator
|
|
44
|
+
makes it "wrap around":
|
|
45
|
+
|
|
46
|
+
- If the index moves past the end of the list, it wraps back
|
|
47
|
+
to 0.
|
|
48
|
+
- If the index moves before the beginning, it wraps to the
|
|
49
|
+
last position.
|
|
50
|
+
|
|
51
|
+
Examples
|
|
52
|
+
--------
|
|
53
|
+
For a list of length 5 (indices 0 to 4):
|
|
54
|
+
|
|
55
|
+
>>> next_index(4, 5) # moves from the end to the start
|
|
56
|
+
0
|
|
57
|
+
>>> next_index(0, 5, direction=-1) # moves from the start to the end
|
|
58
|
+
4
|
|
59
|
+
>>> next_index(2, 5) # normal forward movement
|
|
60
|
+
3
|
|
61
|
+
>>> next_index(2, 5, direction=-1) # normal backward movement
|
|
62
|
+
1
|
|
63
|
+
|
|
64
|
+
An empty list has no valid index, so 0 is returned:
|
|
65
|
+
|
|
66
|
+
>>> next_index(0, 0)
|
|
67
|
+
0
|
|
68
|
+
"""
|
|
69
|
+
# An empty list has no index to move to - and % 0 would raise
|
|
70
|
+
if length <= 0:
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
return (current_index + direction) % length
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def clamped_index(current_index: int, length: int, direction: int = 1) -> int:
|
|
77
|
+
"""
|
|
78
|
+
Calculates the next index in a list, stopping at both ends.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
current_index : int
|
|
83
|
+
The current index in the list.
|
|
84
|
+
length : int
|
|
85
|
+
The number of items in the list.
|
|
86
|
+
direction : int, optional
|
|
87
|
+
The direction and size of the movement (1 for one step forward,
|
|
88
|
+
-1 for one step backward).
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
int
|
|
93
|
+
The calculated next index, never outside the list. An empty list
|
|
94
|
+
always yields 0.
|
|
95
|
+
|
|
96
|
+
Examples
|
|
97
|
+
--------
|
|
98
|
+
For a list of length 5 (indices 0 to 4):
|
|
99
|
+
|
|
100
|
+
>>> clamped_index(4, 5) # already at the end, stays there
|
|
101
|
+
4
|
|
102
|
+
>>> clamped_index(0, 5, direction=-1) # already at the start
|
|
103
|
+
0
|
|
104
|
+
>>> clamped_index(2, 5) # normal forward movement
|
|
105
|
+
3
|
|
106
|
+
>>> clamped_index(0, 5, direction=10) # a step past the end is clamped
|
|
107
|
+
4
|
|
108
|
+
"""
|
|
109
|
+
if length <= 0:
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
return max(0, min(current_index + direction, length - 1))
|
termcore/util/logger.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Sets up file and console logging for an application."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"setup_logging",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def setup_logging(appname: str, level: int = logging.DEBUG) -> None:
|
|
12
|
+
"""
|
|
13
|
+
Sets up logging for the application.
|
|
14
|
+
|
|
15
|
+
Logs are stored in the user's home directory, under
|
|
16
|
+
`.local/state/{appname}/app.log`.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
appname : str
|
|
21
|
+
The name of the application, used to create a unique log directory.
|
|
22
|
+
level : int, optional
|
|
23
|
+
The logging level (e.g., logging.DEBUG, logging.INFO),
|
|
24
|
+
by default logging.DEBUG.
|
|
25
|
+
"""
|
|
26
|
+
log_dir = Path.home() / ".local" / "state" / appname
|
|
27
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
|
|
29
|
+
log_file = log_dir / "app.log"
|
|
30
|
+
|
|
31
|
+
logging.basicConfig(
|
|
32
|
+
level=level,
|
|
33
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
34
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
35
|
+
handlers=[logging.FileHandler(log_file, encoding="utf-8")],
|
|
36
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""The metaclass implementing the singleton pattern."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, cast
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"Singleton",
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
# Keyed by class, so that two classes using this metaclass keep their own
|
|
10
|
+
# instance. Holding the registry here rather than on the metaclass is also
|
|
11
|
+
# what lets __call__ report the constructed class as its return type.
|
|
12
|
+
_instances: dict[type, object] = {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Singleton(type):
|
|
16
|
+
"""
|
|
17
|
+
Metaclass for the singleton pattern.
|
|
18
|
+
|
|
19
|
+
Ensures that a class has only one instance and provides global access
|
|
20
|
+
to it.
|
|
21
|
+
|
|
22
|
+
Examples
|
|
23
|
+
--------
|
|
24
|
+
>>> class SomeClass(metaclass=Singleton):
|
|
25
|
+
... pass
|
|
26
|
+
>>> SomeClass() is SomeClass()
|
|
27
|
+
True
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __call__[T](cls: type[T], *args: Any, **kwargs: Any) -> T: # noqa: ANN401
|
|
31
|
+
"""
|
|
32
|
+
Returns the one instance, building it on the first call.
|
|
33
|
+
|
|
34
|
+
The arguments are whatever the class itself takes, which is what Any
|
|
35
|
+
says here. The return type is the class, so that a consumer sees the
|
|
36
|
+
type it asked for instead of `object`.
|
|
37
|
+
"""
|
|
38
|
+
if cls not in _instances:
|
|
39
|
+
_instances[cls] = super().__call__(*args, **kwargs) # pyright: ignore[reportAny]
|
|
40
|
+
|
|
41
|
+
return cast("T", _instances[cls])
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def instance(cls) -> object | None:
|
|
45
|
+
"""Returns the single instance, or None before it was built."""
|
|
46
|
+
return _instances.get(cls)
|