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,43 @@
1
+ """Split the string by a separator."""
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 Split(ChildNode):
11
+ """Split the string by a separator."""
12
+
13
+ def handle_str(
14
+ self,
15
+ value: str,
16
+ context: Context,
17
+ *args: Any,
18
+ **kwargs: Any,
19
+ ) -> list[str]:
20
+ sep = kwargs.get("sep")
21
+ maxsplit = kwargs.get("maxsplit", -1)
22
+ return value.split(sep, maxsplit)
23
+
24
+
25
+ def split(sep: str | None = None, maxsplit: int = -1) -> Decorator:
26
+ """
27
+ Split the string by a separator.
28
+
29
+ Type: `ChildNode`
30
+
31
+ Supports: `str`
32
+
33
+ Args:
34
+ sep (str | None):
35
+ The delimiter string. If None, split by whitespace.
36
+ maxsplit (int):
37
+ Maximum number of splits. Defaults to `-1` (no limit).
38
+
39
+ Returns:
40
+ Decorator:
41
+ The decorated function.
42
+ """
43
+ return Split.as_decorator(sep=sep, maxsplit=maxsplit)
@@ -0,0 +1,148 @@
1
+ """Child decorators to strip characters from strings."""
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 Strip(ChildNode):
11
+ """Child decorator to strip characters from both ends of a string."""
12
+
13
+ def handle_str(
14
+ self, value: str, context: Context, *args: Any, **kwargs: Any
15
+ ) -> str:
16
+ chars: str | None = kwargs.get("chars")
17
+ return value.strip(chars)
18
+
19
+
20
+ class LStrip(ChildNode):
21
+ """Child decorator to strip characters from the left end of a string."""
22
+
23
+ def handle_str(
24
+ self, value: str, context: Context, *args: Any, **kwargs: Any
25
+ ) -> str:
26
+ chars: str | None = kwargs.get("chars")
27
+ return value.lstrip(chars)
28
+
29
+
30
+ class RStrip(ChildNode):
31
+ """Child decorator to strip characters from the right end of a string."""
32
+
33
+ def handle_str(
34
+ self, value: str, context: Context, *args: Any, **kwargs: Any
35
+ ) -> str:
36
+ chars: str | None = kwargs.get("chars")
37
+ return value.rstrip(chars)
38
+
39
+
40
+ def strip(chars: str | None = None) -> Decorator:
41
+ """
42
+ Remove leading and trailing characters from a string.
43
+
44
+ Type: `ChildNode`
45
+
46
+ Supports: `str`
47
+
48
+ Args:
49
+ chars (str | None, optional):
50
+ A string specifying the set of characters to be removed.
51
+ If `None` (default), whitespace characters are removed.
52
+
53
+ Returns:
54
+ Decorator:
55
+ The decorator function.
56
+
57
+ Examples:
58
+ ```python
59
+ @command()
60
+ @option("name", default=" hello ")
61
+ @strip()
62
+ def cmd(name: str) -> None:
63
+ click.echo(f"Name: '{name}'") # Output: Name: 'hello'
64
+ ```
65
+
66
+ ```python
67
+ @command()
68
+ @option("path", default="///path///")
69
+ @strip("/")
70
+ def cmd(path: str) -> None:
71
+ click.echo(f"Path: '{path}'") # Output: Path: 'path'
72
+ ```
73
+ """
74
+ return Strip.as_decorator(chars=chars)
75
+
76
+
77
+ def lstrip(chars: str | None = None) -> Decorator:
78
+ """
79
+ Remove leading (left) characters from a string.
80
+
81
+ Type: `ChildNode`
82
+
83
+ Supports: `str`
84
+
85
+ Args:
86
+ chars (str | None, optional):
87
+ A string specifying the set of characters to be removed.
88
+ If `None` (default), whitespace characters are removed.
89
+
90
+ Returns:
91
+ Decorator:
92
+ The decorator function.
93
+
94
+ Examples:
95
+ ```python
96
+ @command()
97
+ @option("name", default=" hello ")
98
+ @lstrip()
99
+ def cmd(name: str) -> None:
100
+ click.echo(f"Name: '{name}'") # Output: Name: 'hello '
101
+ ```
102
+
103
+ ```python
104
+ @command()
105
+ @option("path", default="///path///")
106
+ @lstrip("/")
107
+ def cmd(path: str) -> None:
108
+ click.echo(f"Path: '{path}'") # Output: Path: 'path///'
109
+ ```
110
+ """
111
+ return LStrip.as_decorator(chars=chars)
112
+
113
+
114
+ def rstrip(chars: str | None = None) -> Decorator:
115
+ """
116
+ Remove trailing (right) characters from a string.
117
+
118
+ Type: `ChildNode`
119
+
120
+ Supports: `str`
121
+
122
+ Args:
123
+ chars (str | None, optional):
124
+ A string specifying the set of characters to be removed.
125
+ If `None` (default), whitespace characters are removed.
126
+
127
+ Returns:
128
+ Decorator:
129
+ The decorator function.
130
+
131
+ Examples:
132
+ ```python
133
+ @command()
134
+ @option("name", default=" hello ")
135
+ @rstrip()
136
+ def cmd(name: str) -> None:
137
+ click.echo(f"Name: '{name}'") # Output: Name: ' hello'
138
+ ```
139
+
140
+ ```python
141
+ @command()
142
+ @option("path", default="///path///")
143
+ @rstrip("/")
144
+ def cmd(path: str) -> None:
145
+ click.echo(f"Path: '{path}'") # Output: Path: '///path'
146
+ ```
147
+ """
148
+ return RStrip.as_decorator(chars=chars)
@@ -0,0 +1,216 @@
1
+ """Child node to convert a string to one of many formats."""
2
+
3
+ from typing import Any, cast
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
+ from click_extended.utils.casing import Casing
9
+
10
+
11
+ class ToCase(ChildNode):
12
+ """Child node to convert a string to one of many formats."""
13
+
14
+ def handle_str(
15
+ self,
16
+ value: str,
17
+ context: Context,
18
+ *args: Any,
19
+ **kwargs: Any,
20
+ ) -> str:
21
+ return cast(str, getattr(Casing, kwargs["method"])(str(value)))
22
+
23
+
24
+ def to_lower_case() -> Decorator:
25
+ """
26
+ Convert a string to `lower case`.
27
+
28
+ Type: `ChildNode`
29
+
30
+ Supports: `str`
31
+
32
+ Returns:
33
+ Decorator:
34
+ The decorator function.
35
+ """
36
+ return ToCase.as_decorator(method="to_lower_case")
37
+
38
+
39
+ def to_upper_case() -> Decorator:
40
+ """
41
+ Convert a string to `UPPER CASE`.
42
+
43
+ Type: `ChildNode`
44
+
45
+ Supports: `str`
46
+
47
+ Returns:
48
+ Decorator:
49
+ The decorator function.
50
+ """
51
+ return ToCase.as_decorator(method="to_upper_case")
52
+
53
+
54
+ def to_meme_case() -> Decorator:
55
+ """
56
+ Convert a string to `mEmE cAsE`.
57
+
58
+ Type: `ChildNode`
59
+
60
+ Supports: `str`
61
+
62
+ Returns:
63
+ Decorator:
64
+ The decorator function.
65
+ """
66
+ return ToCase.as_decorator(method="to_meme_case")
67
+
68
+
69
+ def to_snake_case() -> Decorator:
70
+ """
71
+ Convert a string to `snake_case`.
72
+
73
+ Type: `ChildNode`
74
+
75
+ Supports: `str`
76
+
77
+ Returns:
78
+ Decorator:
79
+ The decorator function.
80
+ """
81
+ return ToCase.as_decorator(method="to_snake_case")
82
+
83
+
84
+ def to_screaming_snake_case() -> Decorator:
85
+ """
86
+ Convert a string to `SCREAMING_SNAKE_CASE`.
87
+
88
+ Type: `ChildNode`
89
+
90
+ Supports: `str`
91
+
92
+ Returns:
93
+ Decorator:
94
+ The decorator function.
95
+ """
96
+ return ToCase.as_decorator(method="to_screaming_snake_case")
97
+
98
+
99
+ def to_camel_case() -> Decorator:
100
+ """
101
+ Convert a string to `camelCase`.
102
+
103
+ Type: `ChildNode`
104
+
105
+ Supports: `str`
106
+
107
+ Returns:
108
+ Decorator:
109
+ The decorator function.
110
+ """
111
+ return ToCase.as_decorator(method="to_camel_case")
112
+
113
+
114
+ def to_pascal_case() -> Decorator:
115
+ """
116
+ Convert a string to `PascalCase`.
117
+
118
+ Type: `ChildNode`
119
+
120
+ Supports: `str`
121
+
122
+ Returns:
123
+ Decorator:
124
+ The decorator function.
125
+ """
126
+ return ToCase.as_decorator(method="to_pascal_case")
127
+
128
+
129
+ def to_kebab_case() -> Decorator:
130
+ """
131
+ Convert a string to `kebab-case`.
132
+
133
+ Type: `ChildNode`
134
+
135
+ Supports: `str`
136
+
137
+ Returns:
138
+ Decorator:
139
+ The decorator function.
140
+ """
141
+ return ToCase.as_decorator(method="to_kebab_case")
142
+
143
+
144
+ def to_train_case() -> Decorator:
145
+ """
146
+ Convert a string to `Train-Case`.
147
+
148
+ Type: `ChildNode`
149
+
150
+ Supports: `str`
151
+
152
+ Returns:
153
+ Decorator:
154
+ The decorator function.
155
+ """
156
+ return ToCase.as_decorator(method="to_train_case")
157
+
158
+
159
+ def to_flat_case() -> Decorator:
160
+ """
161
+ Convert a string to `flatcase`.
162
+
163
+ Type: `ChildNode`
164
+
165
+ Supports: `str`
166
+
167
+ Returns:
168
+ Decorator:
169
+ The decorator function.
170
+ """
171
+ return ToCase.as_decorator(method="to_flat_case")
172
+
173
+
174
+ def to_dot_case() -> Decorator:
175
+ """
176
+ Convert a string to `dot.case`.
177
+
178
+ Type: `ChildNode`
179
+
180
+ Supports: `str`
181
+
182
+ Returns:
183
+ Decorator:
184
+ The decorator function.
185
+ """
186
+ return ToCase.as_decorator(method="to_dot_case")
187
+
188
+
189
+ def to_title_case() -> Decorator:
190
+ """
191
+ Convert a string to `Title Case`.
192
+
193
+ Type: `ChildNode`
194
+
195
+ Supports: `str`
196
+
197
+ Returns:
198
+ Decorator:
199
+ The decorator function.
200
+ """
201
+ return ToCase.as_decorator(method="to_title_case")
202
+
203
+
204
+ def to_path_case() -> Decorator:
205
+ """
206
+ Convert a string to `path/case`.
207
+
208
+ Type: `ChildNode`
209
+
210
+ Supports: `str`
211
+
212
+ Returns:
213
+ Decorator:
214
+ The decorator function.
215
+ """
216
+ return ToCase.as_decorator(method="to_path_case")
@@ -0,0 +1,75 @@
1
+ """Child decorator to convert a string to a date."""
2
+
3
+ from datetime import date, datetime
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 ToDate(ChildNode):
14
+ """Child decorator to convert a string to a date."""
15
+
16
+ def handle_str(
17
+ self,
18
+ value: str,
19
+ context: Context,
20
+ *args: Any,
21
+ **kwargs: Any,
22
+ ) -> date:
23
+ formats = kwargs["formats"] or (
24
+ "%Y-%m-%d",
25
+ "%d/%m/%Y",
26
+ "%m/%d/%Y",
27
+ )
28
+
29
+ for fmt in formats:
30
+ try:
31
+ normalized_fmt = normalize_datetime_format(fmt)
32
+ dt = datetime.strptime(value, normalized_fmt)
33
+ return dt.date()
34
+ except ValueError:
35
+ continue
36
+
37
+ fmt_text = (
38
+ "either of the formats" if len(formats) != 1 else "in the format"
39
+ )
40
+ raise ValueError(
41
+ f"Invalid date '{value}', must be in "
42
+ f"{fmt_text} {humanize_iterable(formats, sep='or')}"
43
+ )
44
+
45
+
46
+ def to_date(
47
+ *formats: str,
48
+ ) -> Decorator:
49
+ """
50
+ Convert a string to a date by trying multiple formats.
51
+
52
+ Type: `ChildNode`
53
+
54
+ Supports: `str`
55
+
56
+ Args:
57
+ *formats (str):
58
+ One or more date format strings to try. Supports both Python
59
+ strptime format (e.g., "%Y-%m-%d", "%d/%m/%Y") and simplified format
60
+ (e.g., "YYYY-MM-DD", "DD/MM/YYYY"). The decorator will attempt each
61
+ format in order until one succeeds. Defaults to `"%Y-%m-%d"`,
62
+ `"%d/%m/%Y"`, and `"%m/%d/%Y"`.
63
+
64
+ Returns:
65
+ Decorator:
66
+ The decorated function.
67
+
68
+ Example:
69
+ @to_date("YYYY-MM-DD", "DD/MM/YYYY")
70
+ # Or using Python strptime format:
71
+ @to_date("%Y-%m-%d", "%d/%m/%Y")
72
+ def process_date(date_val: date):
73
+ print(date_val)
74
+ """
75
+ return ToDate.as_decorator(formats=formats)
@@ -0,0 +1,83 @@
1
+ """Child decorator to convert a string to a datetime."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any
5
+ from zoneinfo import ZoneInfo
6
+
7
+ from click_extended.core.nodes.child_node import ChildNode
8
+ from click_extended.core.other.context import Context
9
+ from click_extended.types import Decorator
10
+ from click_extended.utils import humanize_iterable
11
+ from click_extended.utils.time import normalize_datetime_format
12
+
13
+
14
+ class ToDatetime(ChildNode):
15
+ """Child decorator to convert a string to a datetime."""
16
+
17
+ def handle_str(
18
+ self,
19
+ value: str,
20
+ context: Context,
21
+ *args: Any,
22
+ **kwargs: Any,
23
+ ) -> datetime:
24
+ formats = kwargs["formats"] or (
25
+ "%Y-%m-%d",
26
+ "%H:%M:%S",
27
+ "%Y-%m-%d %H:%M:%S",
28
+ )
29
+ tz = kwargs.get("tz")
30
+
31
+ for fmt in formats:
32
+ try:
33
+ normalized_fmt = normalize_datetime_format(fmt)
34
+ dt = datetime.strptime(value, normalized_fmt)
35
+ if tz:
36
+ dt = dt.replace(tzinfo=ZoneInfo(tz))
37
+ return dt
38
+ except ValueError:
39
+ continue
40
+
41
+ fmt = "either of the formats" if len(formats) != 1 else "in the format"
42
+ raise ValueError(
43
+ f"Invalid datetime '{value}', must be in "
44
+ f"{fmt} {humanize_iterable(formats, sep='or')}"
45
+ )
46
+
47
+
48
+ def to_datetime(
49
+ *formats: str,
50
+ tz: str | None = None,
51
+ ) -> Decorator:
52
+ """
53
+ Convert a string to a datetime by trying multiple formats.
54
+
55
+ Type: `ChildNode`
56
+
57
+ Supports: `str`
58
+
59
+ Args:
60
+ *formats (str):
61
+ One or more datetime format strings to try. Supports both Python
62
+ strptime format (e.g., "%Y-%m-%d", "%d/%m/%Y") and simplified format
63
+ (e.g., "YYYY-MM-DD", "DD/MM/YYYY"). The decorator will attempt each
64
+ format in order until one succeeds. Defaults to `"%Y-%m-%d"`,
65
+ `"%H:%M:%S"` and `"%Y-%m-%d %H:%M:%S"`,
66
+
67
+ tz (str | None, optional):
68
+ Timezone name (e.g., "UTC", "America/New_York", "Europe/Stockholm")
69
+ to apply to the parsed datetime. Uses zoneinfo.ZoneInfo for timezone
70
+ handling. Defaults to `None` (naive datetime).
71
+
72
+ Returns:
73
+ Decorator:
74
+ The decorated function.
75
+
76
+ Example:
77
+ @to_datetime("YYYY-MM-DD", "DD/MM/YYYY", tz="America/New_York")
78
+ # Or using Python strptime format:
79
+ @to_datetime("%Y-%m-%d", "%d/%m/%Y", tz="America/New_York")
80
+ def process_date(date: datetime):
81
+ print(date)
82
+ """
83
+ return ToDatetime.as_decorator(formats=formats, tz=tz)