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,183 @@
1
+ """Child decorator to validate that a value is more than a threshold."""
2
+
3
+ from datetime import date, datetime, time
4
+ from decimal import Decimal
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
+
11
+
12
+ class GreaterThan(ChildNode):
13
+ """Child decorator to validate that a value is more than a threshold."""
14
+
15
+ def handle_numeric(
16
+ self,
17
+ value: int | float | Decimal,
18
+ context: Context,
19
+ *args: Any,
20
+ **kwargs: Any,
21
+ ) -> int | float | Decimal:
22
+ threshold = kwargs["threshold"]
23
+ inclusive = kwargs["inclusive"]
24
+
25
+ if inclusive:
26
+ if value < threshold:
27
+ raise ValueError(
28
+ f"Value must be at least {threshold}, got {value}"
29
+ )
30
+ else:
31
+ if value <= threshold:
32
+ raise ValueError(
33
+ f"Value must be more than {threshold}, got {value}"
34
+ )
35
+
36
+ return value
37
+
38
+ def handle_datetime(
39
+ self,
40
+ value: datetime,
41
+ context: Context,
42
+ *args: Any,
43
+ **kwargs: Any,
44
+ ) -> datetime:
45
+ threshold = kwargs["threshold"]
46
+ inclusive = kwargs["inclusive"]
47
+
48
+ if inclusive:
49
+ if value < threshold:
50
+ raise ValueError(
51
+ f"Value must be at least {threshold}, got {value}"
52
+ )
53
+ else:
54
+ if value <= threshold:
55
+ raise ValueError(
56
+ f"Value must be more than {threshold}, got {value}"
57
+ )
58
+
59
+ return value
60
+
61
+ def handle_date(
62
+ self,
63
+ value: date,
64
+ context: Context,
65
+ *args: Any,
66
+ **kwargs: Any,
67
+ ) -> date:
68
+ threshold = kwargs["threshold"]
69
+ inclusive = kwargs["inclusive"]
70
+
71
+ if inclusive:
72
+ if value < threshold:
73
+ raise ValueError(
74
+ f"Value must be at least {threshold}, got {value}"
75
+ )
76
+ else:
77
+ if value <= threshold:
78
+ raise ValueError(
79
+ f"Value must be more than {threshold}, got {value}"
80
+ )
81
+
82
+ return value
83
+
84
+ def handle_time(
85
+ self,
86
+ value: time,
87
+ context: Context,
88
+ *args: Any,
89
+ **kwargs: Any,
90
+ ) -> time:
91
+ threshold = kwargs["threshold"]
92
+ inclusive = kwargs["inclusive"]
93
+
94
+ if inclusive:
95
+ if value < threshold:
96
+ raise ValueError(
97
+ f"Value must be at least {threshold}, got {value}"
98
+ )
99
+ else:
100
+ if value <= threshold:
101
+ raise ValueError(
102
+ f"Value must be more than {threshold}, got {value}"
103
+ )
104
+
105
+ return value
106
+
107
+
108
+ def greater_than(
109
+ threshold: int | float | Decimal | datetime | date | time,
110
+ inclusive: bool = False,
111
+ ) -> Decorator:
112
+ """
113
+ Validate that a value is more than a threshold.
114
+
115
+ Type: `ChildNode`
116
+
117
+ Supports: `int`, `float`, `Decimal`, `datetime`, `date`, `time`
118
+
119
+ Args:
120
+ threshold (int | float | Decimal | datetime | date | time):
121
+ The threshold value to compare against.
122
+ inclusive (bool):
123
+ If `True`, allows values equal to threshold (>=).
124
+ If `False`, requires values strictly greater (>).
125
+ Defaults to `False`.
126
+
127
+ Raises:
128
+ ValueError:
129
+ If value is not more than (or at least) the threshold.
130
+ TypeError:
131
+ If value type cannot be compared with threshold.
132
+
133
+ Examples:
134
+ Basic numeric validation:
135
+
136
+ >>> @command()
137
+ >>> @option("age", type=int)
138
+ >>> @greater_than(18)
139
+ >>> def cmd(age):
140
+ ... click.echo(f"Age: {age}")
141
+
142
+ >>> # Valid: 19, 20, 100
143
+ >>> # Invalid: 18, 17, 0, -5
144
+
145
+ With inclusive parameter:
146
+
147
+ >>> @command()
148
+ >>> @option("score", type=float)
149
+ >>> @greater_than(0.0, inclusive=True)
150
+ >>> def cmd(score):
151
+ ... click.echo(f"Score: {score}")
152
+
153
+ >>> # Valid: 0.0, 0.1, 100.0
154
+ >>> # Invalid: -0.1, -1.0
155
+
156
+ Datetime validation:
157
+
158
+ >>> @command()
159
+ >>> @option("start_date")
160
+ >>> @to_date()
161
+ >>> @greater_than(date(2024, 1, 1), inclusive=True)
162
+ >>> def cmd(start_date):
163
+ ... click.echo(f"Start: {start_date}")
164
+
165
+ >>> # Valid: 2024-01-01, 2024-12-31, 2025-01-01
166
+ >>> # Invalid: 2023-12-31, 2023-01-01
167
+
168
+ Multiple values (automatic handling):
169
+
170
+ >>> @command()
171
+ >>> @option("numbers", type=int, multiple=True)
172
+ >>> @greater_than(0)
173
+ >>> def cmd(numbers):
174
+ ... click.echo(f"Numbers: {numbers}")
175
+
176
+ >>> # Valid: (1, 2, 3), (5, 10, 15)
177
+ >>> # Invalid: (1, 0, 3), (-1, 2, 3)
178
+ >>> # Note: Validation applies to each number individually
179
+ """
180
+ return GreaterThan.as_decorator(
181
+ threshold=threshold,
182
+ inclusive=inclusive,
183
+ )
@@ -0,0 +1,183 @@
1
+ """Child decorator to validate that a value is less than a threshold."""
2
+
3
+ from datetime import date, datetime, time
4
+ from decimal import Decimal
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
+
11
+
12
+ class LessThan(ChildNode):
13
+ """Child decorator to validate that a value is less than a threshold."""
14
+
15
+ def handle_numeric(
16
+ self,
17
+ value: int | float | Decimal,
18
+ context: Context,
19
+ *args: Any,
20
+ **kwargs: Any,
21
+ ) -> int | float | Decimal:
22
+ threshold = kwargs["threshold"]
23
+ inclusive = kwargs["inclusive"]
24
+
25
+ if inclusive:
26
+ if value > threshold:
27
+ raise ValueError(
28
+ f"Value must be at most {threshold}, got {value}"
29
+ )
30
+ else:
31
+ if value >= threshold:
32
+ raise ValueError(
33
+ f"Value must be less than {threshold}, got {value}"
34
+ )
35
+
36
+ return value
37
+
38
+ def handle_datetime(
39
+ self,
40
+ value: datetime,
41
+ context: Context,
42
+ *args: Any,
43
+ **kwargs: Any,
44
+ ) -> datetime:
45
+ threshold = kwargs["threshold"]
46
+ inclusive = kwargs["inclusive"]
47
+
48
+ if inclusive:
49
+ if value > threshold:
50
+ raise ValueError(
51
+ f"Value must be at most {threshold}, got {value}"
52
+ )
53
+ else:
54
+ if value >= threshold:
55
+ raise ValueError(
56
+ f"Value must be less than {threshold}, got {value}"
57
+ )
58
+
59
+ return value
60
+
61
+ def handle_date(
62
+ self,
63
+ value: date,
64
+ context: Context,
65
+ *args: Any,
66
+ **kwargs: Any,
67
+ ) -> date:
68
+ threshold = kwargs["threshold"]
69
+ inclusive = kwargs["inclusive"]
70
+
71
+ if inclusive:
72
+ if value > threshold:
73
+ raise ValueError(
74
+ f"Value must be at most {threshold}, got {value}"
75
+ )
76
+ else:
77
+ if value >= threshold:
78
+ raise ValueError(
79
+ f"Value must be less than {threshold}, got {value}"
80
+ )
81
+
82
+ return value
83
+
84
+ def handle_time(
85
+ self,
86
+ value: time,
87
+ context: Context,
88
+ *args: Any,
89
+ **kwargs: Any,
90
+ ) -> time:
91
+ threshold = kwargs["threshold"]
92
+ inclusive = kwargs["inclusive"]
93
+
94
+ if inclusive:
95
+ if value > threshold:
96
+ raise ValueError(
97
+ f"Value must be at most {threshold}, got {value}"
98
+ )
99
+ else:
100
+ if value >= threshold:
101
+ raise ValueError(
102
+ f"Value must be less than {threshold}, got {value}"
103
+ )
104
+
105
+ return value
106
+
107
+
108
+ def less_than(
109
+ threshold: int | float | Decimal | datetime | date | time,
110
+ inclusive: bool = False,
111
+ ) -> Decorator:
112
+ """
113
+ Validate that a value is less than a threshold.
114
+
115
+ Type: `ChildNode`
116
+
117
+ Supports: `int`, `float`, `Decimal`, `datetime`, `date`, `time`
118
+
119
+ Args:
120
+ threshold (int | float | Decimal | datetime | date | time):
121
+ The threshold value to compare against.
122
+ inclusive (bool):
123
+ If `True`, allows values equal to threshold (<=).
124
+ If `False`, requires values strictly less (<).
125
+ Defaults to `False`.
126
+
127
+ Raises:
128
+ ValueError:
129
+ If value is not less than (or at most) the threshold.
130
+ TypeError:
131
+ If value type cannot be compared with threshold.
132
+
133
+ Examples:
134
+ Basic numeric validation:
135
+
136
+ >>> @command()
137
+ >>> @option("age", type=int)
138
+ >>> @less_than(100)
139
+ >>> def cmd(age):
140
+ ... click.echo(f"Age: {age}")
141
+
142
+ >>> # Valid: 99, 50, 0, -5
143
+ >>> # Invalid: 100, 101, 200
144
+
145
+ With inclusive parameter:
146
+
147
+ >>> @command()
148
+ >>> @option("percentage", type=float)
149
+ >>> @less_than(100.0, inclusive=True)
150
+ >>> def cmd(percentage):
151
+ ... click.echo(f"Percentage: {percentage}")
152
+
153
+ >>> # Valid: 100.0, 99.9, 50.0, 0.0
154
+ >>> # Invalid: 100.1, 150.0
155
+
156
+ Datetime validation:
157
+
158
+ >>> @command()
159
+ >>> @option("deadline")
160
+ >>> @to_date()
161
+ >>> @less_than(date(2025, 12, 31), inclusive=True)
162
+ >>> def cmd(deadline):
163
+ ... click.echo(f"Deadline: {deadline}")
164
+
165
+ >>> # Valid: 2025-12-31, 2025-01-01, 2024-12-31
166
+ >>> # Invalid: 2026-01-01, 2026-12-31
167
+
168
+ Multiple values (automatic handling):
169
+
170
+ >>> @command()
171
+ >>> @option("scores", type=int, multiple=True)
172
+ >>> @less_than(100, inclusive=True)
173
+ >>> def cmd(scores):
174
+ ... click.echo(f"Scores: {scores}")
175
+
176
+ >>> # Valid: (90, 85, 100), (50, 60, 70)
177
+ >>> # Invalid: (90, 101, 85), (100, 100, 105)
178
+ >>> # Note: Validation applies to each score individually
179
+ """
180
+ return LessThan.as_decorator(
181
+ threshold=threshold,
182
+ inclusive=inclusive,
183
+ )
@@ -0,0 +1,31 @@
1
+ """Initialization file for the `click_extended.decorators.convert` module."""
2
+
3
+ from click_extended.decorators.convert.convert_angle import convert_angle
4
+ from click_extended.decorators.convert.convert_area import convert_area
5
+ from click_extended.decorators.convert.convert_bits import convert_bits
6
+ from click_extended.decorators.convert.convert_distance import convert_distance
7
+ from click_extended.decorators.convert.convert_energy import convert_energy
8
+ from click_extended.decorators.convert.convert_power import convert_power
9
+ from click_extended.decorators.convert.convert_pressure import convert_pressure
10
+ from click_extended.decorators.convert.convert_speed import convert_speed
11
+ from click_extended.decorators.convert.convert_temperature import (
12
+ convert_temperature,
13
+ )
14
+ from click_extended.decorators.convert.convert_time import convert_time
15
+ from click_extended.decorators.convert.convert_volume import convert_volume
16
+ from click_extended.decorators.convert.convert_weight import convert_weight
17
+
18
+ __all__ = [
19
+ "convert_angle",
20
+ "convert_area",
21
+ "convert_bits",
22
+ "convert_distance",
23
+ "convert_energy",
24
+ "convert_power",
25
+ "convert_pressure",
26
+ "convert_time",
27
+ "convert_speed",
28
+ "convert_temperature",
29
+ "convert_volume",
30
+ "convert_weight",
31
+ ]
@@ -0,0 +1,94 @@
1
+ """Convert between different angle units."""
2
+
3
+ import math
4
+ from decimal import Decimal, getcontext
5
+ from typing import Any, Literal
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
+
11
+ getcontext().prec = 35
12
+
13
+ PI = Decimal(math.pi)
14
+
15
+ UNITS = {
16
+ "deg": Decimal("1"),
17
+ "rad": Decimal("180") / PI,
18
+ "grad": Decimal("0.9"),
19
+ "turn": Decimal("360"),
20
+ "rev": Decimal("360"),
21
+ "arcmin": Decimal("1") / Decimal("60"),
22
+ "arcsec": Decimal("1") / Decimal("3600"),
23
+ "mil": Decimal("0.05625"),
24
+ }
25
+
26
+
27
+ class ConvertAngle(ChildNode):
28
+ """Convert between different angle units."""
29
+
30
+ def handle_numeric(
31
+ self,
32
+ value: int | float,
33
+ context: Context,
34
+ *args: Any,
35
+ **kwargs: Any,
36
+ ) -> float:
37
+ from_unit = kwargs["from_unit"]
38
+ to_unit = kwargs["to_unit"]
39
+ val = Decimal(str(value))
40
+
41
+ if from_unit not in UNITS:
42
+ raise ValueError(f"Unknown unit '{from_unit}'")
43
+ if to_unit not in UNITS:
44
+ raise ValueError(f"Unknown unit '{to_unit}'")
45
+
46
+ degrees = val * UNITS[from_unit]
47
+
48
+ return float(degrees / UNITS[to_unit])
49
+
50
+
51
+ def convert_angle(
52
+ from_unit: Literal[
53
+ "deg",
54
+ "rad",
55
+ "grad",
56
+ "turn",
57
+ "arcmin",
58
+ "arcsec",
59
+ "rev",
60
+ "mil",
61
+ ],
62
+ to_unit: Literal[
63
+ "deg",
64
+ "rad",
65
+ "grad",
66
+ "turn",
67
+ "arcmin",
68
+ "arcsec",
69
+ "rev",
70
+ "mil",
71
+ ],
72
+ ) -> Decorator:
73
+ """
74
+ Convert between different angle units.
75
+
76
+ Type: `ChildNode`
77
+
78
+ Supports: `int`, `float`
79
+
80
+ Units:
81
+ - **deg**: Degree
82
+ - **rad**: Radian
83
+ - **grad**: Gradian
84
+ - **turn**: Full turn
85
+ - **arcmin**: Arcminute
86
+ - **arcsec**: Arcsecond
87
+ - **rev**: Revolution
88
+ - **mil**: NATO angular mil
89
+
90
+ Returns:
91
+ Decorator:
92
+ The decorated function.
93
+ """
94
+ return ConvertAngle.as_decorator(from_unit=from_unit, to_unit=to_unit)
@@ -0,0 +1,123 @@
1
+ """Convert between different area units."""
2
+
3
+ from decimal import Decimal, getcontext
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
+ getcontext().prec = 35
11
+
12
+ UNITS = {
13
+ "mm2": Decimal("1e-6"),
14
+ "cm2": Decimal("1e-4"),
15
+ "dm2": Decimal("1e-2"),
16
+ "m2": Decimal("1"),
17
+ "a": Decimal("100"),
18
+ "ha": Decimal("10000"),
19
+ "km2": Decimal("1e6"),
20
+ "in2": Decimal("0.00064516"),
21
+ "ft2": Decimal("0.09290304"),
22
+ "yd2": Decimal("0.83612736"),
23
+ "mi2": Decimal("2589988.110336"),
24
+ "acre": Decimal("4046.8564224"),
25
+ "rood": Decimal("1011.7141056"),
26
+ "perch": Decimal("25.29285264"),
27
+ "ang2": Decimal("1e-20"),
28
+ "tunnland": Decimal("4936.4"),
29
+ }
30
+
31
+
32
+ class ConvertArea(ChildNode):
33
+ """Convert between different area units."""
34
+
35
+ def handle_numeric(
36
+ self,
37
+ value: int | float,
38
+ context: Context,
39
+ *args: Any,
40
+ **kwargs: Any,
41
+ ) -> float:
42
+ from_unit = kwargs["from_unit"]
43
+ to_unit = kwargs["to_unit"]
44
+ val = Decimal(str(value))
45
+
46
+ if from_unit not in UNITS:
47
+ raise ValueError(f"Unknown unit '{from_unit}'")
48
+ if to_unit not in UNITS:
49
+ raise ValueError(f"Unknown unit '{to_unit}'")
50
+
51
+ m2 = val * UNITS[from_unit]
52
+
53
+ return float(m2 / UNITS[to_unit])
54
+
55
+
56
+ def convert_area(
57
+ from_unit: Literal[
58
+ "mm2",
59
+ "cm2",
60
+ "dm2",
61
+ "m2",
62
+ "a",
63
+ "ha",
64
+ "km2",
65
+ "in2",
66
+ "ft2",
67
+ "yd2",
68
+ "mi2",
69
+ "acre",
70
+ "rood",
71
+ "perch",
72
+ "ang2",
73
+ "tunnland",
74
+ ],
75
+ to_unit: Literal[
76
+ "mm2",
77
+ "cm2",
78
+ "dm2",
79
+ "m2",
80
+ "a",
81
+ "ha",
82
+ "km2",
83
+ "in2",
84
+ "ft2",
85
+ "yd2",
86
+ "mi2",
87
+ "acre",
88
+ "rood",
89
+ "perch",
90
+ "ang2",
91
+ "tunnland",
92
+ ],
93
+ ) -> Decorator:
94
+ """
95
+ Convert between different area units.
96
+
97
+ Type: `ChildNode`
98
+
99
+ Supports: `int`, `float`.
100
+
101
+ Units:
102
+ - **mm2**: Square millimeter
103
+ - **cm2**: Square centimeter
104
+ - **dm2**: Square decimeter
105
+ - **m2**: Square meter
106
+ - **a**: Are (100 m2)
107
+ - **ha**: Hectare / hektar (10,000 m2)
108
+ - **km2**: Square kilometer
109
+ - **in2**: Square inch
110
+ - **ft2**: Square foot
111
+ - **yd2**: Square yard
112
+ - **mi2**: Square mile
113
+ - **acre**: Acre (43,560 ft2)
114
+ - **rood**: Rood (1/4 acre)
115
+ - **perch**: Perch (1/160 acre)
116
+ - **ang2**: Square ångström
117
+ - **tunnland**: Historical Swedish area unit (~4,937 m2)
118
+
119
+ Returns:
120
+ Decorator:
121
+ The decorated function.
122
+ """
123
+ return ConvertArea.as_decorator(from_unit=from_unit, to_unit=to_unit)