click-extended 1.0.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/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/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-1.0.0.dist-info → click_extended-1.0.1.dist-info}/METADATA +2 -1
- click_extended-1.0.1.dist-info/RECORD +149 -0
- click_extended-1.0.0.dist-info/RECORD +0 -10
- {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/WHEEL +0 -0
- {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/licenses/AUTHORS.md +0 -0
- {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {click_extended-1.0.0.dist-info → click_extended-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Initialization file for the 'click_extended.core' module."""
|
|
2
|
+
|
|
3
|
+
from click_extended.core.decorators import *
|
|
4
|
+
from click_extended.core.decorators import __all__ as decorators_all
|
|
5
|
+
from click_extended.core.nodes import *
|
|
6
|
+
from click_extended.core.nodes import __all__ as nodes_all
|
|
7
|
+
from click_extended.core.other import *
|
|
8
|
+
from click_extended.core.other import __all__ as other_all
|
|
9
|
+
|
|
10
|
+
__all__ = [*decorators_all, *nodes_all, *other_all] # type: ignore
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Initialization file for the `click_extended.core.decorators` module."""
|
|
2
|
+
|
|
3
|
+
from click_extended.core.decorators.argument import Argument
|
|
4
|
+
from click_extended.core.decorators.command import Command
|
|
5
|
+
from click_extended.core.decorators.env import Env
|
|
6
|
+
from click_extended.core.decorators.group import Group
|
|
7
|
+
from click_extended.core.decorators.option import Option
|
|
8
|
+
from click_extended.core.decorators.prompt import Prompt
|
|
9
|
+
from click_extended.core.decorators.selection import Selection
|
|
10
|
+
from click_extended.core.decorators.tag import Tag
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Argument",
|
|
14
|
+
"Command",
|
|
15
|
+
"Env",
|
|
16
|
+
"Group",
|
|
17
|
+
"Option",
|
|
18
|
+
"Prompt",
|
|
19
|
+
"Selection",
|
|
20
|
+
"Tag",
|
|
21
|
+
]
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Class to support arguments in the command line interface."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=too-many-arguments
|
|
4
|
+
# pylint: disable=too-many-positional-arguments
|
|
5
|
+
# pylint: disable=redefined-builtin
|
|
6
|
+
|
|
7
|
+
from builtins import type as builtins_type
|
|
8
|
+
from typing import Any, Type, cast
|
|
9
|
+
|
|
10
|
+
from click_extended.core.nodes.argument_node import ArgumentNode
|
|
11
|
+
from click_extended.core.other.context import Context
|
|
12
|
+
from click_extended.types import Decorator
|
|
13
|
+
from click_extended.utils.casing import Casing
|
|
14
|
+
from click_extended.utils.humanize import humanize_type
|
|
15
|
+
from click_extended.utils.naming import validate_name
|
|
16
|
+
|
|
17
|
+
_MISSING = object()
|
|
18
|
+
SUPPORTED_TYPES = (str, int, float, bool)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Argument(ArgumentNode):
|
|
22
|
+
"""`ArgumentNode` that represents a Click argument."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
name: str,
|
|
27
|
+
param: str | None = None,
|
|
28
|
+
nargs: int = 1,
|
|
29
|
+
type: Type[Any] | Any = None,
|
|
30
|
+
help: str | None = None,
|
|
31
|
+
required: bool = True,
|
|
32
|
+
default: Any = _MISSING,
|
|
33
|
+
tags: str | list[str] | None = None,
|
|
34
|
+
**kwargs: Any,
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
Initialize a new `Argument` instance.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
name (str):
|
|
41
|
+
The argument name in snake_case.
|
|
42
|
+
Examples: "filename", "input_file"
|
|
43
|
+
param (str, optional):
|
|
44
|
+
Custom parameter name for the function.
|
|
45
|
+
If not provided, uses the name directly.
|
|
46
|
+
nargs (int):
|
|
47
|
+
Number of arguments to accept. Use `-1` for unlimited.
|
|
48
|
+
Defaults to `1`.
|
|
49
|
+
type (Any, optional):
|
|
50
|
+
The type to convert the value to (`int`, `str`, `float`, etc.).
|
|
51
|
+
help (str, optional):
|
|
52
|
+
Help text for this argument.
|
|
53
|
+
required (bool):
|
|
54
|
+
Whether this argument is required. Defaults to `True` unless
|
|
55
|
+
`default` is provided, which makes it optional automatically.
|
|
56
|
+
default (Any):
|
|
57
|
+
Default value if not provided. When set, automatically makes
|
|
58
|
+
the argument optional (`required=False`). Defaults to `None`.
|
|
59
|
+
tags (str | list[str], optional):
|
|
60
|
+
Tag(s) to associate with this argument for grouping.
|
|
61
|
+
**kwargs (Any):
|
|
62
|
+
Additional keyword arguments.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: If both `default` is provided and `required=True` is
|
|
66
|
+
explicitly set (detected via kwargs inspection in decorator).
|
|
67
|
+
"""
|
|
68
|
+
validate_name(name, "argument name")
|
|
69
|
+
|
|
70
|
+
param_name = param if param is not None else name
|
|
71
|
+
|
|
72
|
+
validate_name(param_name, "parameter name")
|
|
73
|
+
|
|
74
|
+
if default is not _MISSING and required is True:
|
|
75
|
+
required = False
|
|
76
|
+
|
|
77
|
+
if default is _MISSING:
|
|
78
|
+
default = None
|
|
79
|
+
|
|
80
|
+
if type is None:
|
|
81
|
+
if default is not None:
|
|
82
|
+
type = cast(Type[Any], builtins_type(default)) # type: ignore
|
|
83
|
+
else:
|
|
84
|
+
type = str
|
|
85
|
+
|
|
86
|
+
if type not in SUPPORTED_TYPES:
|
|
87
|
+
types = humanize_type(
|
|
88
|
+
type.__name__ if hasattr(type, "__name__") else type
|
|
89
|
+
)
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"Argument '{name}' has unsupported type '{types}'. "
|
|
92
|
+
"Only basic primitives are supported: str, int, float, bool. "
|
|
93
|
+
"For complex types, use child decorators (e.g., @to_path, "
|
|
94
|
+
"@to_datetime, ..)."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
super().__init__(
|
|
98
|
+
name=name,
|
|
99
|
+
param=param if param is not None else name,
|
|
100
|
+
nargs=nargs,
|
|
101
|
+
type=type,
|
|
102
|
+
help=help,
|
|
103
|
+
required=required,
|
|
104
|
+
default=default,
|
|
105
|
+
tags=tags,
|
|
106
|
+
)
|
|
107
|
+
self.extra_kwargs = kwargs
|
|
108
|
+
|
|
109
|
+
def get_display_name(self) -> str:
|
|
110
|
+
"""
|
|
111
|
+
Get a formatted display name for error messages.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
str:
|
|
115
|
+
The argument name in SCREAMING_SNAKE_CASE.
|
|
116
|
+
"""
|
|
117
|
+
return Casing.to_screaming_snake_case(self.name)
|
|
118
|
+
|
|
119
|
+
def load(
|
|
120
|
+
self,
|
|
121
|
+
value: str | int | float | bool | None,
|
|
122
|
+
context: Context,
|
|
123
|
+
*args: Any,
|
|
124
|
+
**kwargs: Any,
|
|
125
|
+
) -> Any:
|
|
126
|
+
"""
|
|
127
|
+
Load and return the CLI argument value.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
value (str | int | float | bool | None):
|
|
131
|
+
The parsed CLI argument value from Click.
|
|
132
|
+
context (Context):
|
|
133
|
+
The current context instance.
|
|
134
|
+
*args (Any):
|
|
135
|
+
Optional positional arguments.
|
|
136
|
+
**kwargs (Any):
|
|
137
|
+
Optional keyword arguments.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Any:
|
|
141
|
+
The argument value to inject into the function.
|
|
142
|
+
"""
|
|
143
|
+
return value
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def argument(
|
|
147
|
+
name: str,
|
|
148
|
+
param: str | None = None,
|
|
149
|
+
nargs: int = 1,
|
|
150
|
+
type: Type[str | int | float | bool] | None = None,
|
|
151
|
+
help: str | None = None,
|
|
152
|
+
required: bool = True,
|
|
153
|
+
default: Any = _MISSING,
|
|
154
|
+
tags: str | list[str] | None = None,
|
|
155
|
+
**kwargs: Any,
|
|
156
|
+
) -> Decorator:
|
|
157
|
+
"""
|
|
158
|
+
A `ParentNode` decorator to create a Click argument with value injection.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
name (str):
|
|
162
|
+
The argument name in snake_case.
|
|
163
|
+
Examples: "filename", "input_file"
|
|
164
|
+
param (str, optional):
|
|
165
|
+
Custom parameter name for the function.
|
|
166
|
+
If not provided, uses the name directly.
|
|
167
|
+
nargs (int):
|
|
168
|
+
Number of arguments to accept. Use `-1` for unlimited.
|
|
169
|
+
Defaults to `1`.
|
|
170
|
+
type (Type[str | int | float | bool] | None, optional):
|
|
171
|
+
The type to convert the value to.
|
|
172
|
+
help (str, optional):
|
|
173
|
+
Help text for this argument.
|
|
174
|
+
required (bool):
|
|
175
|
+
Whether this argument is required. Defaults to `True` unless
|
|
176
|
+
`default` is provided, which automatically makes it optional.
|
|
177
|
+
default (Any):
|
|
178
|
+
Default value if not provided. When set, automatically makes
|
|
179
|
+
the argument optional (`required=False`). Defaults to `None`.
|
|
180
|
+
tags (str | list[str], optional):
|
|
181
|
+
Tag(s) to associate with this argument for grouping.
|
|
182
|
+
**kwargs (Any):
|
|
183
|
+
Additional Click argument parameters.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Decorator:
|
|
187
|
+
A decorator function that registers the argument parent node.
|
|
188
|
+
|
|
189
|
+
Examples:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
@argument("filename")
|
|
193
|
+
def my_func(filename):
|
|
194
|
+
print(f"File: {filename}")
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
@argument("files", nargs=-1, help="Files to process")
|
|
199
|
+
def my_func(files):
|
|
200
|
+
for file in files:
|
|
201
|
+
print(f"Processing: {file}")
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
@argument("port", type=int, default=8080)
|
|
206
|
+
def my_func(port):
|
|
207
|
+
print(f"Port: {port}")
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
# Custom parameter name
|
|
212
|
+
@argument("input_file", param="infile")
|
|
213
|
+
def my_func(infile): # param: infile, CLI: INPUT_FILE
|
|
214
|
+
print(f"Input: {infile}")
|
|
215
|
+
```
|
|
216
|
+
"""
|
|
217
|
+
return Argument.as_decorator(
|
|
218
|
+
name=name,
|
|
219
|
+
param=param,
|
|
220
|
+
nargs=nargs,
|
|
221
|
+
type=type,
|
|
222
|
+
help=help,
|
|
223
|
+
required=required,
|
|
224
|
+
default=default,
|
|
225
|
+
tags=tags,
|
|
226
|
+
**kwargs,
|
|
227
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Command implementation for the `click_extended` library."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=redefined-builtin
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from click_extended.core.nodes._root_node import RootNode
|
|
10
|
+
from click_extended.core.other._click_command import ClickCommand
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Command(RootNode):
|
|
14
|
+
"""Command implementation for the `click_extended` library."""
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def _get_click_decorator(cls) -> Callable[..., Any]:
|
|
18
|
+
"""Return the click.command decorator (no longer used)."""
|
|
19
|
+
return click.command
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def _get_click_cls(cls) -> type[click.Command]: # type: ignore[override]
|
|
23
|
+
"""Return the ClickCommand class."""
|
|
24
|
+
return ClickCommand
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def wrap(
|
|
28
|
+
cls,
|
|
29
|
+
wrapped_func: Callable[..., Any],
|
|
30
|
+
name: str,
|
|
31
|
+
instance: RootNode,
|
|
32
|
+
**kwargs: Any,
|
|
33
|
+
) -> ClickCommand:
|
|
34
|
+
"""Override to return proper ClickCommand type."""
|
|
35
|
+
return super().wrap(
|
|
36
|
+
wrapped_func,
|
|
37
|
+
name,
|
|
38
|
+
instance,
|
|
39
|
+
**kwargs,
|
|
40
|
+
) # type: ignore[return-value]
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def as_decorator(
|
|
44
|
+
cls, name: str | None = None, /, **kwargs: Any
|
|
45
|
+
) -> Callable[[Callable[..., Any]], ClickCommand]:
|
|
46
|
+
"""Override to return proper ClickCommand type."""
|
|
47
|
+
return super().as_decorator(
|
|
48
|
+
name,
|
|
49
|
+
**kwargs,
|
|
50
|
+
) # type: ignore[return-value]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def command(
|
|
54
|
+
name: str | None = None,
|
|
55
|
+
*,
|
|
56
|
+
aliases: str | list[str] | None = None,
|
|
57
|
+
help: str | None = None,
|
|
58
|
+
**kwargs: Any,
|
|
59
|
+
) -> Callable[[Callable[..., Any]], ClickCommand]:
|
|
60
|
+
"""
|
|
61
|
+
A `ParentNode` decorator to create a click command with value injection
|
|
62
|
+
from parent nodes.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
name (str, optional):
|
|
66
|
+
The name of the command. If `None`, uses the
|
|
67
|
+
decorated function's name.
|
|
68
|
+
aliases (str | list[str], optional):
|
|
69
|
+
Alternative name(s) for the command. Can be a single
|
|
70
|
+
string or a list of strings.
|
|
71
|
+
help (str, optional):
|
|
72
|
+
The help message for the command. If not provided,
|
|
73
|
+
uses the first line of the function's docstring.
|
|
74
|
+
**kwargs (Any):
|
|
75
|
+
Additional arguments to pass to `click.Command`.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Callable:
|
|
79
|
+
A decorator function that returns a Click command.
|
|
80
|
+
"""
|
|
81
|
+
if aliases is not None:
|
|
82
|
+
kwargs["aliases"] = aliases
|
|
83
|
+
if help is not None:
|
|
84
|
+
kwargs["help"] = help
|
|
85
|
+
|
|
86
|
+
def decorator(func: Callable[..., Any]) -> ClickCommand:
|
|
87
|
+
if help is None and func.__doc__:
|
|
88
|
+
first_line = func.__doc__.strip().split("\n")[0].strip()
|
|
89
|
+
if first_line:
|
|
90
|
+
kwargs["help"] = first_line
|
|
91
|
+
return Command.as_decorator(name, **kwargs)(func)
|
|
92
|
+
|
|
93
|
+
return decorator
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""`ParentNode` that loads a value from an environment variable."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=too-many-arguments
|
|
4
|
+
# pylint: disable=too-many-positional-arguments
|
|
5
|
+
# pylint: disable=redefined-builtin
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any, Callable, ParamSpec, TypeVar, cast
|
|
9
|
+
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
|
|
12
|
+
from click_extended.core.nodes.parent_node import ParentNode
|
|
13
|
+
from click_extended.core.other.context import Context
|
|
14
|
+
from click_extended.utils.casing import Casing
|
|
15
|
+
|
|
16
|
+
load_dotenv()
|
|
17
|
+
|
|
18
|
+
P = ParamSpec("P")
|
|
19
|
+
T = TypeVar("T")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Env(ParentNode):
|
|
23
|
+
"""`ParentNode` that loads a value from an environment variable."""
|
|
24
|
+
|
|
25
|
+
def load(self, context: Context, *args: Any, **kwargs: Any) -> Any:
|
|
26
|
+
"""
|
|
27
|
+
Load and return the environment variable value.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
context (Context):
|
|
31
|
+
The current context instance.
|
|
32
|
+
*args (Any):
|
|
33
|
+
Optional positional arguments.
|
|
34
|
+
**kwargs (Any):
|
|
35
|
+
Keyword arguments from the decorator, including:
|
|
36
|
+
- env_name (str): The environment variable name to read.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Any:
|
|
40
|
+
The value of the environment variable, or the default value
|
|
41
|
+
if not required and not set.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError:
|
|
45
|
+
If the environment variable is required but not set.
|
|
46
|
+
"""
|
|
47
|
+
env_name = kwargs.get("env_name")
|
|
48
|
+
if env_name is None:
|
|
49
|
+
raise ValueError("env_name must be provided")
|
|
50
|
+
|
|
51
|
+
value = os.getenv(env_name)
|
|
52
|
+
|
|
53
|
+
if value is None:
|
|
54
|
+
if self.required:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"Required environment variable '{env_name}' " "is not set."
|
|
57
|
+
)
|
|
58
|
+
value = self.default
|
|
59
|
+
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
def get_display_name(self) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Get a formatted display name for error messages.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
str:
|
|
68
|
+
The environment variable name in SCREAMING_SNAKE_CASE.
|
|
69
|
+
"""
|
|
70
|
+
return Casing.to_screaming_snake_case(self.name)
|
|
71
|
+
|
|
72
|
+
def check_required(self) -> str | None:
|
|
73
|
+
"""
|
|
74
|
+
Check if required environment variable is set.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
str | None:
|
|
78
|
+
The name of the missing environment variable if required
|
|
79
|
+
and not set, otherwise None.
|
|
80
|
+
"""
|
|
81
|
+
env_name = self.decorator_kwargs.get("env_name")
|
|
82
|
+
if env_name and self.required and os.getenv(env_name) is None:
|
|
83
|
+
return cast(str, env_name)
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def env(
|
|
88
|
+
env_name: str,
|
|
89
|
+
name: str | None = None,
|
|
90
|
+
param: str | None = None,
|
|
91
|
+
help: str | None = None,
|
|
92
|
+
required: bool = False,
|
|
93
|
+
default: Any = None,
|
|
94
|
+
tags: str | list[str] | None = None,
|
|
95
|
+
**kwargs: Any,
|
|
96
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
97
|
+
"""
|
|
98
|
+
A `ParentNode` decorator to inject an environment variable value
|
|
99
|
+
into a command.
|
|
100
|
+
|
|
101
|
+
Type: `ParentNode`
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
env_name (str):
|
|
105
|
+
The name of the environment variable to read (e.g., "API_KEY").
|
|
106
|
+
Can be in any format, typically SCREAMING_SNAKE_CASE.
|
|
107
|
+
name (str, optional):
|
|
108
|
+
Internal node name (must be snake_case). If not provided,
|
|
109
|
+
uses env_name converted to snake_case.
|
|
110
|
+
param (str, optional):
|
|
111
|
+
The parameter name to inject into the function.
|
|
112
|
+
If not provided, uses name (or derived name).
|
|
113
|
+
help (str, optional):
|
|
114
|
+
Help text for this parameter.
|
|
115
|
+
required (bool):
|
|
116
|
+
Whether this parameter is required. Defaults to `False`.
|
|
117
|
+
If `True`, the environment variable must be set, even if
|
|
118
|
+
a default value is provided. The default is ignored when
|
|
119
|
+
`required=True`.
|
|
120
|
+
default (Any):
|
|
121
|
+
Default value if environment variable is not set and
|
|
122
|
+
`required=False`. Defaults to `None`.
|
|
123
|
+
tags (str | list[str], optional):
|
|
124
|
+
Tag(s) to associate with this parameter for grouping.
|
|
125
|
+
**kwargs (Any):
|
|
126
|
+
Additional keyword arguments.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Callable:
|
|
130
|
+
A decorator function that registers the env parent node.
|
|
131
|
+
|
|
132
|
+
Examples:
|
|
133
|
+
>>> @env("API_KEY")
|
|
134
|
+
... def my_func(api_key):
|
|
135
|
+
... print(api_key)
|
|
136
|
+
|
|
137
|
+
>>> @env("DATABASE_URL", param="db", required=True)
|
|
138
|
+
... def my_func(db):
|
|
139
|
+
... print(db)
|
|
140
|
+
|
|
141
|
+
>>> @env("MY_ENV", param="api_key")
|
|
142
|
+
... def my_func(api_key):
|
|
143
|
+
... print(api_key)
|
|
144
|
+
"""
|
|
145
|
+
node_name = name if name is not None else Casing.to_snake_case(env_name)
|
|
146
|
+
return Env.as_decorator(
|
|
147
|
+
name=node_name,
|
|
148
|
+
env_name=env_name,
|
|
149
|
+
param=param,
|
|
150
|
+
help=help,
|
|
151
|
+
required=required,
|
|
152
|
+
default=default,
|
|
153
|
+
tags=tags,
|
|
154
|
+
**kwargs,
|
|
155
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Group implementation for the `click_extended` library."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=protected-access
|
|
4
|
+
# pylint: disable=redefined-builtin
|
|
5
|
+
# pylint: disable=too-many-locals
|
|
6
|
+
# pylint: disable=too-many-branches
|
|
7
|
+
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from click_extended.core.nodes._root_node import RootNode
|
|
13
|
+
from click_extended.core.other._click_group import ClickGroup
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Group(RootNode):
|
|
17
|
+
"""Group implementation for the `click_extended` library."""
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def _get_click_decorator(cls) -> Callable[..., Any]:
|
|
21
|
+
"""Return the click.group decorator (no longer used)."""
|
|
22
|
+
return click.group
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def _get_click_cls(cls) -> type[click.Command]: # type: ignore[override]
|
|
26
|
+
"""Return the ClickGroup class."""
|
|
27
|
+
return ClickGroup
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def wrap(
|
|
31
|
+
cls,
|
|
32
|
+
wrapped_func: Callable[..., Any],
|
|
33
|
+
name: str,
|
|
34
|
+
instance: RootNode,
|
|
35
|
+
**kwargs: Any,
|
|
36
|
+
) -> ClickGroup:
|
|
37
|
+
"""Override to return proper ClickGroup type."""
|
|
38
|
+
return super().wrap(
|
|
39
|
+
wrapped_func,
|
|
40
|
+
name,
|
|
41
|
+
instance,
|
|
42
|
+
**kwargs,
|
|
43
|
+
) # type: ignore[return-value]
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def as_decorator(
|
|
47
|
+
cls, name: str | None = None, /, **kwargs: Any
|
|
48
|
+
) -> Callable[[Callable[..., Any]], ClickGroup]:
|
|
49
|
+
"""Override to return proper ClickGroup type."""
|
|
50
|
+
return super().as_decorator(
|
|
51
|
+
name,
|
|
52
|
+
**kwargs,
|
|
53
|
+
) # type: ignore[return-value]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def group(
|
|
57
|
+
name: str | None = None,
|
|
58
|
+
*,
|
|
59
|
+
aliases: str | list[str] | None = None,
|
|
60
|
+
help: str | None = None,
|
|
61
|
+
**kwargs: Any,
|
|
62
|
+
) -> Callable[[Callable[..., Any]], ClickGroup]:
|
|
63
|
+
"""
|
|
64
|
+
A `RootNode` decorator to create a click group with value injection
|
|
65
|
+
from parent nodes.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
name (str, optional):
|
|
69
|
+
The name of the group. If `None`, uses the
|
|
70
|
+
decorated function's name.
|
|
71
|
+
aliases (str | list[str], optional):
|
|
72
|
+
Alternative name(s) for the group. Can be a single
|
|
73
|
+
string or a list of strings.
|
|
74
|
+
help (str, optional):
|
|
75
|
+
The help message for the group. If not provided,
|
|
76
|
+
uses the first line of the function's docstring.
|
|
77
|
+
**kwargs (Any):
|
|
78
|
+
Additional arguments to pass to `click.Group`.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Callable:
|
|
82
|
+
A decorator function that returns a ClickGroup.
|
|
83
|
+
"""
|
|
84
|
+
if aliases is not None:
|
|
85
|
+
kwargs["aliases"] = aliases
|
|
86
|
+
if help is not None:
|
|
87
|
+
kwargs["help"] = help
|
|
88
|
+
|
|
89
|
+
def decorator(func: Callable[..., Any]) -> ClickGroup:
|
|
90
|
+
if help is None and func.__doc__:
|
|
91
|
+
first_line = func.__doc__.strip().split("\n")[0].strip()
|
|
92
|
+
if first_line:
|
|
93
|
+
kwargs["help"] = first_line
|
|
94
|
+
return Group.as_decorator(name, **kwargs)(func)
|
|
95
|
+
|
|
96
|
+
return decorator
|