click-extended 0.4.0__py3-none-any.whl → 1.0.1__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.
- click_extended/__init__.py +10 -6
- click_extended/classes.py +12 -8
- click_extended/core/__init__.py +10 -0
- click_extended/core/decorators/__init__.py +21 -0
- click_extended/core/decorators/argument.py +227 -0
- click_extended/core/decorators/command.py +93 -0
- click_extended/core/decorators/env.py +155 -0
- click_extended/core/decorators/group.py +96 -0
- click_extended/core/decorators/option.py +347 -0
- click_extended/core/decorators/prompt.py +69 -0
- click_extended/core/decorators/selection.py +155 -0
- click_extended/core/decorators/tag.py +109 -0
- click_extended/core/nodes/__init__.py +21 -0
- click_extended/core/nodes/_root_node.py +1012 -0
- click_extended/core/nodes/argument_node.py +165 -0
- click_extended/core/nodes/child_node.py +555 -0
- click_extended/core/nodes/child_validation_node.py +100 -0
- click_extended/core/nodes/node.py +55 -0
- click_extended/core/nodes/option_node.py +205 -0
- click_extended/core/nodes/parent_node.py +220 -0
- click_extended/core/nodes/validation_node.py +124 -0
- click_extended/core/other/__init__.py +7 -0
- click_extended/core/other/_click_command.py +60 -0
- click_extended/core/other/_click_group.py +246 -0
- click_extended/core/other/_tree.py +491 -0
- click_extended/core/other/context.py +496 -0
- click_extended/decorators/__init__.py +29 -0
- click_extended/decorators/check/__init__.py +57 -0
- click_extended/decorators/check/conflicts.py +149 -0
- click_extended/decorators/check/contains.py +69 -0
- click_extended/decorators/check/dependencies.py +115 -0
- click_extended/decorators/check/divisible_by.py +48 -0
- click_extended/decorators/check/ends_with.py +85 -0
- click_extended/decorators/check/exclusive.py +75 -0
- click_extended/decorators/check/falsy.py +37 -0
- click_extended/decorators/check/is_email.py +43 -0
- click_extended/decorators/check/is_hex_color.py +41 -0
- click_extended/decorators/check/is_hostname.py +47 -0
- click_extended/decorators/check/is_ipv4.py +46 -0
- click_extended/decorators/check/is_ipv6.py +46 -0
- click_extended/decorators/check/is_json.py +40 -0
- click_extended/decorators/check/is_mac_address.py +40 -0
- click_extended/decorators/check/is_negative.py +37 -0
- click_extended/decorators/check/is_non_zero.py +37 -0
- click_extended/decorators/check/is_port.py +39 -0
- click_extended/decorators/check/is_positive.py +37 -0
- click_extended/decorators/check/is_url.py +75 -0
- click_extended/decorators/check/is_uuid.py +40 -0
- click_extended/decorators/check/length.py +68 -0
- click_extended/decorators/check/not_empty.py +49 -0
- click_extended/decorators/check/regex.py +47 -0
- click_extended/decorators/check/requires.py +190 -0
- click_extended/decorators/check/starts_with.py +87 -0
- click_extended/decorators/check/truthy.py +37 -0
- click_extended/decorators/compare/__init__.py +15 -0
- click_extended/decorators/compare/at_least.py +57 -0
- click_extended/decorators/compare/at_most.py +57 -0
- click_extended/decorators/compare/between.py +119 -0
- click_extended/decorators/compare/greater_than.py +183 -0
- click_extended/decorators/compare/less_than.py +183 -0
- click_extended/decorators/convert/__init__.py +31 -0
- click_extended/decorators/convert/convert_angle.py +94 -0
- click_extended/decorators/convert/convert_area.py +123 -0
- click_extended/decorators/convert/convert_bits.py +211 -0
- click_extended/decorators/convert/convert_distance.py +154 -0
- click_extended/decorators/convert/convert_energy.py +155 -0
- click_extended/decorators/convert/convert_power.py +128 -0
- click_extended/decorators/convert/convert_pressure.py +131 -0
- click_extended/decorators/convert/convert_speed.py +122 -0
- click_extended/decorators/convert/convert_temperature.py +89 -0
- click_extended/decorators/convert/convert_time.py +108 -0
- click_extended/decorators/convert/convert_volume.py +218 -0
- click_extended/decorators/convert/convert_weight.py +158 -0
- click_extended/decorators/load/__init__.py +13 -0
- click_extended/decorators/load/load_csv.py +117 -0
- click_extended/decorators/load/load_json.py +61 -0
- click_extended/decorators/load/load_toml.py +47 -0
- click_extended/decorators/load/load_yaml.py +72 -0
- click_extended/decorators/math/__init__.py +37 -0
- click_extended/decorators/math/absolute.py +35 -0
- click_extended/decorators/math/add.py +48 -0
- click_extended/decorators/math/ceil.py +36 -0
- click_extended/decorators/math/clamp.py +51 -0
- click_extended/decorators/math/divide.py +42 -0
- click_extended/decorators/math/floor.py +36 -0
- click_extended/decorators/math/maximum.py +39 -0
- click_extended/decorators/math/minimum.py +39 -0
- click_extended/decorators/math/modulo.py +39 -0
- click_extended/decorators/math/multiply.py +51 -0
- click_extended/decorators/math/normalize.py +76 -0
- click_extended/decorators/math/power.py +39 -0
- click_extended/decorators/math/rounded.py +39 -0
- click_extended/decorators/math/sqrt.py +39 -0
- click_extended/decorators/math/subtract.py +39 -0
- click_extended/decorators/math/to_percent.py +63 -0
- click_extended/decorators/misc/__init__.py +17 -0
- click_extended/decorators/misc/choice.py +139 -0
- click_extended/decorators/misc/confirm_if.py +147 -0
- click_extended/decorators/misc/default.py +95 -0
- click_extended/decorators/misc/deprecated.py +131 -0
- click_extended/decorators/misc/experimental.py +79 -0
- click_extended/decorators/misc/now.py +42 -0
- click_extended/decorators/random/__init__.py +21 -0
- click_extended/decorators/random/random_bool.py +49 -0
- click_extended/decorators/random/random_choice.py +63 -0
- click_extended/decorators/random/random_datetime.py +140 -0
- click_extended/decorators/random/random_float.py +62 -0
- click_extended/decorators/random/random_integer.py +56 -0
- click_extended/decorators/random/random_prime.py +196 -0
- click_extended/decorators/random/random_string.py +77 -0
- click_extended/decorators/random/random_uuid.py +119 -0
- click_extended/decorators/transform/__init__.py +71 -0
- click_extended/decorators/transform/add_prefix.py +58 -0
- click_extended/decorators/transform/add_suffix.py +58 -0
- click_extended/decorators/transform/apply.py +35 -0
- click_extended/decorators/transform/basename.py +44 -0
- click_extended/decorators/transform/dirname.py +44 -0
- click_extended/decorators/transform/expand_vars.py +36 -0
- click_extended/decorators/transform/remove_prefix.py +57 -0
- click_extended/decorators/transform/remove_suffix.py +57 -0
- click_extended/decorators/transform/replace.py +46 -0
- click_extended/decorators/transform/slugify.py +45 -0
- click_extended/decorators/transform/split.py +43 -0
- click_extended/decorators/transform/strip.py +148 -0
- click_extended/decorators/transform/to_case.py +216 -0
- click_extended/decorators/transform/to_date.py +75 -0
- click_extended/decorators/transform/to_datetime.py +83 -0
- click_extended/decorators/transform/to_path.py +274 -0
- click_extended/decorators/transform/to_time.py +77 -0
- click_extended/decorators/transform/to_timestamp.py +114 -0
- click_extended/decorators/transform/truncate.py +47 -0
- click_extended/types.py +1 -1
- click_extended/utils/__init__.py +13 -0
- click_extended/utils/casing.py +169 -0
- click_extended/utils/checks.py +48 -0
- click_extended/utils/dispatch.py +1016 -0
- click_extended/utils/format.py +101 -0
- click_extended/utils/humanize.py +209 -0
- click_extended/utils/naming.py +238 -0
- click_extended/utils/process.py +294 -0
- click_extended/utils/selection.py +267 -0
- click_extended/utils/time.py +46 -0
- {click_extended-0.4.0.dist-info → click_extended-1.0.1.dist-info}/METADATA +100 -29
- click_extended-1.0.1.dist-info/RECORD +149 -0
- click_extended-0.4.0.dist-info/RECORD +0 -10
- {click_extended-0.4.0.dist-info → click_extended-1.0.1.dist-info}/WHEEL +0 -0
- {click_extended-0.4.0.dist-info → click_extended-1.0.1.dist-info}/licenses/AUTHORS.md +0 -0
- {click_extended-0.4.0.dist-info → click_extended-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {click_extended-0.4.0.dist-info → click_extended-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Convert a string to a `pathlib.Path` object."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=too-many-locals
|
|
4
|
+
# pylint: disable=too-many-branches
|
|
5
|
+
# pylint: disable=too-many-statements
|
|
6
|
+
# pylint: disable=too-many-arguments
|
|
7
|
+
|
|
8
|
+
import fnmatch
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from click_extended.core.nodes.child_node import ChildNode
|
|
14
|
+
from click_extended.core.other.context import Context
|
|
15
|
+
from click_extended.types import Decorator
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ToPath(ChildNode):
|
|
19
|
+
"""Convert a string to a `pathlib.Path` object."""
|
|
20
|
+
|
|
21
|
+
def handle_str(
|
|
22
|
+
self, value: str, context: Context, *args: Any, **kwargs: Any
|
|
23
|
+
) -> Path:
|
|
24
|
+
parent = context.get_current_parent_as_parent()
|
|
25
|
+
name = parent.name
|
|
26
|
+
exists = kwargs["exists"]
|
|
27
|
+
parents = kwargs["parents"]
|
|
28
|
+
extensions = kwargs["extensions"]
|
|
29
|
+
include_pattern = kwargs["include_pattern"]
|
|
30
|
+
exclude_pattern = kwargs["exclude_pattern"]
|
|
31
|
+
allow_file = kwargs["allow_file"]
|
|
32
|
+
allow_directory = kwargs["allow_directory"]
|
|
33
|
+
allow_empty_directory = kwargs["allow_empty_directory"]
|
|
34
|
+
allow_empty_file = kwargs["allow_empty_file"]
|
|
35
|
+
allow_symlink = kwargs["allow_symlink"]
|
|
36
|
+
follow_symlink = kwargs["follow_symlink"]
|
|
37
|
+
resolve = kwargs["resolve"]
|
|
38
|
+
is_readable = kwargs["is_readable"]
|
|
39
|
+
is_writable = kwargs["is_writable"]
|
|
40
|
+
is_executable = kwargs["is_executable"]
|
|
41
|
+
|
|
42
|
+
path = Path(value) if isinstance(value, str) else value
|
|
43
|
+
path = path.expanduser()
|
|
44
|
+
|
|
45
|
+
# Symlinks
|
|
46
|
+
if not allow_symlink and path.is_symlink():
|
|
47
|
+
raise OSError(
|
|
48
|
+
f"Path '{path}' is a symlink, but symlinks are not allowed "
|
|
49
|
+
f"for '{name}'"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if resolve:
|
|
53
|
+
path = path.resolve() if follow_symlink else path.absolute()
|
|
54
|
+
elif follow_symlink and path.is_symlink():
|
|
55
|
+
path = path.resolve()
|
|
56
|
+
else:
|
|
57
|
+
path = path.absolute()
|
|
58
|
+
|
|
59
|
+
# Existence
|
|
60
|
+
if exists and not path.exists():
|
|
61
|
+
raise FileNotFoundError(f"Path '{path}' does not exist.")
|
|
62
|
+
|
|
63
|
+
# Parents
|
|
64
|
+
if parents and not path.parent.exists():
|
|
65
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
|
|
67
|
+
if path.exists():
|
|
68
|
+
is_file = path.is_file()
|
|
69
|
+
is_dir = path.is_dir()
|
|
70
|
+
|
|
71
|
+
# File disallowed
|
|
72
|
+
if is_file and not allow_file:
|
|
73
|
+
raise IsADirectoryError(
|
|
74
|
+
f"Path '{path}' is a file, but files are not allowed "
|
|
75
|
+
f"for '{name}'"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Directory disallowed
|
|
79
|
+
if is_dir and not allow_directory:
|
|
80
|
+
raise NotADirectoryError(
|
|
81
|
+
f"Path '{path}' is a directory, but directories are not "
|
|
82
|
+
f"allowed for '{name}'"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Check empty directory
|
|
86
|
+
if is_dir and not allow_empty_directory:
|
|
87
|
+
if not any(path.iterdir()):
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"Directory '{path}' is empty, but empty directories "
|
|
90
|
+
f"are not allowed for '{name}'"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Check empty file
|
|
94
|
+
if is_file and not allow_empty_file:
|
|
95
|
+
if path.stat().st_size == 0:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"File '{path}' is empty, but empty files are not "
|
|
98
|
+
f"allowed for '{name}'"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Check permissions
|
|
102
|
+
if is_readable and not os.access(path, os.R_OK):
|
|
103
|
+
raise PermissionError(
|
|
104
|
+
f"Path '{path}' is not readable for '{name}'"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if is_writable and not os.access(path, os.W_OK):
|
|
108
|
+
raise PermissionError(
|
|
109
|
+
f"Path '{path}' is not writable for '{name}'"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if is_executable and not os.access(path, os.X_OK):
|
|
113
|
+
raise PermissionError(
|
|
114
|
+
f"Path '{path}' is not executable for '{name}'"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Check extensions
|
|
118
|
+
if extensions and (not path.exists() or path.is_file()):
|
|
119
|
+
extensions = [
|
|
120
|
+
ext if ext.startswith(".") else f".{ext}" for ext in extensions
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
if not any(path.name.endswith(ext) for ext in extensions):
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"Path '{path}' does not have an allowed extension. "
|
|
126
|
+
f"Allowed extensions: {', '.join(extensions)} "
|
|
127
|
+
f"for '{name}'"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Patterns
|
|
131
|
+
if include_pattern:
|
|
132
|
+
if not fnmatch.fnmatch(path.name, include_pattern):
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"Path '{path}' does not match include pattern "
|
|
135
|
+
f"'{include_pattern}' for '{name}'"
|
|
136
|
+
)
|
|
137
|
+
elif exclude_pattern and fnmatch.fnmatch(path.name, exclude_pattern):
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"Path '{path}' matches exclude pattern "
|
|
140
|
+
f"'{exclude_pattern}' for '{name}'"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return path
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def to_path(
|
|
147
|
+
*,
|
|
148
|
+
exists: bool = True,
|
|
149
|
+
parents: bool = False,
|
|
150
|
+
resolve: bool = True,
|
|
151
|
+
extensions: list[str] | None = None,
|
|
152
|
+
include_pattern: str | None = None,
|
|
153
|
+
exclude_pattern: str | None = None,
|
|
154
|
+
allow_file: bool = True,
|
|
155
|
+
allow_directory: bool = True,
|
|
156
|
+
allow_empty_directory: bool = True,
|
|
157
|
+
allow_empty_file: bool = True,
|
|
158
|
+
allow_symlink: bool = False,
|
|
159
|
+
follow_symlink: bool = True,
|
|
160
|
+
is_readable: bool = False,
|
|
161
|
+
is_writable: bool = False,
|
|
162
|
+
is_executable: bool = False,
|
|
163
|
+
) -> Decorator:
|
|
164
|
+
"""
|
|
165
|
+
Convert, validate, and process a string to a `pathlib.Path` object.
|
|
166
|
+
|
|
167
|
+
Type: `ChildNode`
|
|
168
|
+
|
|
169
|
+
Supports: `str`
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
exists (bool, optional):
|
|
173
|
+
Whether the path needs to exist.
|
|
174
|
+
Defaults to `True`.
|
|
175
|
+
|
|
176
|
+
parents (bool, optional):
|
|
177
|
+
Create parent directories if they don't exist.
|
|
178
|
+
Defaults to `False`.
|
|
179
|
+
|
|
180
|
+
resolve (bool, optional):
|
|
181
|
+
Convert the path to an absolute path. When `True`, resolves
|
|
182
|
+
`.` and `..` components. When False, only makes the path
|
|
183
|
+
absolute without resolution.
|
|
184
|
+
Defaults to `True`.
|
|
185
|
+
|
|
186
|
+
extensions (list[str], optional):
|
|
187
|
+
A list of extensions to require the path to end with.
|
|
188
|
+
By default, all extensions are allowed.
|
|
189
|
+
|
|
190
|
+
include_pattern (str, optional):
|
|
191
|
+
A whitelist pattern. Uses shell-style glob patterns.
|
|
192
|
+
Defaults to `None` (All file names allowed).
|
|
193
|
+
|
|
194
|
+
When both `include_pattern` and `exclude_pattern` are provided,
|
|
195
|
+
`include_pattern` takes precedence (files matching include
|
|
196
|
+
are always accepted).
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
include_pattern="*.py" # Only Python files
|
|
200
|
+
include_pattern="config_*" # Files starting with "config_"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
exclude_pattern (str, optional):
|
|
204
|
+
A blacklist pattern. Uses shell-style glob patterns.
|
|
205
|
+
Defaults to `None` (No file names excluded).
|
|
206
|
+
|
|
207
|
+
If provided without `include_pattern`, file names matching this
|
|
208
|
+
pattern will be rejected, and all other file names will be
|
|
209
|
+
allowed.
|
|
210
|
+
|
|
211
|
+
When both `include_pattern` and `exclude_pattern` are provided,
|
|
212
|
+
`include_pattern` takes precedence (files matching include
|
|
213
|
+
are always accepted).
|
|
214
|
+
|
|
215
|
+
allow_file (bool, optional):
|
|
216
|
+
Whether to allow the path to be a file or not.
|
|
217
|
+
Defaults to `True`.
|
|
218
|
+
|
|
219
|
+
allow_directory (bool, optional):
|
|
220
|
+
Whether to allow the path to be a directory or not.
|
|
221
|
+
Defaults to `True`.
|
|
222
|
+
|
|
223
|
+
allow_empty_directory (bool, optional):
|
|
224
|
+
If the path points to a directory, whether to allow
|
|
225
|
+
the directory to be empty. Only checked when the path
|
|
226
|
+
is a directory. Defaults to `True`.
|
|
227
|
+
|
|
228
|
+
allow_empty_file (bool, optional):
|
|
229
|
+
If the path points to a file, whether to allow the
|
|
230
|
+
file to be empty. Only checked when the path is a file.
|
|
231
|
+
Defaults to `True`.
|
|
232
|
+
|
|
233
|
+
allow_symlink (bool, optional):
|
|
234
|
+
Whether to allow the path to be a symlink or not.
|
|
235
|
+
Defaults to `False`.
|
|
236
|
+
|
|
237
|
+
follow_symlink (bool, optional):
|
|
238
|
+
Whether to follow symlinks when resolving the path.
|
|
239
|
+
Only applies when resolve=True.
|
|
240
|
+
Defaults to `True`.
|
|
241
|
+
|
|
242
|
+
is_readable (bool, optional):
|
|
243
|
+
Whether the file has `read` permissions.
|
|
244
|
+
Defaults to `False`.
|
|
245
|
+
|
|
246
|
+
is_writable (bool, optional):
|
|
247
|
+
Whether the file has `write` permissions.
|
|
248
|
+
Default to `False`.
|
|
249
|
+
|
|
250
|
+
is_executable (bool, optional):
|
|
251
|
+
A unix-only feature that checks if the file has
|
|
252
|
+
`execute` permissions. Defaults to `False`.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Decorator:
|
|
256
|
+
The decorated function.
|
|
257
|
+
"""
|
|
258
|
+
return ToPath.as_decorator(
|
|
259
|
+
exists=exists,
|
|
260
|
+
parents=parents,
|
|
261
|
+
resolve=resolve,
|
|
262
|
+
extensions=extensions,
|
|
263
|
+
include_pattern=include_pattern,
|
|
264
|
+
exclude_pattern=exclude_pattern,
|
|
265
|
+
allow_file=allow_file,
|
|
266
|
+
allow_directory=allow_directory,
|
|
267
|
+
allow_empty_directory=allow_empty_directory,
|
|
268
|
+
allow_empty_file=allow_empty_file,
|
|
269
|
+
allow_symlink=allow_symlink,
|
|
270
|
+
follow_symlink=follow_symlink,
|
|
271
|
+
is_readable=is_readable,
|
|
272
|
+
is_writable=is_writable,
|
|
273
|
+
is_executable=is_executable,
|
|
274
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Child decorator to convert a string to a time."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, time
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from click_extended.core.nodes.child_node import ChildNode
|
|
7
|
+
from click_extended.core.other.context import Context
|
|
8
|
+
from click_extended.types import Decorator
|
|
9
|
+
from click_extended.utils import humanize_iterable
|
|
10
|
+
from click_extended.utils.time import normalize_datetime_format
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ToTime(ChildNode):
|
|
14
|
+
"""Child decorator to convert a string to a time."""
|
|
15
|
+
|
|
16
|
+
def handle_str(
|
|
17
|
+
self,
|
|
18
|
+
value: str,
|
|
19
|
+
context: Context,
|
|
20
|
+
*args: Any,
|
|
21
|
+
**kwargs: Any,
|
|
22
|
+
) -> time:
|
|
23
|
+
formats = kwargs["formats"] or (
|
|
24
|
+
"%H:%M:%S",
|
|
25
|
+
"%H:%M",
|
|
26
|
+
"%I:%M:%S %p",
|
|
27
|
+
"%I:%M %p",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
for fmt in formats:
|
|
31
|
+
try:
|
|
32
|
+
normalized_fmt = normalize_datetime_format(fmt)
|
|
33
|
+
|
|
34
|
+
dt = datetime.strptime(value, normalized_fmt)
|
|
35
|
+
return dt.time()
|
|
36
|
+
except ValueError:
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
fmt_text = (
|
|
40
|
+
"either of the formats" if len(formats) != 1 else "in the format"
|
|
41
|
+
)
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"Invalid time '{value}', must be in "
|
|
44
|
+
f"{fmt_text} {humanize_iterable(formats, sep='or')}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def to_time(
|
|
49
|
+
*formats: str,
|
|
50
|
+
) -> Decorator:
|
|
51
|
+
"""
|
|
52
|
+
Convert a string to a time by trying multiple formats.
|
|
53
|
+
|
|
54
|
+
Type: `ChildNode`
|
|
55
|
+
|
|
56
|
+
Supports: `str`
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
*formats (str):
|
|
60
|
+
One or more time format strings to try. Supports both Python
|
|
61
|
+
strptime format (e.g., "%H:%M:%S", "%I:%M %p") and simplified format
|
|
62
|
+
(e.g., "HH:mm:SS", "HH:mm"). The decorator will attempt each
|
|
63
|
+
format in order until one succeeds. Defaults to `"%H:%M:%S"`,
|
|
64
|
+
`"%H:%M"`, `"%I:%M:%S %p"`, and `"%I:%M %p"`.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Decorator:
|
|
68
|
+
The decorated function.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
@to_time("HH:mm:SS", "HH:mm")
|
|
72
|
+
# Or using Python strptime format:
|
|
73
|
+
@to_time("%H:%M:%S", "%H:%M")
|
|
74
|
+
def process_time(time_val: time):
|
|
75
|
+
print(time_val)
|
|
76
|
+
"""
|
|
77
|
+
return ToTime.as_decorator(formats=formats)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Convert datetime, date, or time objects to Unix timestamps."""
|
|
2
|
+
|
|
3
|
+
from datetime import date, datetime, time, timezone
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from click_extended.core.nodes.child_node import ChildNode
|
|
7
|
+
from click_extended.core.other.context import Context
|
|
8
|
+
from click_extended.types import Decorator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ToTimestamp(ChildNode):
|
|
12
|
+
"""Convert datetime, date, or time objects to Unix timestamps."""
|
|
13
|
+
|
|
14
|
+
def handle_datetime(
|
|
15
|
+
self, value: datetime, context: Context, *args: Any, **kwargs: Any
|
|
16
|
+
) -> int:
|
|
17
|
+
unit = kwargs["unit"]
|
|
18
|
+
|
|
19
|
+
if value.date() == date(1900, 1, 1):
|
|
20
|
+
today = datetime.now(timezone.utc).date()
|
|
21
|
+
value = datetime.combine(today, value.time())
|
|
22
|
+
if value.tzinfo is None:
|
|
23
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
24
|
+
elif value.tzinfo is None:
|
|
25
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
26
|
+
|
|
27
|
+
timestamp = value.timestamp()
|
|
28
|
+
|
|
29
|
+
if unit == "s":
|
|
30
|
+
return int(timestamp)
|
|
31
|
+
|
|
32
|
+
if unit == "ms":
|
|
33
|
+
return int(timestamp * 1000)
|
|
34
|
+
|
|
35
|
+
if unit == "us":
|
|
36
|
+
return int(timestamp * 1_000_000)
|
|
37
|
+
|
|
38
|
+
return int(timestamp * 1_000_000_000)
|
|
39
|
+
|
|
40
|
+
def handle_date(
|
|
41
|
+
self, value: date, context: Context, *args: Any, **kwargs: Any
|
|
42
|
+
) -> int:
|
|
43
|
+
dt = datetime.combine(value, datetime.min.time(), tzinfo=timezone.utc)
|
|
44
|
+
return self.handle_datetime(dt, context, *args, **kwargs)
|
|
45
|
+
|
|
46
|
+
def handle_time(
|
|
47
|
+
self, value: time, context: Context, *args: Any, **kwargs: Any
|
|
48
|
+
) -> int:
|
|
49
|
+
today = datetime.now(timezone.utc).date()
|
|
50
|
+
|
|
51
|
+
if value.tzinfo is None:
|
|
52
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
53
|
+
|
|
54
|
+
dt = datetime.combine(today, value)
|
|
55
|
+
return self.handle_datetime(dt, context, *args, **kwargs)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def to_timestamp(unit: Literal["s", "ms", "us", "ns"] = "s") -> Decorator:
|
|
59
|
+
"""
|
|
60
|
+
Convert datetime, date, or time objects to Unix timestamps.
|
|
61
|
+
|
|
62
|
+
Type: `ChildNode`
|
|
63
|
+
|
|
64
|
+
Supports: `datetime`, `date`, `time`
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
unit (Literal["s", "ms", "us", "ns"], optional):
|
|
68
|
+
The unit of the timestamp:
|
|
69
|
+
- `"s"`: Seconds (standard Unix timestamp)
|
|
70
|
+
- `"ms"`: Milliseconds (JavaScript/Java style)
|
|
71
|
+
- `"us"`: Microseconds (Python datetime precision)
|
|
72
|
+
- `"ns"`: Nanoseconds (high-precision logging)
|
|
73
|
+
Defaults to `"s"`.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Decorator:
|
|
77
|
+
The decorated function.
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
Basic usage with datetime:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
@command()
|
|
84
|
+
@option("dt", default=None)
|
|
85
|
+
@to_datetime()
|
|
86
|
+
@to_timestamp()
|
|
87
|
+
def cmd(dt: int) -> None:
|
|
88
|
+
click.echo(f"Timestamp: {dt}")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
With milliseconds for JavaScript:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
@command()
|
|
95
|
+
@option("dt", default=None)
|
|
96
|
+
@to_datetime()
|
|
97
|
+
@to_timestamp("ms")
|
|
98
|
+
def cmd(dt: int) -> None:
|
|
99
|
+
click.echo(f"Milliseconds: {dt}")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Multiple timestamps:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
@command()
|
|
106
|
+
@option("dates", default=None, nargs=3)
|
|
107
|
+
@to_date()
|
|
108
|
+
@to_timestamp()
|
|
109
|
+
def cmd(dates: tuple[int, ...]) -> None:
|
|
110
|
+
for ts in dates:
|
|
111
|
+
click.echo(f"Timestamp: {ts}")
|
|
112
|
+
```
|
|
113
|
+
"""
|
|
114
|
+
return ToTimestamp.as_decorator(unit=unit)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Truncate the string to a specific length."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from click_extended.core.nodes.child_node import ChildNode
|
|
6
|
+
from click_extended.core.other.context import Context
|
|
7
|
+
from click_extended.types import Decorator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Truncate(ChildNode):
|
|
11
|
+
"""Truncate the string to a specific length."""
|
|
12
|
+
|
|
13
|
+
def handle_str(
|
|
14
|
+
self,
|
|
15
|
+
value: str,
|
|
16
|
+
context: Context,
|
|
17
|
+
*args: Any,
|
|
18
|
+
**kwargs: Any,
|
|
19
|
+
) -> Any:
|
|
20
|
+
length = kwargs["length"]
|
|
21
|
+
suffix = kwargs["suffix"]
|
|
22
|
+
|
|
23
|
+
if len(value) <= length:
|
|
24
|
+
return value
|
|
25
|
+
|
|
26
|
+
return value[: length - len(suffix)] + suffix
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def truncate(length: int, suffix: str = "...") -> Decorator:
|
|
30
|
+
"""
|
|
31
|
+
Truncate the string to a specific length.
|
|
32
|
+
|
|
33
|
+
Type: `ChildNode`
|
|
34
|
+
|
|
35
|
+
Supports: `str`
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
length (int):
|
|
39
|
+
The maximum length of the string.
|
|
40
|
+
suffix (str):
|
|
41
|
+
The suffix to append when truncated. Defaults to `...`.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Decorator:
|
|
45
|
+
The decorated function.
|
|
46
|
+
"""
|
|
47
|
+
return Truncate.as_decorator(length=length, suffix=suffix)
|
click_extended/types.py
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Initialization file for the 'click_extended.utils' module."""
|
|
2
|
+
|
|
3
|
+
from click_extended.utils.casing import Casing
|
|
4
|
+
from click_extended.utils.checks import is_argument, is_option, is_tag
|
|
5
|
+
from click_extended.utils.humanize import humanize_iterable
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Casing",
|
|
9
|
+
"is_argument",
|
|
10
|
+
"is_option",
|
|
11
|
+
"is_tag",
|
|
12
|
+
"humanize_iterable",
|
|
13
|
+
]
|