transcrypto 1.6.0__py3-none-any.whl → 1.8.0__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.
transcrypto/profiler.py CHANGED
@@ -1,189 +1,242 @@
1
- #!/usr/bin/env python3
2
- #
3
- # Copyright 2025 Daniel Balparda (balparda@github.com) - Apache-2.0 license
4
- #
1
+ # SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
2
+ # SPDX-License-Identifier: Apache-2.0
5
3
  """Balparda's TransCrypto Profiler command line interface.
6
4
 
7
- See README.md for documentation on how to use.
5
+ See <profiler.md> for documentation on how to use. Quick examples:
8
6
 
9
- Notes on the layout (quick mental model):
7
+ --- Primes / DSA ---
8
+ poetry run profiler -n 10 primes
9
+ poetry run profiler --no-serial -n 20 dsa
10
10
 
11
- primes
12
- dsa
13
- doc md
11
+ --- Markdown ---
12
+ poetry run profiler markdown > profiler.md
13
+
14
+ Test this CLI with:
15
+
16
+ poetry run pytest -vvv tests/profiler_test.py
14
17
  """
15
18
 
16
19
  from __future__ import annotations
17
20
 
18
- import argparse
19
- # import pdb
20
- import sys
21
- from typing import Callable
21
+ import dataclasses
22
+ from collections import abc
22
23
 
24
+ import typer
23
25
  from rich import console as rich_console
24
26
 
25
- from . import base, modmath, dsa
26
-
27
- __author__ = 'balparda@github.com'
28
- __version__: str = base.__version__ # version comes from base!
29
- __version_tuple__: tuple[int, ...] = base.__version_tuple__
30
-
31
-
32
- def _BuildParser() -> argparse.ArgumentParser: # pylint: disable=too-many-statements,too-many-locals
33
- """Construct the CLI argument parser (kept in sync with the docs)."""
34
- # ========================= main parser ==========================================================
35
- parser: argparse.ArgumentParser = argparse.ArgumentParser(
36
- prog='poetry run profiler',
37
- description=('profiler: CLI for TransCrypto Profiler, measure library performance.'),
38
- epilog=(
39
- 'Examples:\n\n'
40
- ' # --- Primes ---\n'
41
- ' poetry run profiler -p -n 10 primes\n'
42
- ' poetry run profiler -n 20 dsa\n'
43
- ),
44
- formatter_class=argparse.RawTextHelpFormatter)
45
- sub = parser.add_subparsers(dest='command')
46
-
47
- # ========================= global flags =========================================================
48
- # -v/-vv/-vvv/-vvvv for ERROR/WARN/INFO/DEBUG
49
- parser.add_argument(
50
- '-v', '--verbose', action='count', default=0,
51
- help='Increase verbosity (use -v/-vv/-vvv/-vvvv for ERROR/WARN/INFO/DEBUG)')
52
-
53
- thread_grp = parser.add_mutually_exclusive_group()
54
- thread_grp.add_argument(
55
- '-s', '--serial', action='store_true',
56
- help='If test can be serial, do it like that with no parallelization (default)')
57
- thread_grp.add_argument(
58
- '-p', '--parallel', action='store_true',
59
- help='If test can be parallelized into processes, do it like that')
60
-
61
- parser.add_argument(
62
- '-n', '--number', type=int, default=15,
63
- help='Number of experiments (repeats) for every measurement')
64
- parser.add_argument(
65
- '-c', '--confidence', type=int, default=98,
66
- help=('Confidence level to evaluate measurements at as int percentage points [50,99], '
67
- 'inclusive, representing 50% to 99%'))
68
-
69
- parser.add_argument(
70
- '-b', '--bits', type=str, default='1000,9000,1000',
71
- help=('Bit lengths to investigate as "int,int,int"; behaves like arguments for range(), '
72
- 'i.e., "start,stop,step", eg. "1000,3000,500" will investigate 1000,1500,2000,2500'))
73
-
74
- # ========================= Prime Generation =====================================================
75
-
76
- # Regular prime generation
77
- sub.add_parser(
78
- 'primes',
79
- help='Measure regular prime generation.',
80
- epilog=('-n 30 -b 9000,11000,1000 primes\nStarting SERIAL regular primes test\n'
81
- '9000 → 38.88 s ± 14.74 s [24.14 s … 53.63 s]98%CI@30\n'
82
- '10000 → 41.26 s ± 22.82 s [18.44 s … 1.07 min]98%CI@30\nFinished in 40.07 min'))
83
-
84
- # DSA primes generation
85
- sub.add_parser(
86
- 'dsa',
87
- help='Measure DSA prime generation.',
88
- epilog=('-p -n 2 -b 1000,1500,100 -c 80 dsa\nStarting PARALLEL DSA primes test\n'
89
- '1000 → 236.344 ms ± 273.236 ms [*0.00 s … 509.580 ms]80%CI@2\n'
90
- '1100 319.308 ms ± 639.775 ms [*0.00 s … 959.083 ms]80%CI@2\n'
91
- '1200 → 523.885 ms ± 879.981 ms [*0.00 s … 1.40 s]80%CI@2\n'
92
- '1300 506.285 ms ± 687.153 ms [*0.00 s … 1.19 s]80%CI@2\n'
93
- '1400 → 552.840 ms ± 47.012 ms [505.828 ms … 599.852 ms]80%CI@2\nFinished in 4.12 s'))
94
-
95
- # ========================= Markdown Generation ==================================================
96
-
97
- # Documentation generation
98
- doc: argparse.ArgumentParser = sub.add_parser(
99
- 'doc', help='Documentation utilities. (Not for regular use: these are developer utils.)')
100
- doc_sub = doc.add_subparsers(dest='doc_command')
101
- doc_sub.add_parser(
102
- 'md',
103
- help='Emit Markdown docs for the CLI (see README.md section "Creating a New Version").',
104
- epilog='doc md > profiler.md\n<<saves file>>')
105
-
106
- return parser
27
+ from transcrypto.cli import clibase
28
+
29
+ from . import __version__, base, dsa, modmath
30
+
31
+
32
+ @dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
33
+ class ProfilerConfig(clibase.CLIConfig):
34
+ """CLI global context, storing the configuration."""
35
+
36
+ serial: bool
37
+ repeats: int
38
+ confidence: int
39
+ bits: tuple[int, int, int]
40
+
41
+
42
+ # CLI app setup, this is an important object and can be imported elsewhere and called
43
+ app = typer.Typer(
44
+ add_completion=True,
45
+ no_args_is_help=True,
46
+ help='profiler: CLI for TransCrypto Profiler, measure library performance.',
47
+ epilog=(
48
+ 'Examples:\n\n\n\n'
49
+ '# --- Primes / DSA ---\n\n'
50
+ 'poetry run profiler -n 10 primes\n\n'
51
+ 'poetry run profiler --no-serial -n 20 dsa\n\n\n\n'
52
+ '# --- Markdown ---\n\n'
53
+ 'poetry run profiler markdown > profiler.md'
54
+ ),
55
+ )
56
+
57
+
58
+ def Run() -> None:
59
+ """Run the CLI."""
60
+ app()
61
+
62
+
63
+ @app.callback(
64
+ invoke_without_command=True, # have only one; this is the "constructor"
65
+ help='Profile TransCrypto library performance.',
66
+ )
67
+ @clibase.CLIErrorGuard
68
+ def Main( # documentation is help/epilog/args # noqa: D103
69
+ *,
70
+ ctx: typer.Context, # global context
71
+ version: bool = typer.Option(False, '--version', help='Show version and exit.'),
72
+ verbose: int = typer.Option(
73
+ 0,
74
+ '-v',
75
+ '--verbose',
76
+ count=True,
77
+ help='Verbosity (nothing=ERROR, -v=WARNING, -vv=INFO, -vvv=DEBUG).',
78
+ min=0,
79
+ max=3,
80
+ ),
81
+ color: bool | None = typer.Option(
82
+ None,
83
+ '--color/--no-color',
84
+ help=(
85
+ 'Force enable/disable colored output (respects NO_COLOR env var if not provided). '
86
+ 'Defaults to having colors.' # state default because None default means docs don't show it
87
+ ),
88
+ ),
89
+ serial: bool = typer.Option(
90
+ True,
91
+ '--serial/--no-serial',
92
+ help='Execute operation serially (i.e. do not use threads/multiprocessing).',
93
+ ),
94
+ repeats: int = typer.Option(
95
+ 15,
96
+ '-n',
97
+ '--number',
98
+ help='Number of experiments (repeats) for every measurement.',
99
+ min=1,
100
+ max=1000,
101
+ ),
102
+ confidence: int = typer.Option(
103
+ 98,
104
+ '-c',
105
+ '--confidence',
106
+ help=(
107
+ 'Confidence level to evaluate measurements at as int percentage points [50,99], '
108
+ 'inclusive, representing 50% to 99%'
109
+ ),
110
+ min=50,
111
+ max=99,
112
+ ),
113
+ bits: str = typer.Option(
114
+ '1000,9000,1000',
115
+ '-b',
116
+ '--bits',
117
+ help=(
118
+ 'Bit lengths to investigate as [green]"int,int,int"[/]; behaves like arguments for range(), '
119
+ 'i.e., [green]"start,stop,step"[/], eg. [green]"1000,3000,500"[/] will investigate '
120
+ '[yellow]1000,1500,2000,2500[/]'
121
+ ),
122
+ ),
123
+ ) -> None:
124
+ if version:
125
+ typer.echo(__version__)
126
+ raise typer.Exit(0)
127
+ console, verbose, color = clibase.InitLogging(
128
+ verbose,
129
+ color=color,
130
+ include_process=False, # decide if you want process names in logs
131
+ soft_wrap=False, # decide if you want soft wrapping of long lines
132
+ )
133
+ # create context with the arguments we received
134
+ int_bits: tuple[int, ...] = tuple(int(x, 10) for x in bits.strip().split(','))
135
+ if len(int_bits) != 3: # noqa: PLR2004
136
+ raise typer.BadParameter(
137
+ '-b/--bits should be 3 ints, like: start,stop,step; eg.: 1000,3000,500'
138
+ )
139
+ ctx.obj = ProfilerConfig(
140
+ console=console,
141
+ verbose=verbose,
142
+ color=color,
143
+ serial=serial,
144
+ repeats=repeats,
145
+ confidence=confidence,
146
+ bits=(int_bits[0], int_bits[1], int_bits[2]),
147
+ )
148
+
149
+
150
+ @app.command(
151
+ 'primes',
152
+ help='Measure regular prime generation.',
153
+ epilog=(
154
+ 'Example:\n\n\n\n'
155
+ '$ poetry run profiler -n 30 -b 9000,11000,1000 primes\n\n'
156
+ 'Starting [yellow]SERIAL regular primes[/] test\n\n'
157
+ '9000 → 38.88 s ± 14.74 s [24.14 s … 53.63 s]98%CI@30\n\n'
158
+ '10000 → 41.26 s ± 22.82 s [18.44 s … 1.07 min]98%CI@30\n\n'
159
+ 'Finished in 40.07 min'
160
+ ),
161
+ )
162
+ @clibase.CLIErrorGuard
163
+ def Primes(*, ctx: typer.Context) -> None: # documentation is help/epilog/args # noqa: D103
164
+ config: ProfilerConfig = ctx.obj # get application global config
165
+ config.console.print(
166
+ f'Starting [yellow]{"SERIAL" if config.serial else "PARALLEL"} regular primes[/] test'
167
+ )
168
+ _PrimeProfiler(
169
+ lambda n: modmath.NBitRandomPrimes(n, serial=config.serial, n_primes=1).pop(),
170
+ config.console,
171
+ config.repeats,
172
+ config.bits,
173
+ config.confidence / 100.0,
174
+ )
175
+
176
+
177
+ @app.command(
178
+ 'dsa',
179
+ help='Measure DSA prime generation.',
180
+ epilog=(
181
+ 'Example:\n\n\n\n'
182
+ '$ poetry run profiler --no-serial -n 2 -b 1000,1500,100 -c 80 dsa\n\n'
183
+ 'Starting [yellow]PARALLEL DSA primes[/] test\n\n'
184
+ '1000 → 236.344 ms ± 273.236 ms [*0.00 s … 509.580 ms]80%CI@2\n\n'
185
+ '1100 → 319.308 ms ± 639.775 ms [*0.00 s … 959.083 ms]80%CI@2\n\n'
186
+ '1200 → 523.885 ms ± 879.981 ms [*0.00 s … 1.40 s]80%CI@2\n\n'
187
+ '1300 → 506.285 ms ± 687.153 ms [*0.00 s … 1.19 s]80%CI@2\n\n'
188
+ '1400 → 552.840 ms ± 47.012 ms [505.828 ms … 599.852 ms]80%CI@2\n\n'
189
+ 'Finished in 4.12 s'
190
+ ),
191
+ )
192
+ @clibase.CLIErrorGuard
193
+ def DSA(*, ctx: typer.Context) -> None: # documentation is help/epilog/args # noqa: D103
194
+ config: ProfilerConfig = ctx.obj # get application global config
195
+ config.console.print(
196
+ f'Starting [yellow]{"SERIAL" if config.serial else "PARALLEL"} DSA primes[/] test'
197
+ )
198
+ _PrimeProfiler(
199
+ lambda n: dsa.NBitRandomDSAPrimes(n, n // 2, serial=config.serial)[0],
200
+ config.console,
201
+ config.repeats,
202
+ config.bits,
203
+ config.confidence / 100.0,
204
+ )
205
+
206
+
207
+ @app.command(
208
+ 'markdown',
209
+ help='Emit Markdown docs for the CLI (see README.md section "Creating a New Version").',
210
+ epilog='Example:\n\n\n\n$ poetry run profiler markdown > profiler.md\n\n<<saves CLI doc>>',
211
+ )
212
+ @clibase.CLIErrorGuard
213
+ def Markdown() -> None: # documentation is help/epilog/args # noqa: D103
214
+ console: rich_console.Console = clibase.Console()
215
+ console.print(clibase.GenerateTyperHelpMarkdown(app, prog_name='profiler'))
107
216
 
108
217
 
109
218
  def _PrimeProfiler(
110
- prime_callable: Callable[[int], int],
111
- repeats: int, n_bits_range: tuple[int, int, int], confidence: float,
112
- console: rich_console.Console, /) -> None:
113
- primes: dict[int, list[float]] = {}
114
- for n_bits in range(*n_bits_range):
115
- # investigate for size n_bits
116
- primes[n_bits] = []
117
- for _ in range(repeats):
118
- with base.Timer(emit_log=False) as tmr:
119
- pr: int = prime_callable(n_bits)
120
- assert pr and pr.bit_length() == n_bits
121
- primes[n_bits].append(tmr.elapsed)
122
- # finished collecting n_bits-sized primes
123
- measurements: str = base.HumanizedMeasurements(
124
- primes[n_bits], parser=base.HumanizedSeconds, confidence=confidence)
125
- console.print(f'{n_bits} {measurements}')
126
-
127
-
128
- def main(argv: list[str] | None = None, /) -> int: # pylint: disable=invalid-name,too-many-locals,too-many-branches,too-many-statements
129
- """Main entry point."""
130
- # build the parser and parse args
131
- parser: argparse.ArgumentParser = _BuildParser()
132
- args: argparse.Namespace = parser.parse_args(argv)
133
- # take care of global options
134
- console: rich_console.Console = base.InitLogging(args.verbose)
135
-
136
- try:
137
- # get the command, do basic checks and switch
138
- command: str = args.command.lower().strip() if args.command else ''
139
- repeats: int = 1 if args.number < 1 else args.number
140
- confidence: int = 55 if args.confidence < 55 else args.confidence
141
- confidence = 99 if confidence > 99 else confidence
142
- args.serial = True if (not args.serial and not args.parallel) else args.serial # make default
143
- bits: tuple[int, ...] = tuple(int(x, 10) for x in args.bits.strip().split(','))
144
- if len(bits) != 3:
145
- raise base.InputError('-b/--bits should be 3 ints, like: start,stop,step; eg.: 1000,3000,500')
146
- with base.Timer(emit_log=False) as tmr:
147
- match command:
148
- # -------- Primes ----------
149
- case 'primes':
150
- console.print(f'Starting {"SERIAL" if args.serial else "PARALLEL"} regular primes test')
151
- _PrimeProfiler(
152
- lambda n: modmath.NBitRandomPrimes(n, serial=args.serial, n_primes=1).pop(),
153
- repeats, bits, confidence / 100.0, console)
154
-
155
- case 'dsa':
156
- console.print(f'Starting {"SERIAL" if args.serial else "PARALLEL"} DSA primes test')
157
- _PrimeProfiler(
158
- lambda n: dsa.NBitRandomDSAPrimes(n, n // 2, serial=args.serial)[0],
159
- repeats, bits, confidence / 100.0, console)
160
-
161
- # -------- Documentation ----------
162
- case 'doc':
163
- doc_command: str = (
164
- args.doc_command.lower().strip() if getattr(args, 'doc_command', '') else '')
165
- match doc_command:
166
- case 'md':
167
- console.print(base.GenerateCLIMarkdown(
168
- 'profiler', _BuildParser(), description=(
169
- '`profiler` is a command-line utility that provides stats on TransCrypto '
170
- 'performance.')))
171
- case _:
172
- raise NotImplementedError()
173
-
174
- case _:
175
- parser.print_help()
176
-
177
- if command not in ('doc',):
178
- console.print(f'Finished in {tmr}')
179
-
180
- except NotImplementedError as err:
181
- console.print(f'Invalid command: {err}')
182
- except (base.Error, ValueError) as err:
183
- console.print(str(err))
184
-
185
- return 0
186
-
187
-
188
- if __name__ == '__main__':
189
- sys.exit(main())
219
+ prime_callable: abc.Callable[[int], int],
220
+ console: rich_console.Console,
221
+ repeats: int,
222
+ n_bits_range: tuple[int, int, int],
223
+ confidence: float,
224
+ /,
225
+ ) -> None:
226
+ with base.Timer(emit_log=False) as total_time:
227
+ primes: dict[int, list[float]] = {}
228
+ for n_bits in range(*n_bits_range):
229
+ # investigate for size n_bits
230
+ primes[n_bits] = []
231
+ for _ in range(repeats):
232
+ with base.Timer(emit_log=False) as run_time:
233
+ pr: int = prime_callable(n_bits)
234
+ assert pr # noqa: S101
235
+ assert pr.bit_length() == n_bits # noqa: S101
236
+ primes[n_bits].append(run_time.elapsed)
237
+ # finished collecting n_bits-sized primes
238
+ measurements: str = base.HumanizedMeasurements(
239
+ primes[n_bits], parser=base.HumanizedSeconds, confidence=confidence
240
+ )
241
+ console.print(f'{n_bits} {measurements}')
242
+ console.print(f'Finished in {total_time}')