click-extended 1.0.0__py3-none-any.whl → 1.0.2__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 (148) hide show
  1. click_extended/__init__.py +2 -0
  2. click_extended/core/__init__.py +10 -0
  3. click_extended/core/decorators/__init__.py +21 -0
  4. click_extended/core/decorators/argument.py +227 -0
  5. click_extended/core/decorators/command.py +93 -0
  6. click_extended/core/decorators/context.py +56 -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/utils/__init__.py +13 -0
  133. click_extended/utils/casing.py +169 -0
  134. click_extended/utils/checks.py +48 -0
  135. click_extended/utils/dispatch.py +1016 -0
  136. click_extended/utils/format.py +101 -0
  137. click_extended/utils/humanize.py +209 -0
  138. click_extended/utils/naming.py +238 -0
  139. click_extended/utils/process.py +294 -0
  140. click_extended/utils/selection.py +267 -0
  141. click_extended/utils/time.py +46 -0
  142. {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/METADATA +2 -1
  143. click_extended-1.0.2.dist-info/RECORD +150 -0
  144. click_extended-1.0.0.dist-info/RECORD +0 -10
  145. {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/WHEEL +0 -0
  146. {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/licenses/AUTHORS.md +0 -0
  147. {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/licenses/LICENSE +0 -0
  148. {click_extended-1.0.0.dist-info → click_extended-1.0.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,49 @@
1
+ """Parent node for generating a random boolean."""
2
+
3
+ import random
4
+ from typing import Any
5
+
6
+ from click_extended.core.nodes.parent_node import ParentNode
7
+ from click_extended.core.other.context import Context
8
+ from click_extended.types import Decorator
9
+
10
+
11
+ class RandomBool(ParentNode):
12
+ """Parent node for generating a random boolean."""
13
+
14
+ def load(self, context: Context, *args: Any, **kwargs: Any) -> bool:
15
+ if kwargs.get("seed") is not None:
16
+ random.seed(kwargs["seed"])
17
+
18
+ return bool(random.random() < min(1.0, max(0.0, kwargs["weight"])))
19
+
20
+
21
+ def random_bool(
22
+ name: str,
23
+ weight: float = 0.5,
24
+ seed: int | None = None,
25
+ ) -> Decorator:
26
+ """
27
+ Generate a random boolean.
28
+
29
+ Type: `ParentNode`
30
+
31
+ Args:
32
+ name (str):
33
+ The name of the parent node.
34
+ weight (float):
35
+ The probability of returning `True` (0.0 to 1.0).
36
+ Defaults to 0.5 (50% chance). The value is clamped and will always
37
+ be in the range 0.0 to 1.0.
38
+ seed (int | None):
39
+ Optional seed for reproducible randomness.
40
+
41
+ Returns:
42
+ Decorator:
43
+ The decorator function.
44
+ """
45
+ return RandomBool.as_decorator(
46
+ name=name,
47
+ weight=weight,
48
+ seed=seed,
49
+ )
@@ -0,0 +1,63 @@
1
+ """Parent node to choose a random value from an iterable."""
2
+
3
+ import random
4
+ from typing import Any, Sequence
5
+
6
+ from click_extended.classes import ParentNode
7
+ from click_extended.core.other.context import Context
8
+ from click_extended.types import Decorator
9
+
10
+
11
+ class RandomChoice(ParentNode):
12
+ """Get a random choice from an iterable."""
13
+
14
+ def load(self, context: Context, *args: Any, **kwargs: Any) -> Any:
15
+ if kwargs.get("seed") is not None:
16
+ random.seed(kwargs["seed"])
17
+
18
+ iterable: Sequence[str | int | float | bool] = kwargs.get(
19
+ "iterable", []
20
+ )
21
+ weights: Sequence[float] | None = kwargs.get("weights")
22
+
23
+ if weights:
24
+ if len(weights) != len(iterable):
25
+ raise ValueError(
26
+ f"The length of weights ({len(weights)}) must match "
27
+ f"iterable length ({len(iterable)})"
28
+ )
29
+ return random.choices(iterable, weights=weights, k=1)[0]
30
+ return random.choice(iterable)
31
+
32
+
33
+ def random_choice(
34
+ name: str,
35
+ iterable: Sequence[str | int | float | bool],
36
+ weights: Sequence[float] | None = None,
37
+ seed: int | None = None,
38
+ ) -> Decorator:
39
+ """
40
+ Select a random element from an iterable with the option to add weights.
41
+
42
+ Type: `ParentNode`
43
+
44
+ Args:
45
+ name (str):
46
+ The name of the parent node.
47
+ iterable (Sequence[str|int|float|bool]):
48
+ The iterable to choose from.
49
+ weights (Sequence[float] | None, optional):
50
+ A sequence of floating point numbers representing the weights.
51
+ seed (int | None, None):
52
+ The seed to use for reproducibility.
53
+
54
+ Returns:
55
+ Decorator:
56
+ The decorator function.
57
+ """
58
+ return RandomChoice.as_decorator(
59
+ name=name,
60
+ iterable=iterable,
61
+ weights=weights,
62
+ seed=seed,
63
+ )
@@ -0,0 +1,140 @@
1
+ """Parent node for generating a random datetime."""
2
+
3
+ # pylint: disable=too-many-arguments
4
+ # pylint: disable=too-many-positional-arguments
5
+
6
+ import random
7
+ from datetime import datetime, timedelta
8
+ from typing import Any
9
+ from zoneinfo import ZoneInfo
10
+
11
+ from click_extended.core.nodes.parent_node import ParentNode
12
+ from click_extended.core.other.context import Context
13
+ from click_extended.types import Decorator
14
+
15
+
16
+ class RandomDateTime(ParentNode):
17
+ """Parent node for generating a random datetime."""
18
+
19
+ def _parse_datetime(self, value: str | datetime) -> datetime:
20
+ """Parse a datetime string or return datetime as-is."""
21
+ if isinstance(value, datetime):
22
+ return value
23
+
24
+ value_lower = value.lower()
25
+
26
+ if value_lower == "now":
27
+ return datetime.now()
28
+
29
+ if value_lower == "today":
30
+ return datetime.now().replace(
31
+ hour=0, minute=0, second=0, microsecond=0
32
+ )
33
+
34
+ if value_lower == "tomorrow":
35
+ today = datetime.now().replace(
36
+ hour=0, minute=0, second=0, microsecond=0
37
+ )
38
+ return today + timedelta(days=1, hours=23, minutes=59, seconds=59)
39
+
40
+ if value_lower == "yesterday":
41
+ today = datetime.now().replace(
42
+ hour=0, minute=0, second=0, microsecond=0
43
+ )
44
+ return today - timedelta(days=1)
45
+
46
+ formats = [
47
+ "%Y-%m-%d %H:%M:%S", # YYYY-MM-DD HH:MM:SS
48
+ "%Y-%m-%d", # YYYY-MM-DD
49
+ "%H:%M:%S", # HH:MM:SS (today's date)
50
+ ]
51
+
52
+ for fmt in formats:
53
+ try:
54
+ return datetime.strptime(value, fmt)
55
+ except ValueError:
56
+ continue
57
+
58
+ raise ValueError(
59
+ f"Unable to parse datetime '{value}'. "
60
+ f"Supported formats: YYYY-MM-DD HH:MM:SS, YYYY-MM-DD, HH:MM:SS, "
61
+ f"or special keywords: 'now', 'today', 'tomorrow'"
62
+ )
63
+
64
+ def load(self, context: Context, *args: Any, **kwargs: Any) -> datetime:
65
+ if kwargs.get("seed") is not None:
66
+ random.seed(kwargs["seed"])
67
+
68
+ start = self._parse_datetime(kwargs["start_date"])
69
+ end = self._parse_datetime(kwargs["end_date"])
70
+
71
+ if start >= end:
72
+ raise ValueError("start_date must be before end_date")
73
+
74
+ start_timestamp = start.timestamp()
75
+ end_timestamp = end.timestamp()
76
+ random_timestamp = random.uniform(start_timestamp, end_timestamp)
77
+
78
+ result = datetime.fromtimestamp(random_timestamp)
79
+
80
+ timezone = kwargs.get("timezone")
81
+ if timezone:
82
+ try:
83
+ tz = ZoneInfo(timezone)
84
+ result = result.replace(tzinfo=tz)
85
+ except Exception as e:
86
+ raise ValueError(f"Invalid timezone '{timezone}' ({e})") from e
87
+
88
+ return result
89
+
90
+
91
+ def random_datetime(
92
+ name: str,
93
+ start_date: str | datetime,
94
+ end_date: str | datetime,
95
+ timezone: str | None = None,
96
+ seed: int | None = None,
97
+ ) -> Decorator:
98
+ """
99
+ Generate a random datetime.
100
+
101
+ Type: `ParentNode`
102
+
103
+ Args:
104
+ name (str):
105
+ The name of the parent node.
106
+ start_date (str | datetime):
107
+ The datetime to start at.
108
+ If a string is provided, the following formats are supported:
109
+ - YYYY-MM-DD HH:MM:SS
110
+ - YYYY-MM-DD
111
+ - HH:MM:SS
112
+ - Special keywords: 'now', 'today', 'tomorrow', 'yesterday'
113
+ end_date (str | datetime):
114
+ The datetime to end at.
115
+ If a string is provided, the following formats are supported:
116
+ - YYYY-MM-DD HH:MM:SS
117
+ - YYYY-MM-DD
118
+ - HH:MM:SS
119
+ - Special keywords: 'now', 'today', 'tomorrow', 'yesterday'
120
+ timezone (str | None, optional):
121
+ The timezone to use (e.g., 'UTC', 'US/Eastern', 'Europe/London').
122
+ If None, the datetime will be timezone-naive.
123
+ seed (int | None, optional):
124
+ Optional seed for reproducible randomness.
125
+
126
+ Returns:
127
+ Decorator:
128
+ The decorator function.
129
+
130
+ Raises:
131
+ ValueError:
132
+ If the timezone is invalid or start_date is after end_date.
133
+ """
134
+ return RandomDateTime.as_decorator(
135
+ name=name,
136
+ start_date=start_date,
137
+ end_date=end_date,
138
+ timezone=timezone,
139
+ seed=seed,
140
+ )
@@ -0,0 +1,62 @@
1
+ """Parent node for generating a random floating point value."""
2
+
3
+ # pylint: disable=too-many-arguments
4
+ # pylint: disable=too-many-positional-arguments
5
+
6
+ import random
7
+ from typing import Any
8
+
9
+ from click_extended.core.nodes.parent_node import ParentNode
10
+ from click_extended.core.other.context import Context
11
+ from click_extended.types import Decorator
12
+
13
+
14
+ class RandomFloat(ParentNode):
15
+ """Parent node for generating random floating point values."""
16
+
17
+ def load(self, context: Context, *args: Any, **kwargs: Any) -> float:
18
+ if kwargs.get("seed") is not None:
19
+ random.seed(kwargs["seed"])
20
+
21
+ if kwargs["max_value"] < kwargs["min_value"]:
22
+ raise ValueError("min_value can not be larger than max_value.")
23
+
24
+ value = random.uniform(kwargs["min_value"], kwargs["max_value"])
25
+ return float(round(value, kwargs["decimals"]))
26
+
27
+
28
+ def random_float(
29
+ name: str,
30
+ min_value: float = 0.0,
31
+ max_value: float = 1.0,
32
+ decimals: int = 3,
33
+ seed: int | None = None,
34
+ ) -> Decorator:
35
+ """
36
+ Generate a random floating point values.
37
+
38
+ Type: `ParentNode`
39
+
40
+ Args:
41
+ name (str):
42
+ The name of the parent node.
43
+ min_value (float):
44
+ The lower value in the range. Defaults to 0.0.
45
+ max_value (float):
46
+ The upper value in the range. Defaults to 1.0.
47
+ decimals (int):
48
+ The number of decimal places to round to. Defaults to 3.
49
+ seed (int | None):
50
+ Optional seed for reproducible randomness.
51
+
52
+ Returns:
53
+ Decorator:
54
+ The decorator function.
55
+ """
56
+ return RandomFloat.as_decorator(
57
+ name=name,
58
+ min_value=min_value,
59
+ max_value=max_value,
60
+ decimals=decimals,
61
+ seed=seed,
62
+ )
@@ -0,0 +1,56 @@
1
+ """Parent node for generating a random integer."""
2
+
3
+ # pylint: disable=too-many-arguments
4
+ # pylint: disable=too-many-positional-arguments
5
+
6
+ import random
7
+ from typing import Any
8
+
9
+ from click_extended.core.nodes.parent_node import ParentNode
10
+ from click_extended.core.other.context import Context
11
+ from click_extended.types import Decorator
12
+
13
+
14
+ class RandomInteger(ParentNode):
15
+ """Parent node for generating random integers."""
16
+
17
+ def load(self, context: Context, *args: Any, **kwargs: Any) -> int:
18
+ if kwargs.get("seed") is not None:
19
+ random.seed(kwargs["seed"])
20
+
21
+ if kwargs["max_value"] < kwargs["min_value"]:
22
+ raise ValueError("min_value can not be larger than max_value.")
23
+ return random.randint(kwargs["min_value"], kwargs["max_value"])
24
+
25
+
26
+ def random_integer(
27
+ name: str,
28
+ min_value: int = 0,
29
+ max_value: int = 100,
30
+ seed: int | None = None,
31
+ ) -> Decorator:
32
+ """
33
+ Generate a random integers.
34
+
35
+ Type: `ParentNode`
36
+
37
+ Args:
38
+ name (str):
39
+ The name of the parent node.
40
+ min_value (int):
41
+ The lower value in the range. Defaults to 0.
42
+ max_value (int):
43
+ The upper value in the range. Defaults to 100.
44
+ seed (int | None):
45
+ Optional seed for reproducible randomness.
46
+
47
+ Returns:
48
+ Decorator:
49
+ The decorator function.
50
+ """
51
+ return RandomInteger.as_decorator(
52
+ name=name,
53
+ min_value=min_value,
54
+ max_value=max_value,
55
+ seed=seed,
56
+ )
@@ -0,0 +1,196 @@
1
+ """Parent node for generating a random prime number."""
2
+
3
+ # pylint: disable=too-many-arguments
4
+ # pylint: disable=too-many-positional-arguments
5
+
6
+ import math
7
+ import random
8
+ from typing import Any
9
+
10
+ from click_extended.core.nodes.parent_node import ParentNode
11
+ from click_extended.core.other.context import Context
12
+ from click_extended.types import Decorator
13
+
14
+
15
+ class RandomPrime(ParentNode):
16
+ """Parent node for generating a random prime number."""
17
+
18
+ _SMALL_PRIMES = [
19
+ 2,
20
+ 3,
21
+ 5,
22
+ 7,
23
+ 11,
24
+ 13,
25
+ 17,
26
+ 19,
27
+ 23,
28
+ 29,
29
+ 31,
30
+ 37,
31
+ 41,
32
+ 43,
33
+ 47,
34
+ 53,
35
+ 59,
36
+ 61,
37
+ 67,
38
+ 71,
39
+ 73,
40
+ 79,
41
+ 83,
42
+ 89,
43
+ 97,
44
+ 101,
45
+ 103,
46
+ 107,
47
+ 109,
48
+ 113,
49
+ 127,
50
+ 131,
51
+ 137,
52
+ 139,
53
+ 149,
54
+ 151,
55
+ 157,
56
+ 163,
57
+ 167,
58
+ 173,
59
+ 179,
60
+ 181,
61
+ 191,
62
+ 193,
63
+ 197,
64
+ 199,
65
+ 211,
66
+ 223,
67
+ 227,
68
+ 229,
69
+ 233,
70
+ 239,
71
+ 241,
72
+ 251,
73
+ 257,
74
+ 263,
75
+ 269,
76
+ 271,
77
+ 277,
78
+ 281,
79
+ 283,
80
+ 293,
81
+ 307,
82
+ 311,
83
+ 313,
84
+ 317,
85
+ 331,
86
+ 337,
87
+ 347,
88
+ 349,
89
+ 353,
90
+ 359,
91
+ 367,
92
+ 373,
93
+ 379,
94
+ 383,
95
+ 389,
96
+ 397,
97
+ 401,
98
+ 409,
99
+ 419,
100
+ 421,
101
+ 431,
102
+ 433,
103
+ 439,
104
+ 443,
105
+ 449,
106
+ 457,
107
+ 461,
108
+ 463,
109
+ 467,
110
+ 479,
111
+ 487,
112
+ 491,
113
+ 499,
114
+ 503,
115
+ 509,
116
+ 521,
117
+ 523,
118
+ 541,
119
+ ]
120
+
121
+ def _calculate_prime(self, k: int) -> int:
122
+ """Calculate the k:th prime number (1-indexed)."""
123
+ if k <= 0:
124
+ raise ValueError("k must be positive")
125
+
126
+ if k <= len(self._SMALL_PRIMES):
127
+ return self._SMALL_PRIMES[k - 1]
128
+
129
+ # For larger k, use sieve with better upper bound estimation
130
+ # Using Rosser's theorem: p_n < n(ln(n) + ln(ln(n))) for n >= 6
131
+ if k < 6:
132
+ n = 30
133
+ else:
134
+ log_k = math.log(k)
135
+ log_log_k = math.log(log_k)
136
+ n = int(k * (log_k + log_log_k) * 1.3) # 30% buffer
137
+
138
+ sieve = [True] * (n + 1)
139
+ sieve[0] = sieve[1] = False
140
+
141
+ for p in range(2, int(math.sqrt(n)) + 1):
142
+ if sieve[p]:
143
+ start = p * p
144
+ step = p if p == 2 else p * 2
145
+ for i in range(start, n + 1, step):
146
+ sieve[i] = False
147
+
148
+ primes: list[int] = []
149
+ for i in range(2, n + 1):
150
+ if sieve[i]:
151
+ primes.append(i)
152
+ if len(primes) == k:
153
+ return primes[k - 1]
154
+
155
+ raise ValueError(
156
+ f"Unable to calculate {k}th prime with buffer size {n}"
157
+ )
158
+
159
+ def load(self, context: Context, *args: Any, **kwargs: Any) -> int:
160
+ if kwargs.get("seed") is not None:
161
+ random.seed(kwargs["seed"])
162
+
163
+ k = random.randint(1, kwargs["k"])
164
+ return self._calculate_prime(k)
165
+
166
+
167
+ def random_prime(
168
+ name: str,
169
+ k: int = 100,
170
+ seed: int | None = None,
171
+ ) -> Decorator:
172
+ """
173
+ Generate a random prime number.
174
+
175
+ Type: `ParentNode`
176
+
177
+ Args:
178
+ name (str):
179
+ The name of the parent node.
180
+ k (int):
181
+ A random prime from the first `k` primes.
182
+ It's important to remember that calculating large prime
183
+ numbers is a slow process, so try to keep the `k` value
184
+ small to avoid slowing down your program.
185
+ seed (int | None):
186
+ Optional seed for reproducible randomness.
187
+
188
+ Returns:
189
+ Decorator:
190
+ The decorator function.
191
+ """
192
+ return RandomPrime.as_decorator(
193
+ name=name,
194
+ k=k,
195
+ seed=seed,
196
+ )
@@ -0,0 +1,77 @@
1
+ """Parent node for generating a random string."""
2
+
3
+ # pylint: disable=too-many-arguments
4
+ # pylint: disable=too-many-positional-arguments
5
+
6
+ import random
7
+ from string import ascii_lowercase, ascii_uppercase, digits, punctuation
8
+ from typing import Any
9
+
10
+ from click_extended.core.nodes.parent_node import ParentNode
11
+ from click_extended.core.other.context import Context
12
+ from click_extended.types import Decorator
13
+
14
+
15
+ class RandomString(ParentNode):
16
+ """Parent node for generating random strings."""
17
+
18
+ def load(self, context: Context, *args: Any, **kwargs: Any) -> str:
19
+ if kwargs.get("seed") is not None:
20
+ random.seed(kwargs["seed"])
21
+
22
+ chars = ""
23
+ if kwargs["lowercase"]:
24
+ chars += ascii_lowercase
25
+ if kwargs["uppercase"]:
26
+ chars += ascii_uppercase
27
+ if kwargs["numbers"]:
28
+ chars += digits
29
+ if kwargs["symbols"]:
30
+ chars += punctuation
31
+
32
+ return "".join(random.choice(chars) for _ in range(kwargs["length"]))
33
+
34
+
35
+ def random_string(
36
+ name: str,
37
+ length: int = 8,
38
+ lowercase: bool = True,
39
+ uppercase: bool = True,
40
+ numbers: bool = True,
41
+ symbols: bool = True,
42
+ seed: int | None = None,
43
+ ) -> Decorator:
44
+ """
45
+ Generate a random string.
46
+
47
+ Type: `ParentNode`
48
+
49
+ Args:
50
+ name (str):
51
+ The name of the parent node.
52
+ length (int):
53
+ The length of the string to generate.
54
+ lowercase (bool):
55
+ Whether to include lowercase characters.
56
+ uppercase (bool):
57
+ Whether to include uppercase characters.
58
+ numbers (bool):
59
+ Whether to include numbers.
60
+ symbols (bool):
61
+ Whether to include symbols.
62
+ seed (int | None):
63
+ Optional seed for reproducible randomness.
64
+
65
+ Returns:
66
+ Decorator:
67
+ The decorator function.
68
+ """
69
+ return RandomString.as_decorator(
70
+ name=name,
71
+ length=length,
72
+ lowercase=lowercase,
73
+ uppercase=uppercase,
74
+ numbers=numbers,
75
+ symbols=symbols,
76
+ seed=seed,
77
+ )