transcrypto 1.7.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.
@@ -0,0 +1,427 @@
1
+ # SPDX-FileCopyrightText: Copyright 2026 Daniel Balparda <balparda@github.com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Balparda's TransCrypto CLI: Integer mathematics commands."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import typer
8
+
9
+ from transcrypto import base, modmath, transcrypto
10
+ from transcrypto.cli import clibase
11
+
12
+ # =============================== "PRIME"-like COMMANDS ============================================
13
+
14
+
15
+ @transcrypto.app.command(
16
+ 'isprime',
17
+ help='Primality test with safe defaults, useful for any integer size.',
18
+ epilog=(
19
+ 'Example:\n\n\n\n'
20
+ '$ poetry run transcrypto isprime 2305843009213693951\n\n'
21
+ 'True\n\n'
22
+ '$ poetry run transcrypto isprime 2305843009213693953\n\n'
23
+ 'False'
24
+ ),
25
+ )
26
+ @clibase.CLIErrorGuard
27
+ def IsPrimeCLI( # documentation is help/epilog/args # noqa: D103
28
+ *,
29
+ ctx: typer.Context,
30
+ n: str = typer.Argument(..., help='Integer to test, ≥ 1'),
31
+ ) -> None:
32
+ config: transcrypto.TransConfig = ctx.obj
33
+ n_i: int = transcrypto.ParseInt(n, min_value=1)
34
+ config.console.print(str(modmath.IsPrime(n_i)))
35
+
36
+
37
+ @transcrypto.app.command(
38
+ 'primegen',
39
+ help='Generate (stream) primes ≥ `start` (prints a limited `count` by default).',
40
+ epilog=('Example:\n\n\n\n$ poetry run transcrypto primegen 100 -c 3\n\n101\n\n103\n\n107'),
41
+ )
42
+ @clibase.CLIErrorGuard
43
+ def PrimeGenCLI( # documentation is help/epilog/args # noqa: D103
44
+ *,
45
+ ctx: typer.Context,
46
+ start: str = typer.Argument(..., help='Starting integer (inclusive), ≥ 0'),
47
+ count: int = typer.Option(1, '-c', '--count', min=1, help='How many to print, ≥ 1'),
48
+ ) -> None:
49
+ config: transcrypto.TransConfig = ctx.obj
50
+ start_i: int = transcrypto.ParseInt(start, min_value=0)
51
+ for i, pr in enumerate(modmath.PrimeGenerator(start_i)):
52
+ if i >= count:
53
+ return
54
+ config.console.print(pr)
55
+
56
+
57
+ @transcrypto.app.command(
58
+ 'mersenne',
59
+ help=(
60
+ 'Generate (stream) Mersenne prime exponents `k`, also outputting `2^k-1` '
61
+ '(the Mersenne prime, `M`) and `M×2^(k-1)` (the associated perfect number), ' # noqa: RUF001
62
+ 'starting at `min-k` and stopping once `k` > `max-k`.'
63
+ ),
64
+ epilog=(
65
+ 'Example:\n\n\n\n'
66
+ '$ poetry run transcrypto mersenne -k 0 -m 15\n\n'
67
+ 'k=2 M=3 perfect=6\n\n'
68
+ 'k=3 M=7 perfect=28\n\n'
69
+ 'k=5 M=31 perfect=496\n\n'
70
+ 'k=7 M=127 perfect=8128\n\n'
71
+ 'k=13 M=8191 perfect=33550336\n\n'
72
+ 'k=17 M=131071 perfect=8589869056'
73
+ ),
74
+ )
75
+ @clibase.CLIErrorGuard
76
+ def MersenneCLI( # documentation is help/epilog/args # noqa: D103
77
+ *,
78
+ ctx: typer.Context,
79
+ min_k: int = typer.Option(2, '-k', '--min-k', min=1, help='Starting exponent `k`, ≥ 2'),
80
+ max_k: int = typer.Option(10000, '-m', '--max-k', min=1, help='Stop once `k` > `max-k`, ≥ 2'),
81
+ ) -> None:
82
+ config: transcrypto.TransConfig = ctx.obj
83
+ if max_k < min_k:
84
+ raise base.InputError(f'max-k ({max_k}) must be >= min-k ({min_k})')
85
+ for k, m, perfect in modmath.MersennePrimesGenerator(min_k):
86
+ if k > max_k:
87
+ return
88
+ config.console.print(f'k={k} M={m} perfect={perfect}')
89
+
90
+
91
+ # ================================== "*GCD" COMMANDS ===============================================
92
+
93
+
94
+ @transcrypto.app.command(
95
+ 'gcd',
96
+ help='Greatest Common Divisor (GCD) of integers `a` and `b`.',
97
+ epilog=(
98
+ 'Example:\n\n\n\n'
99
+ '$ poetry run transcrypto gcd 462 1071\n\n'
100
+ '21\n\n'
101
+ '$ poetry run transcrypto gcd 0 5\n\n'
102
+ '5\n\n'
103
+ '$ poetry run transcrypto gcd 127 13\n\n'
104
+ '1'
105
+ ),
106
+ )
107
+ @clibase.CLIErrorGuard
108
+ def GcdCLI( # documentation is help/epilog/args # noqa: D103
109
+ *,
110
+ ctx: typer.Context,
111
+ a: str = typer.Argument(..., help='Integer, ≥ 0'),
112
+ b: str = typer.Argument(..., help="Integer, ≥ 0 (can't be both zero)"),
113
+ ) -> None:
114
+ config: transcrypto.TransConfig = ctx.obj
115
+ a_i: int = transcrypto.ParseInt(a, min_value=0)
116
+ b_i: int = transcrypto.ParseInt(b, min_value=0)
117
+ if a_i == 0 and b_i == 0:
118
+ raise base.InputError("`a` and `b` can't both be zero")
119
+ config.console.print(base.GCD(a_i, b_i))
120
+
121
+
122
+ @transcrypto.app.command(
123
+ 'xgcd',
124
+ help=(
125
+ 'Extended Greatest Common Divisor (x-GCD) of integers `a` and `b`, '
126
+ 'will return `(g, x, y)` where `a×x+b×y==g`.' # noqa: RUF001
127
+ ),
128
+ epilog=(
129
+ 'Example:\n\n\n\n'
130
+ '$ poetry run transcrypto xgcd 462 1071\n\n'
131
+ '(21, 7, -3)\n\n'
132
+ '$ poetry run transcrypto xgcd 0 5\n\n'
133
+ '(5, 0, 1)\n\n'
134
+ '$ poetry run transcrypto xgcd 127 13\n\n'
135
+ '(1, 4, -39)'
136
+ ),
137
+ )
138
+ @clibase.CLIErrorGuard
139
+ def XgcdCLI( # documentation is help/epilog/args # noqa: D103
140
+ *,
141
+ ctx: typer.Context,
142
+ a: str = typer.Argument(..., help='Integer, ≥ 0'),
143
+ b: str = typer.Argument(..., help="Integer, ≥ 0 (can't be both zero)"),
144
+ ) -> None:
145
+ config: transcrypto.TransConfig = ctx.obj
146
+ a_i: int = transcrypto.ParseInt(a, min_value=0)
147
+ b_i: int = transcrypto.ParseInt(b, min_value=0)
148
+ if a_i == 0 and b_i == 0:
149
+ raise base.InputError("`a` and `b` can't both be zero")
150
+ config.console.print(str(base.ExtendedGCD(a_i, b_i)))
151
+
152
+
153
+ # ================================= "RANDOM" COMMAND ===============================================
154
+
155
+
156
+ random_app = typer.Typer(
157
+ no_args_is_help=True,
158
+ help='Cryptographically secure randomness, from the OS CSPRNG.',
159
+ )
160
+ transcrypto.app.add_typer(random_app, name='random')
161
+
162
+
163
+ @random_app.command(
164
+ 'bits',
165
+ help='Random integer with exact bit length = `bits` (MSB will be 1).',
166
+ epilog=('Example:\n\n\n\n$ poetry run transcrypto random bits 16\n\n36650'),
167
+ )
168
+ @clibase.CLIErrorGuard
169
+ def RandomBits( # documentation is help/epilog/args # noqa: D103
170
+ *,
171
+ ctx: typer.Context,
172
+ bits: int = typer.Argument(..., min=8, help='Number of bits, ≥ 8'),
173
+ ) -> None:
174
+ config: transcrypto.TransConfig = ctx.obj
175
+ config.console.print(base.RandBits(bits))
176
+
177
+
178
+ @random_app.command(
179
+ 'int',
180
+ help='Uniform random integer in `[min, max]` range, inclusive.',
181
+ epilog=('Example:\n\n\n\n$ poetry run transcrypto random int 1000 2000\n\n1628'),
182
+ )
183
+ @clibase.CLIErrorGuard
184
+ def RandomInt( # documentation is help/epilog/args # noqa: D103
185
+ *,
186
+ ctx: typer.Context,
187
+ min_: str = typer.Argument(..., help='Minimum, ≥ 0'),
188
+ max_: str = typer.Argument(..., help='Maximum, > `min`'),
189
+ ) -> None:
190
+ config: transcrypto.TransConfig = ctx.obj
191
+ min_i: int = transcrypto.ParseInt(min_, min_value=0)
192
+ max_i: int = transcrypto.ParseInt(max_, min_value=min_i + 1)
193
+ config.console.print(base.RandInt(min_i, max_i))
194
+
195
+
196
+ @random_app.command(
197
+ 'bytes',
198
+ help='Generates `n` cryptographically secure random bytes.',
199
+ epilog=(
200
+ 'Example:\n\n\n\n'
201
+ '$ poetry run transcrypto random bytes 32\n\n'
202
+ '6c6f1f88cb93c4323285a2224373d6e59c72a9c2b82e20d1c376df4ffbe9507f'
203
+ ),
204
+ )
205
+ @clibase.CLIErrorGuard
206
+ def RandomBytes( # documentation is help/epilog/args # noqa: D103
207
+ *,
208
+ ctx: typer.Context,
209
+ n: int = typer.Argument(..., min=1, help='Number of bytes, ≥ 1'),
210
+ ) -> None:
211
+ config: transcrypto.TransConfig = ctx.obj
212
+ config.console.print(transcrypto.BytesToText(base.RandBytes(n), config.output_format))
213
+
214
+
215
+ @random_app.command(
216
+ 'prime',
217
+ help='Generate a random prime with exact bit length = `bits` (MSB will be 1).',
218
+ epilog=('Example:\n\n\n\n$ poetry run transcrypto random prime 32\n\n2365910551'),
219
+ )
220
+ @clibase.CLIErrorGuard
221
+ def RandomPrime( # documentation is help/epilog/args # noqa: D103
222
+ *,
223
+ ctx: typer.Context,
224
+ bits: int = typer.Argument(..., min=11, help='Bit length, ≥ 11'),
225
+ ) -> None:
226
+ config: transcrypto.TransConfig = ctx.obj
227
+ config.console.print(modmath.NBitRandomPrimes(bits).pop())
228
+
229
+
230
+ # =================================== "MOD" COMMAND ================================================
231
+
232
+
233
+ mod_app = typer.Typer(
234
+ no_args_is_help=True,
235
+ help='Modular arithmetic helpers.',
236
+ )
237
+ transcrypto.app.add_typer(mod_app, name='mod')
238
+
239
+
240
+ @mod_app.command(
241
+ 'inv',
242
+ help=(
243
+ 'Modular inverse: find integer 0≤`i`<`m` such that `a×i ≡ 1 (mod m)`. ' # noqa: RUF001
244
+ 'Will only work if `gcd(a,m)==1`, else will fail with a message.'
245
+ ),
246
+ epilog=(
247
+ 'Example:\n\n\n\n'
248
+ '$ poetry run transcrypto mod inv 127 13\n\n'
249
+ '4\n\n'
250
+ '$ poetry run transcrypto mod inv 17 3120\n\n'
251
+ '2753\n\n'
252
+ '$ poetry run transcrypto mod inv 462 1071\n\n'
253
+ '<<INVALID>> no modular inverse exists (ModularDivideError)'
254
+ ),
255
+ )
256
+ @clibase.CLIErrorGuard
257
+ def ModInv( # documentation is help/epilog/args # noqa: D103
258
+ *,
259
+ ctx: typer.Context,
260
+ a: str = typer.Argument(..., help='Integer to invert'),
261
+ m: str = typer.Argument(..., help='Modulus `m`, ≥ 2'),
262
+ ) -> None:
263
+ config: transcrypto.TransConfig = ctx.obj
264
+ a_i: int = transcrypto.ParseInt(a)
265
+ m_i: int = transcrypto.ParseInt(m, min_value=2)
266
+ try:
267
+ config.console.print(modmath.ModInv(a_i, m_i))
268
+ except modmath.ModularDivideError:
269
+ config.console.print('<<INVALID>> no modular inverse exists (ModularDivideError)')
270
+
271
+
272
+ @mod_app.command(
273
+ 'div',
274
+ help=(
275
+ 'Modular division: find integer 0≤`z`<`m` such that `z×y ≡ x (mod m)`. ' # noqa: RUF001
276
+ 'Will only work if `gcd(y,m)==1` and `y!=0`, else will fail with a message.'
277
+ ),
278
+ epilog=(
279
+ 'Example:\n\n\n\n'
280
+ '$ poetry run transcrypto mod div 6 127 13\n\n'
281
+ '11\n\n'
282
+ '$ poetry run transcrypto mod div 6 0 13\n\n'
283
+ '<<INVALID>> divide-by-zero or not invertible (ModularDivideError)'
284
+ ),
285
+ )
286
+ @clibase.CLIErrorGuard
287
+ def ModDiv( # documentation is help/epilog/args # noqa: D103
288
+ *,
289
+ ctx: typer.Context,
290
+ x: str = typer.Argument(..., help='Integer'),
291
+ y: str = typer.Argument(..., help='Integer, cannot be zero'),
292
+ m: str = typer.Argument(..., help='Modulus `m`, ≥ 2'),
293
+ ) -> None:
294
+ config: transcrypto.TransConfig = ctx.obj
295
+ x_i: int = transcrypto.ParseInt(x)
296
+ y_i: int = transcrypto.ParseInt(y)
297
+ m_i: int = transcrypto.ParseInt(m, min_value=2)
298
+ try:
299
+ config.console.print(modmath.ModDiv(x_i, y_i, m_i))
300
+ except modmath.ModularDivideError:
301
+ config.console.print('<<INVALID>> divide-by-zero or not invertible (ModularDivideError)')
302
+
303
+
304
+ @mod_app.command(
305
+ 'exp',
306
+ help='Modular exponentiation: `a^e mod m`. Efficient, can handle huge values.',
307
+ epilog=(
308
+ 'Example:\n\n\n\n'
309
+ '$ poetry run transcrypto mod exp 438 234 127\n\n'
310
+ '32\n\n'
311
+ '$ poetry run transcrypto mod exp 438 234 89854\n\n'
312
+ '60622'
313
+ ),
314
+ )
315
+ @clibase.CLIErrorGuard
316
+ def ModExp( # documentation is help/epilog/args # noqa: D103
317
+ *,
318
+ ctx: typer.Context,
319
+ a: str = typer.Argument(..., help='Integer value'),
320
+ e: str = typer.Argument(..., help='Integer exponent, ≥ 0'),
321
+ m: str = typer.Argument(..., help='Modulus `m`, ≥ 2'),
322
+ ) -> None:
323
+ config: transcrypto.TransConfig = ctx.obj
324
+ a_i: int = transcrypto.ParseInt(a)
325
+ e_i: int = transcrypto.ParseInt(e, min_value=0)
326
+ m_i: int = transcrypto.ParseInt(m, min_value=2)
327
+ config.console.print(modmath.ModExp(a_i, e_i, m_i))
328
+
329
+
330
+ @mod_app.command(
331
+ 'poly',
332
+ help=(
333
+ 'Efficiently evaluate polynomial with `coeff` coefficients at point `x` modulo `m` '
334
+ '(`c₀+c₁×x+c₂×x²+…+cₙ×x^n mod m`).' # noqa: RUF001
335
+ ),
336
+ epilog=(
337
+ 'Example:\n\n\n\n'
338
+ '$ poetry run transcrypto mod poly 12 17 10 20 30\n\n'
339
+ '14 # (10+20×12+30×12² ≡ 14 (mod 17))\n\n' # noqa: RUF001
340
+ '$ poetry run transcrypto mod poly 10 97 3 0 0 1 1\n\n'
341
+ '42 # (3+1×10³+1×10⁴ ≡ 42 (mod 97))' # noqa: RUF001
342
+ ),
343
+ )
344
+ @clibase.CLIErrorGuard
345
+ def ModPoly( # documentation is help/epilog/args # noqa: D103
346
+ *,
347
+ ctx: typer.Context,
348
+ x: str = typer.Argument(..., help='Evaluation point `x`'),
349
+ m: str = typer.Argument(..., help='Modulus `m`, ≥ 2'),
350
+ coeff: list[str] = typer.Argument( # noqa: B008
351
+ ...,
352
+ help='Coefficients (constant-term first: `c₀+c₁×x+c₂×x²+…+cₙ×x^n`)', # noqa: RUF001
353
+ ),
354
+ ) -> None:
355
+ config: transcrypto.TransConfig = ctx.obj
356
+ x_i: int = transcrypto.ParseInt(x)
357
+ m_i: int = transcrypto.ParseInt(m, min_value=2)
358
+ coeff_i: list[int] = [transcrypto.ParseInt(z) for z in coeff]
359
+ config.console.print(modmath.ModPolynomial(x_i, coeff_i, m_i))
360
+
361
+
362
+ @mod_app.command(
363
+ 'lagrange',
364
+ help=(
365
+ 'Lagrange interpolation over modulus `m`: find the `f(x)` solution for the '
366
+ 'given `x` and `zₙ:f(zₙ)` points `pt`. The modulus `m` must be a prime.'
367
+ ),
368
+ epilog=(
369
+ 'Example:\n\n\n\n'
370
+ '$ poetry run transcrypto mod lagrange 5 13 2:4 6:3 7:1\n\n'
371
+ '3 # passes through (2,4), (6,3), (7,1)\n\n'
372
+ '$ poetry run transcrypto mod lagrange 11 97 1:1 2:4 3:9 4:16 5:25\n\n'
373
+ '24 # passes through (1,1), (2,4), (3,9), (4,16), (5,25)'
374
+ ),
375
+ )
376
+ @clibase.CLIErrorGuard
377
+ def ModLagrange( # documentation is help/epilog/args # noqa: D103
378
+ *,
379
+ ctx: typer.Context,
380
+ x: str = typer.Argument(..., help='Evaluation point `x`'),
381
+ m: str = typer.Argument(..., help='Modulus `m`, ≥ 2'),
382
+ pt: list[str] = typer.Argument( # noqa: B008
383
+ ...,
384
+ help='Points `zₙ:f(zₙ)` as `key:value` pairs (e.g., `2:4 5:3 7:1`)',
385
+ ),
386
+ ) -> None:
387
+ config: transcrypto.TransConfig = ctx.obj
388
+ x_i: int = transcrypto.ParseInt(x)
389
+ m_i: int = transcrypto.ParseInt(m, min_value=2)
390
+ pts: dict[int, int] = dict(transcrypto.ParseIntPairCLI(kv) for kv in pt)
391
+ config.console.print(modmath.ModLagrangeInterpolate(x_i, pts, m_i))
392
+
393
+
394
+ @mod_app.command(
395
+ 'crt',
396
+ help=(
397
+ 'Solves Chinese Remainder Theorem (CRT) Pair: finds the unique integer 0≤`x`<`(m1×m2)` ' # noqa: RUF001
398
+ 'satisfying both `x ≡ a1 (mod m1)` and `x ≡ a2 (mod m2)`, if `gcd(m1,m2)==1`.'
399
+ ),
400
+ epilog=(
401
+ 'Example:\n\n\n\n'
402
+ '$ poetry run transcrypto mod crt 6 7 127 13\n\n'
403
+ '62\n\n'
404
+ '$ poetry run transcrypto mod crt 12 56 17 19\n\n'
405
+ '796\n\n'
406
+ '$ poetry run transcrypto mod crt 6 7 462 1071\n\n'
407
+ '<<INVALID>> moduli m1/m2 not co-prime (ModularDivideError)'
408
+ ),
409
+ )
410
+ @clibase.CLIErrorGuard
411
+ def ModCRT( # documentation is help/epilog/args # noqa: D103
412
+ *,
413
+ ctx: typer.Context,
414
+ a1: str = typer.Argument(..., help='Integer residue for first congruence'),
415
+ m1: str = typer.Argument(..., help='Modulus `m1`, ≥ 2'),
416
+ a2: str = typer.Argument(..., help='Integer residue for second congruence'),
417
+ m2: str = typer.Argument(..., help='Modulus `m2`, ≥ 2, !=`m1`, and `gcd(m1,m2)==1`'),
418
+ ) -> None:
419
+ config: transcrypto.TransConfig = ctx.obj
420
+ a1_i: int = transcrypto.ParseInt(a1)
421
+ m1_i: int = transcrypto.ParseInt(m1, min_value=2)
422
+ a2_i: int = transcrypto.ParseInt(a2)
423
+ m2_i: int = transcrypto.ParseInt(m2, min_value=2)
424
+ try:
425
+ config.console.print(modmath.CRTPair(a1_i, m1_i, a2_i, m2_i))
426
+ except modmath.ModularDivideError:
427
+ config.console.print('<<INVALID>> moduli `m1`/`m2` not co-prime (ModularDivideError)')