randpal 0.2__tar.gz → 0.4__tar.gz

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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: randpal
3
- Version: 0.2
3
+ Version: 0.4
4
4
  Summary: Your friendly random pal - at your random service.
5
5
  Project-URL: Repository, https://github.com/soucelover/randpal.git
6
6
  Project-URL: Issues, https://github.com/soucelover/randpal/issues
@@ -112,6 +112,9 @@ runtime-evaluated-base-classes = [
112
112
  ]
113
113
  runtime-evaluated-decorators = ["invoke.tasks.task"]
114
114
 
115
+ [tool.ruff.lint.flake8-builtins]
116
+ ignorelist = ["help"]
117
+
115
118
  # ================
116
119
  # Pyright Settings
117
120
  # ================
@@ -0,0 +1,26 @@
1
+ from importlib import metadata
2
+
3
+ from .rng import (
4
+ FloatGenerator,
5
+ IntegerGenerator,
6
+ PythonRandom,
7
+ RandomProvider,
8
+ SecretsRandom,
9
+ SequencesGenerator,
10
+ )
11
+
12
+ __all__ = [
13
+ "FloatGenerator",
14
+ "IntegerGenerator",
15
+ "PythonRandom",
16
+ "RandomProvider",
17
+ "SecretsRandom",
18
+ "SequencesGenerator",
19
+ "__version__",
20
+ ]
21
+ PACKAGE_NAME = "randpal"
22
+
23
+ try:
24
+ __version__ = metadata.version(PACKAGE_NAME)
25
+ except metadata.PackageNotFoundError:
26
+ __version__ = "0.1.dev1+UNKNOWN"
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from randpal.cli import cli
4
+
5
+
6
+ def main() -> None:
7
+ cli()
8
+
9
+
10
+ if __name__ == "__main__":
11
+ main()
@@ -0,0 +1,3 @@
1
+ from .parser import cli, setup_parser
2
+
3
+ __all__ = ["cli", "setup_parser"]
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from argparse import Namespace
4
+ from collections.abc import Callable
5
+ from typing import Literal, TypeAlias
6
+
7
+ from randpal.rng import PythonRandom, SecretsRandom
8
+
9
+ CommandFunction: TypeAlias = Callable[[Namespace], None]
10
+ GeneratorCommandFunction: TypeAlias = Callable[
11
+ ["GeneratorCommandArgs", "RandomGeneratorFactory"], None
12
+ ]
13
+ ProviderCode: TypeAlias = Literal["python", "secrets"]
14
+
15
+
16
+ class RandomGeneratorFactory:
17
+ def create_python_rng(self) -> PythonRandom:
18
+ return PythonRandom()
19
+
20
+ def create_secrets_rng(self) -> SecretsRandom:
21
+ return SecretsRandom()
22
+
23
+ def create_rng(self, code: ProviderCode) -> PythonRandom | SecretsRandom:
24
+ match code:
25
+ case "python":
26
+ return self.create_python_rng()
27
+ case "secrets":
28
+ return self.create_secrets_rng()
29
+
30
+
31
+ class GeneratorCommandArgs(Namespace):
32
+ provider: ProviderCode
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from randpal.rng import IntegerGenerator, SequencesGenerator
7
+
8
+ from .base import GeneratorCommandArgs, RandomGeneratorFactory
9
+
10
+
11
+ VERBOSE_MODE = True
12
+
13
+
14
+ def command_int(
15
+ args: GeneratorCommandArgs, factory: RandomGeneratorFactory
16
+ ) -> None:
17
+ generator: IntegerGenerator = factory.create_rng(args.provider)
18
+ number = generator.randint(args.a, args.b)
19
+
20
+ match args.output_format:
21
+ case "concise":
22
+ print(number) # noqa: T201
23
+ case "verbose":
24
+ print(f"And so, the random number is... {number}!", end="\n\n") # noqa: T201
25
+ case _:
26
+ raise NotImplementedError
27
+
28
+
29
+ def command_pick(
30
+ args: GeneratorCommandArgs, factory: RandomGeneratorFactory
31
+ ) -> None:
32
+ generator: SequencesGenerator = factory.create_rng(args.provider)
33
+ item = generator.choice(args.items)
34
+
35
+ match args.output_format:
36
+ case "concise":
37
+ print(item) # noqa: T201
38
+ case "verbose":
39
+ print( # noqa: T201
40
+ f'The barrel with cards was shaken and a card with "{item}" '
41
+ "written on it has fallen out.",
42
+ end="\n\n",
43
+ )
44
+ case _:
45
+ raise NotImplementedError
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from argparse import ArgumentParser, RawDescriptionHelpFormatter
5
+ from typing import TYPE_CHECKING
6
+
7
+ import randpal
8
+
9
+ from .base import GeneratorCommandArgs, RandomGeneratorFactory
10
+ from .commands import command_int, command_pick
11
+
12
+ if TYPE_CHECKING:
13
+ from argparse import (
14
+ _SubParsersAction, # pyright: ignore[reportPrivateUsage]
15
+ )
16
+ from collections.abc import Sequence
17
+
18
+ from .base import CommandFunction, GeneratorCommandFunction
19
+
20
+
21
+ def get_docs_header(obj: object) -> tuple[str, str] | tuple[None, None]:
22
+ """Get header of object's docstring.
23
+
24
+ Args:
25
+ obj: Source of docstring.
26
+
27
+ Returns:
28
+ The first line of the dosctring and an original docstring itself.
29
+ """
30
+ docs = inspect.getdoc(obj)
31
+
32
+ if docs is None:
33
+ return None, None
34
+
35
+ header, _, _body = docs.partition("\n")
36
+ return header, docs
37
+
38
+
39
+ def add_command( # noqa: PLR0913
40
+ commands: _SubParsersAction[ArgumentParser],
41
+ name: str,
42
+ function: CommandFunction,
43
+ *,
44
+ help: str | None = None,
45
+ epilog: str | None = None,
46
+ aliases: Sequence[str] = (),
47
+ ) -> ArgumentParser:
48
+ """Register a single command in `SubParsersAction`.
49
+
50
+ Args:
51
+ commands: `ArgumentParser`'s action representing commands
52
+ name: Name for the command.
53
+ function: Function implementing command and containing its docstrings.
54
+ aliases: Optional list of aliases for the command.
55
+
56
+ Returns:
57
+ A newly created parser of the command.
58
+ """
59
+ header, docs = get_docs_header(function)
60
+
61
+ if help is None:
62
+ help = header
63
+
64
+ parser = commands.add_parser(
65
+ name,
66
+ help=help,
67
+ description=docs,
68
+ epilog=epilog,
69
+ aliases=aliases,
70
+ formatter_class=RawDescriptionHelpFormatter,
71
+ )
72
+ parser.set_defaults(command=function)
73
+ return parser
74
+
75
+
76
+ def add_generator_command( # noqa: PLR0913
77
+ commands: _SubParsersAction[ArgumentParser],
78
+ name: str,
79
+ function: GeneratorCommandFunction,
80
+ *,
81
+ providers: Sequence[str] | None = ("python", "secrets"),
82
+ output_formats: Sequence[str] | None = ("verbose", "concise"),
83
+ help: str | None = None,
84
+ epilog: str | None = None,
85
+ aliases: Sequence[str] = (),
86
+ ) -> ArgumentParser:
87
+ def wrapper(args: GeneratorCommandArgs) -> None:
88
+ factory = RandomGeneratorFactory()
89
+
90
+ return function(args, factory)
91
+
92
+ parser = add_command(
93
+ commands,
94
+ name,
95
+ wrapper, # type: ignore [reportArgumentType]
96
+ help=help,
97
+ epilog=epilog,
98
+ aliases=aliases,
99
+ )
100
+
101
+ if providers:
102
+ parser.add_argument(
103
+ "--provider",
104
+ default=providers[0],
105
+ choices=providers,
106
+ help="Provider chosen as a source of entropy "
107
+ "for random generation.",
108
+ )
109
+
110
+ if output_formats:
111
+ parser.add_argument(
112
+ "--output-format",
113
+ "--format",
114
+ default=output_formats[0],
115
+ choices=output_formats,
116
+ help="Output format for the generation results.",
117
+ dest="output_format",
118
+ )
119
+
120
+ return parser
121
+
122
+
123
+ def register_commands(parser: ArgumentParser) -> None:
124
+ commands = parser.add_subparsers(required=True)
125
+
126
+ com_int = add_generator_command(commands, "int", command_int)
127
+ com_int.add_argument("a", type=int)
128
+ com_int.add_argument("b", type=int)
129
+
130
+ com_choose = add_generator_command(commands, "pick", command_pick)
131
+ com_choose.add_argument("items", nargs="+", metavar="item")
132
+
133
+
134
+ def setup_parser() -> ArgumentParser:
135
+ parser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter)
136
+
137
+ parser.add_argument(
138
+ "-V",
139
+ "--version",
140
+ action="version",
141
+ version=f"randpal {randpal.__version__}",
142
+ )
143
+
144
+ register_commands(parser)
145
+
146
+ return parser
147
+
148
+
149
+ def cli(args: list[str] | None = None) -> None:
150
+ parser = setup_parser()
151
+
152
+ parsed = parser.parse_args(args)
153
+ parsed.command(parsed)
@@ -0,0 +1,17 @@
1
+ from .base import (
2
+ FloatGenerator,
3
+ IntegerGenerator,
4
+ RandomProvider,
5
+ SequencesGenerator,
6
+ )
7
+ from .python import PythonRandom
8
+ from .secrets import SecretsRandom
9
+
10
+ __all__ = [
11
+ "FloatGenerator",
12
+ "IntegerGenerator",
13
+ "PythonRandom",
14
+ "RandomProvider",
15
+ "SecretsRandom",
16
+ "SequencesGenerator",
17
+ ]
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ import operator
4
+ from abc import abstractmethod
5
+ from typing import TYPE_CHECKING, Protocol, SupportsIndex, TypeVar, overload
6
+
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Sequence
9
+
10
+ __all__ = [
11
+ "FloatGenerator",
12
+ "IntegerGenerator",
13
+ "RandomProvider",
14
+ "SequencesGenerator",
15
+ ]
16
+
17
+
18
+ _T = TypeVar("_T")
19
+
20
+
21
+ class RandomProvider(Protocol):
22
+ @abstractmethod
23
+ def randbytes(self, n: int) -> bytes:
24
+ raise NotImplementedError
25
+
26
+ # NOTE: It's possible to implement random buffering in future
27
+ def randbits(self, k: int) -> int:
28
+ n = (k + 7) // 8
29
+ value = int.from_bytes(self.randbytes(n), "big")
30
+ return value >> (n * 8 - k)
31
+
32
+ def _randbelow(self, n: int) -> int:
33
+ if n <= 0:
34
+ msg = "Empty range -- no values to choose from"
35
+ raise ValueError(msg)
36
+
37
+ k = n.bit_length()
38
+ randbits = self.randbits
39
+ r = randbits(k)
40
+
41
+ while r >= n:
42
+ r = randbits(k)
43
+
44
+ return r
45
+
46
+
47
+ class IntegerGenerator(RandomProvider, Protocol):
48
+ @overload
49
+ def randrange(self, stop: SupportsIndex, /) -> int: ...
50
+ @overload
51
+ def randrange(
52
+ self, start: SupportsIndex, stop: SupportsIndex, /
53
+ ) -> int: ...
54
+ @overload
55
+ def randrange(
56
+ self, start: SupportsIndex, stop: SupportsIndex, step: SupportsIndex, /
57
+ ) -> int: ...
58
+
59
+ def randrange(
60
+ self,
61
+ start: SupportsIndex,
62
+ stop: SupportsIndex | None = None,
63
+ step: SupportsIndex = 1,
64
+ /,
65
+ ) -> int:
66
+ try:
67
+ istart = operator.index(start) # Validate start
68
+ except TypeError as exc:
69
+ tname = type(start).__name__
70
+ msg = f"Arg 1 of type {tname} couldn't be interpreted as integer"
71
+ raise TypeError(msg) from exc
72
+
73
+ # Fast path: [0; start)
74
+ if stop is None:
75
+ if step != 1:
76
+ msg = "Step was provided without a non-None stop argument"
77
+ raise TypeError(msg)
78
+
79
+ return self._randbelow(istart)
80
+
81
+ try:
82
+ istop = operator.index(stop) # Validate stop
83
+ except TypeError as exc:
84
+ tname = type(stop).__name__
85
+ msg = f"Arg 2 of type {tname} couldn't be interpreted as integer"
86
+ raise TypeError(msg) from exc
87
+
88
+ try:
89
+ istep = operator.index(step) # Validate stop
90
+ except TypeError as exc:
91
+ tname = type(step).__name__
92
+ msg = f"Arg 3 of type {tname} couldn't be interpreted as integer"
93
+ raise TypeError(msg) from exc
94
+
95
+ width = istop - istart
96
+
97
+ # Fast path: [start; stop)
98
+ if istep == 1:
99
+ return istart + self._randbelow(width)
100
+
101
+ if istep == 0:
102
+ msg = "Zero step was provided"
103
+ raise ValueError(msg)
104
+
105
+ # Divide width and round in direction from zero
106
+ n = (width + istep + 1 - (istep > 0) * 2) // istep
107
+
108
+ return istart + istep * self._randbelow(n)
109
+
110
+ def randint(self, a: SupportsIndex, b: SupportsIndex) -> int:
111
+ try:
112
+ ia = operator.index(a) # Validate a
113
+ except TypeError as exc:
114
+ tname = type(a).__name__
115
+ msg = f"Arg 1 of type {tname} couldn't be interpreted as integer"
116
+ raise TypeError(msg) from exc
117
+
118
+ try:
119
+ ib = operator.index(b) # Validate b
120
+ except TypeError as exc:
121
+ tname = type(b).__name__
122
+ msg = f"Arg 2 of type {tname} couldn't be interpreted as integer"
123
+ raise TypeError(msg) from exc
124
+
125
+ width = ib - ia
126
+ return ia + self._randbelow(width + 1)
127
+
128
+ def randbool(self) -> bool:
129
+ return bool(self.randbits(1))
130
+
131
+
132
+ class FloatGenerator(Protocol):
133
+ def uniform(self, a: float, b: float) -> float:
134
+ raise NotImplementedError
135
+
136
+ def chance(self, probability: float) -> bool:
137
+ raise NotImplementedError
138
+
139
+
140
+ class SequencesGenerator(RandomProvider, Protocol):
141
+ def choice(self, seq: Sequence[_T]) -> _T:
142
+ return seq[self._randbelow(len(seq))]
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+
5
+ from typing_extensions import override
6
+
7
+ from .base import IntegerGenerator, RandomProvider, SequencesGenerator
8
+
9
+
10
+ class PythonRandom(SequencesGenerator, IntegerGenerator, RandomProvider):
11
+ _generator: random.Random
12
+
13
+ def __init__(self, generator: random.Random | None = None) -> None:
14
+ if generator is None:
15
+ generator = random.Random() # noqa: S311
16
+
17
+ self._generator = generator
18
+
19
+ @override
20
+ def randbytes(self, n: int) -> bytes:
21
+ return self._generator.randbytes(n)
22
+
23
+ @override
24
+ def randbits(self, k: int) -> int:
25
+ return self._generator.getrandbits(k)
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import secrets
4
+
5
+ from typing_extensions import override
6
+
7
+ from .base import IntegerGenerator, RandomProvider, SequencesGenerator
8
+
9
+
10
+ class SecretsRandom(SequencesGenerator, IntegerGenerator, RandomProvider):
11
+ _generator: secrets.SystemRandom
12
+
13
+ def __init__(self, generator: secrets.SystemRandom | None = None) -> None:
14
+ if generator is None:
15
+ generator = secrets.SystemRandom()
16
+
17
+ self._generator = generator
18
+
19
+ @override
20
+ def randbytes(self, n: int) -> bytes:
21
+ return self._generator.randbytes(n)
22
+
23
+ @override
24
+ def randbits(self, k: int) -> int:
25
+ return self._generator.getrandbits(k)
@@ -1,9 +0,0 @@
1
- from importlib import metadata
2
-
3
- __all__ = ["__version__"]
4
- PACKAGE_NAME = "randpal"
5
-
6
- try:
7
- __version__ = metadata.version(PACKAGE_NAME)
8
- except metadata.PackageNotFoundError:
9
- __version__ = "0.1.dev1+UNKNOWN"
@@ -1,43 +0,0 @@
1
- import argparse
2
- import random
3
-
4
- VERBOSE_MODE = True
5
-
6
-
7
- def command_int(args: argparse.Namespace) -> None:
8
- if VERBOSE_MODE:
9
- print("Random number is ", end="") # noqa: T201
10
-
11
- print(random.randint(args.a, args.b), end="\n\n") # noqa: S311, T201
12
-
13
-
14
- def command_choose(args: argparse.Namespace) -> None:
15
- if VERBOSE_MODE:
16
- print("Random chosen item is ", end="") # noqa: T201
17
-
18
- print(random.choice(args.item), end="\n\n") # noqa: S311, T201
19
-
20
-
21
- def parse_args() -> argparse.Namespace:
22
- parser = argparse.ArgumentParser()
23
- commands = parser.add_subparsers(required=True)
24
-
25
- com_int = commands.add_parser("int")
26
- com_int.add_argument("a", type=int)
27
- com_int.add_argument("b", type=int)
28
- com_int.set_defaults(command=command_int)
29
-
30
- com_choose = commands.add_parser("choose")
31
- com_choose.add_argument("item", nargs="+")
32
- com_choose.set_defaults(command=command_choose)
33
-
34
- return parser.parse_args()
35
-
36
-
37
- def main() -> None:
38
- args = parse_args()
39
- args.command(args)
40
-
41
-
42
- if __name__ == "__main__":
43
- main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes