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,190 @@
|
|
|
1
|
+
"""Requires decorator for enforcing parameter dependencies."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from click_extended.core.decorators.tag import Tag
|
|
6
|
+
from click_extended.core.nodes.child_node import ChildNode
|
|
7
|
+
from click_extended.core.nodes.parent_node import ParentNode
|
|
8
|
+
from click_extended.core.other.context import Context
|
|
9
|
+
from click_extended.types import Decorator
|
|
10
|
+
from click_extended.utils.humanize import humanize_iterable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Requires(ChildNode):
|
|
14
|
+
"""Child node to enforce that required parameters are provided."""
|
|
15
|
+
|
|
16
|
+
def handle_all(
|
|
17
|
+
self, value: Any, context: Context, *args: Any, **kwargs: Any
|
|
18
|
+
) -> Any:
|
|
19
|
+
parent = context.parent
|
|
20
|
+
if parent is None or not isinstance(parent, ParentNode):
|
|
21
|
+
return value
|
|
22
|
+
|
|
23
|
+
if not parent.was_provided:
|
|
24
|
+
return value
|
|
25
|
+
|
|
26
|
+
if missing := self._get_missing_requirements(context, args):
|
|
27
|
+
parent_display = parent.get_display_name()
|
|
28
|
+
missing_display = humanize_iterable(
|
|
29
|
+
[self._get_display_name(name, context) for name in missing],
|
|
30
|
+
wrap="'",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"'{parent_display}' requires {missing_display} to be provided."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
def handle_tag(
|
|
40
|
+
self, value: dict[str, Any], context: Context, *args: Any, **kwargs: Any
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Validate requirements for tags."""
|
|
43
|
+
tag = context.parent
|
|
44
|
+
if tag is None or not isinstance(tag, Tag):
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
tagged_parents = getattr(tag, "parent_nodes", [])
|
|
48
|
+
if not tagged_parents:
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
provided_count = sum(1 for p in tagged_parents if p.was_provided)
|
|
52
|
+
|
|
53
|
+
require_all_tagged = kwargs.get("require_all_tagged", True)
|
|
54
|
+
should_check = False
|
|
55
|
+
if require_all_tagged:
|
|
56
|
+
should_check = provided_count > 0
|
|
57
|
+
else:
|
|
58
|
+
should_check = provided_count == len(tagged_parents)
|
|
59
|
+
|
|
60
|
+
if not should_check:
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
if missing := self._get_missing_requirements(context, args):
|
|
64
|
+
tag_display = f"tag '{tag.name}'"
|
|
65
|
+
missing_display = humanize_iterable(
|
|
66
|
+
[self._get_display_name(name, context) for name in missing],
|
|
67
|
+
wrap="'",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"'{tag_display}' requires {missing_display} to be provided."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def _get_missing_requirements(
|
|
75
|
+
self, context: Context, required_names: tuple[Any, ...]
|
|
76
|
+
) -> list[str]:
|
|
77
|
+
missing: list[str] = []
|
|
78
|
+
|
|
79
|
+
for name in required_names:
|
|
80
|
+
parent = context.get_parent(name)
|
|
81
|
+
if parent is not None:
|
|
82
|
+
if not parent.was_provided:
|
|
83
|
+
missing.append(name)
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
if (tag := context.get_tag(name)) is not None:
|
|
87
|
+
tagged_parents = getattr(tag, "parent_nodes", [])
|
|
88
|
+
if not any(p.was_provided for p in tagged_parents):
|
|
89
|
+
missing.append(name)
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"Required parameter or tag '{name}' does not exist. "
|
|
94
|
+
f"Check that it is defined in the command."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return missing
|
|
98
|
+
|
|
99
|
+
def _get_display_name(self, name: str, context: Context) -> str:
|
|
100
|
+
parent = context.get_parent(name)
|
|
101
|
+
if parent is not None:
|
|
102
|
+
return parent.get_display_name()
|
|
103
|
+
|
|
104
|
+
tag = context.get_tag(name)
|
|
105
|
+
if tag is not None:
|
|
106
|
+
return f"tag '{tag.name}'"
|
|
107
|
+
|
|
108
|
+
return name
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def requires(*names: str, require_all_tagged: bool = True) -> Decorator:
|
|
112
|
+
"""
|
|
113
|
+
Enforce that specified parameters or tags are provided.
|
|
114
|
+
|
|
115
|
+
Type: `ChildNode`
|
|
116
|
+
|
|
117
|
+
Supports: `any`, `tag`
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
*names (str):
|
|
121
|
+
Names of parameters or tags that must be provided.
|
|
122
|
+
require_all_tagged (bool):
|
|
123
|
+
If a tag is references, all parents must be provided.
|
|
124
|
+
Defaults to `True`.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Decorator:
|
|
128
|
+
The decorated function.
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
ValueError:
|
|
132
|
+
If the parent/tag is provided but required dependencies are not met,
|
|
133
|
+
or if a required name doesn't exist.
|
|
134
|
+
|
|
135
|
+
Examples:
|
|
136
|
+
Basic requirement:
|
|
137
|
+
```python
|
|
138
|
+
@command()
|
|
139
|
+
@option("--output")
|
|
140
|
+
@requires("input")
|
|
141
|
+
@option("--input")
|
|
142
|
+
def my_command(input: str, output: str):
|
|
143
|
+
# --output requires --input to be provided
|
|
144
|
+
pass
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Multiple requirements:
|
|
148
|
+
```python
|
|
149
|
+
@command()
|
|
150
|
+
@option("--save")
|
|
151
|
+
@requires("format", "output")
|
|
152
|
+
@option("--format")
|
|
153
|
+
@option("--output")
|
|
154
|
+
def my_command(save: bool, format: str, output: str):
|
|
155
|
+
# --save requires both --format and --output
|
|
156
|
+
pass
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Tag requirements:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
@command()
|
|
163
|
+
@tag("database")
|
|
164
|
+
@requires("host", "port")
|
|
165
|
+
@option("--database-name")
|
|
166
|
+
@option("--database-user")
|
|
167
|
+
@option("--host")
|
|
168
|
+
@option("--port")
|
|
169
|
+
def my_command(**kwargs):
|
|
170
|
+
pass
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Tag with require_all_tagged=False:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
@command()
|
|
177
|
+
@tag("advanced")
|
|
178
|
+
@requires("config", require_all_tagged=False)
|
|
179
|
+
# Only if ALL advanced options provided, require config
|
|
180
|
+
@option("--verbose")
|
|
181
|
+
@option("--debug")
|
|
182
|
+
@option("--config")
|
|
183
|
+
def my_command(**kwargs):
|
|
184
|
+
pass
|
|
185
|
+
```
|
|
186
|
+
"""
|
|
187
|
+
return Requires.as_decorator(*names, require_all_tagged=require_all_tagged)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
__all__ = ["requires", "Requires"]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Child decorator to check if a string starts with one or more substrings."""
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
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.humanize import humanize_iterable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class StartsWith(ChildNode):
|
|
14
|
+
"""
|
|
15
|
+
Child decorator to check if a string starts with one or more substrings.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def handle_str(
|
|
19
|
+
self, value: str, context: Context, *args: Any, **kwargs: Any
|
|
20
|
+
) -> Any:
|
|
21
|
+
patterns: tuple[str | re.Pattern[str], ...] = kwargs.get("text", ())
|
|
22
|
+
matches: list[str | re.Pattern[str]] = []
|
|
23
|
+
|
|
24
|
+
for pattern in patterns:
|
|
25
|
+
if isinstance(pattern, re.Pattern):
|
|
26
|
+
if pattern.match(value):
|
|
27
|
+
matches.append(pattern)
|
|
28
|
+
elif isinstance(pattern, str) and any(
|
|
29
|
+
char in pattern for char in ["*", "?", "[", "]"]
|
|
30
|
+
):
|
|
31
|
+
if fnmatch.fnmatch(
|
|
32
|
+
value,
|
|
33
|
+
f"{pattern}*" if not pattern.endswith("*") else pattern,
|
|
34
|
+
):
|
|
35
|
+
matches.append(pattern)
|
|
36
|
+
elif isinstance(pattern, str):
|
|
37
|
+
if value.startswith(pattern):
|
|
38
|
+
matches.append(pattern)
|
|
39
|
+
|
|
40
|
+
if not matches:
|
|
41
|
+
pattern_strs: list[str] = [
|
|
42
|
+
p.pattern if isinstance(p, re.Pattern) else p for p in patterns
|
|
43
|
+
]
|
|
44
|
+
pattern_list = humanize_iterable(
|
|
45
|
+
pattern_strs,
|
|
46
|
+
wrap="'",
|
|
47
|
+
sep="or",
|
|
48
|
+
prefix_singular="the pattern",
|
|
49
|
+
prefix_plural="one of the patterns",
|
|
50
|
+
)
|
|
51
|
+
raise ValueError(f"Value must start with {pattern_list}")
|
|
52
|
+
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def starts_with(*text: str | re.Pattern[str]) -> Decorator:
|
|
57
|
+
"""
|
|
58
|
+
Check if a string starts with one or more substrings or patterns.
|
|
59
|
+
|
|
60
|
+
Type: `ChildNode`
|
|
61
|
+
|
|
62
|
+
Supports: `str`
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
*text (str | re.Pattern[str]):
|
|
66
|
+
Patterns to check for.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Decorator:
|
|
70
|
+
The decorated function.
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
```python
|
|
74
|
+
# Exact prefix
|
|
75
|
+
@starts_with("http://", "https://")
|
|
76
|
+
|
|
77
|
+
# Glob pattern
|
|
78
|
+
@starts_with("user_*", "admin_*")
|
|
79
|
+
|
|
80
|
+
# Regex matching
|
|
81
|
+
@starts_with(re.compile(r"https?://"))
|
|
82
|
+
|
|
83
|
+
# Mixed patterns
|
|
84
|
+
@starts_with("www.", re.compile(r"https?://"), "ftp_*")
|
|
85
|
+
```
|
|
86
|
+
"""
|
|
87
|
+
return StartsWith.as_decorator(text=text)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Check if a value is truthy."""
|
|
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 Truthy(ChildNode):
|
|
11
|
+
"""Check if a value is truthy."""
|
|
12
|
+
|
|
13
|
+
def handle_all(
|
|
14
|
+
self,
|
|
15
|
+
value: Any,
|
|
16
|
+
context: Context,
|
|
17
|
+
*args: Any,
|
|
18
|
+
**kwargs: Any,
|
|
19
|
+
) -> Any:
|
|
20
|
+
if not value:
|
|
21
|
+
raise ValueError(f"Value '{value}' is not truthy.")
|
|
22
|
+
return value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def truthy() -> Decorator:
|
|
26
|
+
"""
|
|
27
|
+
Check if a value is truthy.
|
|
28
|
+
|
|
29
|
+
Type: `ChildNode`
|
|
30
|
+
|
|
31
|
+
Supports: `Any`
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Decorator:
|
|
35
|
+
The decorated function.
|
|
36
|
+
"""
|
|
37
|
+
return Truthy.as_decorator()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Initialization file for the `click_extended.decorators.compare` module."""
|
|
2
|
+
|
|
3
|
+
from click_extended.decorators.compare.at_least import at_least
|
|
4
|
+
from click_extended.decorators.compare.at_most import at_most
|
|
5
|
+
from click_extended.decorators.compare.between import between
|
|
6
|
+
from click_extended.decorators.compare.greater_than import greater_than
|
|
7
|
+
from click_extended.decorators.compare.less_than import less_than
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"at_least",
|
|
11
|
+
"at_most",
|
|
12
|
+
"between",
|
|
13
|
+
"greater_than",
|
|
14
|
+
"less_than",
|
|
15
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""A child decorator to check if at least n number or arguments are provided."""
|
|
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
|
+
from click_extended.utils.humanize import humanize_iterable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AtLeast(ChildNode):
|
|
12
|
+
"""
|
|
13
|
+
A child decorator to check if at least n
|
|
14
|
+
number or arguments are provided.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def handle_tag(
|
|
18
|
+
self,
|
|
19
|
+
value: Any,
|
|
20
|
+
context: Context,
|
|
21
|
+
*args: Any,
|
|
22
|
+
**kwargs: Any,
|
|
23
|
+
) -> None:
|
|
24
|
+
if context.parent is None:
|
|
25
|
+
raise EnvironmentError("Parent is not defined.")
|
|
26
|
+
|
|
27
|
+
parents = context.get_tagged(context.parent.name)
|
|
28
|
+
required = kwargs["n"]
|
|
29
|
+
provided = sum(p.was_provided for p in parents)
|
|
30
|
+
|
|
31
|
+
if provided < required:
|
|
32
|
+
humanized = humanize_iterable([p.name for p in parents], wrap="'")
|
|
33
|
+
word = "was" if provided == 1 else "were"
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"At least {required} of {humanized} must be provided, "
|
|
36
|
+
f"but only {provided} {word} given."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def at_least(n: int) -> Decorator:
|
|
41
|
+
"""
|
|
42
|
+
Checks if at least `n` number of arguments
|
|
43
|
+
are provided.
|
|
44
|
+
|
|
45
|
+
Type: `ChildNode`
|
|
46
|
+
|
|
47
|
+
Supports: `Tag`
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
n (int):
|
|
51
|
+
The number of arguments required.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Decorator:
|
|
55
|
+
The decorated function.
|
|
56
|
+
"""
|
|
57
|
+
return AtLeast.as_decorator(n=n)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""A child decorator to check if at most n number or arguments are provided."""
|
|
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
|
+
from click_extended.utils.humanize import humanize_iterable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AtMost(ChildNode):
|
|
12
|
+
"""
|
|
13
|
+
A child decorator to check if at most n
|
|
14
|
+
number or arguments are provided.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def handle_tag(
|
|
18
|
+
self,
|
|
19
|
+
value: Any,
|
|
20
|
+
context: Context,
|
|
21
|
+
*args: Any,
|
|
22
|
+
**kwargs: Any,
|
|
23
|
+
) -> None:
|
|
24
|
+
if context.parent is None:
|
|
25
|
+
raise EnvironmentError("Parent is not defined.")
|
|
26
|
+
|
|
27
|
+
parents = context.get_tagged(context.parent.name)
|
|
28
|
+
maximum = kwargs["n"]
|
|
29
|
+
provided = sum(p.was_provided for p in parents)
|
|
30
|
+
|
|
31
|
+
if provided > maximum:
|
|
32
|
+
humanized = humanize_iterable([p.name for p in parents], wrap="'")
|
|
33
|
+
word = "was" if provided == 1 else "were"
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"At most {maximum} of {humanized} can be provided, "
|
|
36
|
+
f"but {provided} {word} given."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def at_most(n: int) -> Decorator:
|
|
41
|
+
"""
|
|
42
|
+
Checks if at most `n` number of arguments
|
|
43
|
+
are provided.
|
|
44
|
+
|
|
45
|
+
Type: `ChildNode`
|
|
46
|
+
|
|
47
|
+
Supports: `Tag`
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
n (int):
|
|
51
|
+
The maximum number of arguments allowed.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Decorator:
|
|
55
|
+
The decorated function.
|
|
56
|
+
"""
|
|
57
|
+
return AtMost.as_decorator(n=n)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Check if a value is between two bounds."""
|
|
2
|
+
|
|
3
|
+
from datetime import date, 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
|
+
|
|
10
|
+
|
|
11
|
+
class Between(ChildNode):
|
|
12
|
+
"""Check if a value is between two bounds."""
|
|
13
|
+
|
|
14
|
+
def _check_between(
|
|
15
|
+
self,
|
|
16
|
+
value: Any,
|
|
17
|
+
lower: Any,
|
|
18
|
+
upper: Any,
|
|
19
|
+
inclusive: bool,
|
|
20
|
+
) -> Any:
|
|
21
|
+
if inclusive:
|
|
22
|
+
if not lower <= value <= upper:
|
|
23
|
+
raise ValueError(
|
|
24
|
+
f"Value '{value}' is not between '{lower}' and '{upper}'."
|
|
25
|
+
)
|
|
26
|
+
else:
|
|
27
|
+
if not lower < value < upper:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
f"Value '{value}' is not between '{lower}' and '{upper}'."
|
|
30
|
+
)
|
|
31
|
+
return value
|
|
32
|
+
|
|
33
|
+
def handle_numeric(
|
|
34
|
+
self,
|
|
35
|
+
value: int | float,
|
|
36
|
+
context: Context,
|
|
37
|
+
*args: Any,
|
|
38
|
+
**kwargs: Any,
|
|
39
|
+
) -> Any:
|
|
40
|
+
return self._check_between(
|
|
41
|
+
value,
|
|
42
|
+
kwargs["lower"],
|
|
43
|
+
kwargs["upper"],
|
|
44
|
+
kwargs["inclusive"],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def handle_time(
|
|
48
|
+
self,
|
|
49
|
+
value: time,
|
|
50
|
+
context: Context,
|
|
51
|
+
*args: Any,
|
|
52
|
+
**kwargs: Any,
|
|
53
|
+
) -> Any:
|
|
54
|
+
return self._check_between(
|
|
55
|
+
value,
|
|
56
|
+
kwargs["lower"],
|
|
57
|
+
kwargs["upper"],
|
|
58
|
+
kwargs["inclusive"],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def handle_date(
|
|
62
|
+
self,
|
|
63
|
+
value: date,
|
|
64
|
+
context: Context,
|
|
65
|
+
*args: Any,
|
|
66
|
+
**kwargs: Any,
|
|
67
|
+
) -> Any:
|
|
68
|
+
return self._check_between(
|
|
69
|
+
value,
|
|
70
|
+
kwargs["lower"],
|
|
71
|
+
kwargs["upper"],
|
|
72
|
+
kwargs["inclusive"],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def handle_datetime(
|
|
76
|
+
self,
|
|
77
|
+
value: datetime,
|
|
78
|
+
context: Context,
|
|
79
|
+
*args: Any,
|
|
80
|
+
**kwargs: Any,
|
|
81
|
+
) -> Any:
|
|
82
|
+
return self._check_between(
|
|
83
|
+
value,
|
|
84
|
+
kwargs["lower"],
|
|
85
|
+
kwargs["upper"],
|
|
86
|
+
kwargs["inclusive"],
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def between(
|
|
91
|
+
lower: int | float | date | time | datetime,
|
|
92
|
+
upper: int | float | date | time | datetime,
|
|
93
|
+
inclusive: bool = True,
|
|
94
|
+
) -> Decorator:
|
|
95
|
+
"""
|
|
96
|
+
Check if a value is between two bounds where the bounds must be of
|
|
97
|
+
the same type.
|
|
98
|
+
|
|
99
|
+
Type: `ChildNode`
|
|
100
|
+
|
|
101
|
+
Supports: `int`, `float`, `date`, `time`, `datetime`
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
lower (int | float | date | time | datetime):
|
|
105
|
+
The lower bound to check.
|
|
106
|
+
upper (int | float | date | time | datetime):
|
|
107
|
+
The upper bound to check.
|
|
108
|
+
inclusive (bool, optional):
|
|
109
|
+
Whether to include the bounds or not. Defaults to `True`.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Decorator:
|
|
113
|
+
The decorated function.
|
|
114
|
+
"""
|
|
115
|
+
return Between.as_decorator(
|
|
116
|
+
lower=lower,
|
|
117
|
+
upper=upper,
|
|
118
|
+
inclusive=inclusive,
|
|
119
|
+
)
|