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.
Files changed (149) hide show
  1. click_extended/__init__.py +10 -6
  2. click_extended/classes.py +12 -8
  3. click_extended/core/__init__.py +10 -0
  4. click_extended/core/decorators/__init__.py +21 -0
  5. click_extended/core/decorators/argument.py +227 -0
  6. click_extended/core/decorators/command.py +93 -0
  7. click_extended/core/decorators/env.py +155 -0
  8. click_extended/core/decorators/group.py +96 -0
  9. click_extended/core/decorators/option.py +347 -0
  10. click_extended/core/decorators/prompt.py +69 -0
  11. click_extended/core/decorators/selection.py +155 -0
  12. click_extended/core/decorators/tag.py +109 -0
  13. click_extended/core/nodes/__init__.py +21 -0
  14. click_extended/core/nodes/_root_node.py +1012 -0
  15. click_extended/core/nodes/argument_node.py +165 -0
  16. click_extended/core/nodes/child_node.py +555 -0
  17. click_extended/core/nodes/child_validation_node.py +100 -0
  18. click_extended/core/nodes/node.py +55 -0
  19. click_extended/core/nodes/option_node.py +205 -0
  20. click_extended/core/nodes/parent_node.py +220 -0
  21. click_extended/core/nodes/validation_node.py +124 -0
  22. click_extended/core/other/__init__.py +7 -0
  23. click_extended/core/other/_click_command.py +60 -0
  24. click_extended/core/other/_click_group.py +246 -0
  25. click_extended/core/other/_tree.py +491 -0
  26. click_extended/core/other/context.py +496 -0
  27. click_extended/decorators/__init__.py +29 -0
  28. click_extended/decorators/check/__init__.py +57 -0
  29. click_extended/decorators/check/conflicts.py +149 -0
  30. click_extended/decorators/check/contains.py +69 -0
  31. click_extended/decorators/check/dependencies.py +115 -0
  32. click_extended/decorators/check/divisible_by.py +48 -0
  33. click_extended/decorators/check/ends_with.py +85 -0
  34. click_extended/decorators/check/exclusive.py +75 -0
  35. click_extended/decorators/check/falsy.py +37 -0
  36. click_extended/decorators/check/is_email.py +43 -0
  37. click_extended/decorators/check/is_hex_color.py +41 -0
  38. click_extended/decorators/check/is_hostname.py +47 -0
  39. click_extended/decorators/check/is_ipv4.py +46 -0
  40. click_extended/decorators/check/is_ipv6.py +46 -0
  41. click_extended/decorators/check/is_json.py +40 -0
  42. click_extended/decorators/check/is_mac_address.py +40 -0
  43. click_extended/decorators/check/is_negative.py +37 -0
  44. click_extended/decorators/check/is_non_zero.py +37 -0
  45. click_extended/decorators/check/is_port.py +39 -0
  46. click_extended/decorators/check/is_positive.py +37 -0
  47. click_extended/decorators/check/is_url.py +75 -0
  48. click_extended/decorators/check/is_uuid.py +40 -0
  49. click_extended/decorators/check/length.py +68 -0
  50. click_extended/decorators/check/not_empty.py +49 -0
  51. click_extended/decorators/check/regex.py +47 -0
  52. click_extended/decorators/check/requires.py +190 -0
  53. click_extended/decorators/check/starts_with.py +87 -0
  54. click_extended/decorators/check/truthy.py +37 -0
  55. click_extended/decorators/compare/__init__.py +15 -0
  56. click_extended/decorators/compare/at_least.py +57 -0
  57. click_extended/decorators/compare/at_most.py +57 -0
  58. click_extended/decorators/compare/between.py +119 -0
  59. click_extended/decorators/compare/greater_than.py +183 -0
  60. click_extended/decorators/compare/less_than.py +183 -0
  61. click_extended/decorators/convert/__init__.py +31 -0
  62. click_extended/decorators/convert/convert_angle.py +94 -0
  63. click_extended/decorators/convert/convert_area.py +123 -0
  64. click_extended/decorators/convert/convert_bits.py +211 -0
  65. click_extended/decorators/convert/convert_distance.py +154 -0
  66. click_extended/decorators/convert/convert_energy.py +155 -0
  67. click_extended/decorators/convert/convert_power.py +128 -0
  68. click_extended/decorators/convert/convert_pressure.py +131 -0
  69. click_extended/decorators/convert/convert_speed.py +122 -0
  70. click_extended/decorators/convert/convert_temperature.py +89 -0
  71. click_extended/decorators/convert/convert_time.py +108 -0
  72. click_extended/decorators/convert/convert_volume.py +218 -0
  73. click_extended/decorators/convert/convert_weight.py +158 -0
  74. click_extended/decorators/load/__init__.py +13 -0
  75. click_extended/decorators/load/load_csv.py +117 -0
  76. click_extended/decorators/load/load_json.py +61 -0
  77. click_extended/decorators/load/load_toml.py +47 -0
  78. click_extended/decorators/load/load_yaml.py +72 -0
  79. click_extended/decorators/math/__init__.py +37 -0
  80. click_extended/decorators/math/absolute.py +35 -0
  81. click_extended/decorators/math/add.py +48 -0
  82. click_extended/decorators/math/ceil.py +36 -0
  83. click_extended/decorators/math/clamp.py +51 -0
  84. click_extended/decorators/math/divide.py +42 -0
  85. click_extended/decorators/math/floor.py +36 -0
  86. click_extended/decorators/math/maximum.py +39 -0
  87. click_extended/decorators/math/minimum.py +39 -0
  88. click_extended/decorators/math/modulo.py +39 -0
  89. click_extended/decorators/math/multiply.py +51 -0
  90. click_extended/decorators/math/normalize.py +76 -0
  91. click_extended/decorators/math/power.py +39 -0
  92. click_extended/decorators/math/rounded.py +39 -0
  93. click_extended/decorators/math/sqrt.py +39 -0
  94. click_extended/decorators/math/subtract.py +39 -0
  95. click_extended/decorators/math/to_percent.py +63 -0
  96. click_extended/decorators/misc/__init__.py +17 -0
  97. click_extended/decorators/misc/choice.py +139 -0
  98. click_extended/decorators/misc/confirm_if.py +147 -0
  99. click_extended/decorators/misc/default.py +95 -0
  100. click_extended/decorators/misc/deprecated.py +131 -0
  101. click_extended/decorators/misc/experimental.py +79 -0
  102. click_extended/decorators/misc/now.py +42 -0
  103. click_extended/decorators/random/__init__.py +21 -0
  104. click_extended/decorators/random/random_bool.py +49 -0
  105. click_extended/decorators/random/random_choice.py +63 -0
  106. click_extended/decorators/random/random_datetime.py +140 -0
  107. click_extended/decorators/random/random_float.py +62 -0
  108. click_extended/decorators/random/random_integer.py +56 -0
  109. click_extended/decorators/random/random_prime.py +196 -0
  110. click_extended/decorators/random/random_string.py +77 -0
  111. click_extended/decorators/random/random_uuid.py +119 -0
  112. click_extended/decorators/transform/__init__.py +71 -0
  113. click_extended/decorators/transform/add_prefix.py +58 -0
  114. click_extended/decorators/transform/add_suffix.py +58 -0
  115. click_extended/decorators/transform/apply.py +35 -0
  116. click_extended/decorators/transform/basename.py +44 -0
  117. click_extended/decorators/transform/dirname.py +44 -0
  118. click_extended/decorators/transform/expand_vars.py +36 -0
  119. click_extended/decorators/transform/remove_prefix.py +57 -0
  120. click_extended/decorators/transform/remove_suffix.py +57 -0
  121. click_extended/decorators/transform/replace.py +46 -0
  122. click_extended/decorators/transform/slugify.py +45 -0
  123. click_extended/decorators/transform/split.py +43 -0
  124. click_extended/decorators/transform/strip.py +148 -0
  125. click_extended/decorators/transform/to_case.py +216 -0
  126. click_extended/decorators/transform/to_date.py +75 -0
  127. click_extended/decorators/transform/to_datetime.py +83 -0
  128. click_extended/decorators/transform/to_path.py +274 -0
  129. click_extended/decorators/transform/to_time.py +77 -0
  130. click_extended/decorators/transform/to_timestamp.py +114 -0
  131. click_extended/decorators/transform/truncate.py +47 -0
  132. click_extended/types.py +1 -1
  133. click_extended/utils/__init__.py +13 -0
  134. click_extended/utils/casing.py +169 -0
  135. click_extended/utils/checks.py +48 -0
  136. click_extended/utils/dispatch.py +1016 -0
  137. click_extended/utils/format.py +101 -0
  138. click_extended/utils/humanize.py +209 -0
  139. click_extended/utils/naming.py +238 -0
  140. click_extended/utils/process.py +294 -0
  141. click_extended/utils/selection.py +267 -0
  142. click_extended/utils/time.py +46 -0
  143. {click_extended-0.4.0.dist-info → click_extended-1.0.1.dist-info}/METADATA +100 -29
  144. click_extended-1.0.1.dist-info/RECORD +149 -0
  145. click_extended-0.4.0.dist-info/RECORD +0 -10
  146. {click_extended-0.4.0.dist-info → click_extended-1.0.1.dist-info}/WHEEL +0 -0
  147. {click_extended-0.4.0.dist-info → click_extended-1.0.1.dist-info}/licenses/AUTHORS.md +0 -0
  148. {click_extended-0.4.0.dist-info → click_extended-1.0.1.dist-info}/licenses/LICENSE +0 -0
  149. {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
@@ -2,7 +2,7 @@
2
2
 
3
3
  from typing import Any, Callable
4
4
 
5
- from click_extended.core.context import Context
5
+ from click_extended.core.other.context import Context
6
6
 
7
7
  Decorator = Callable[[Callable[..., Any]], Callable[..., Any]]
8
8
 
@@ -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
+ ]