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