transcrypto 1.6.0__tar.gz → 1.8.0__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.
Files changed (33) hide show
  1. {transcrypto-1.6.0/src/transcrypto.egg-info → transcrypto-1.8.0}/PKG-INFO +88 -66
  2. {transcrypto-1.6.0 → transcrypto-1.8.0}/README.md +67 -59
  3. transcrypto-1.8.0/pyproject.toml +316 -0
  4. transcrypto-1.8.0/src/transcrypto/__init__.py +7 -0
  5. {transcrypto-1.6.0 → transcrypto-1.8.0}/src/transcrypto/aes.py +150 -44
  6. {transcrypto-1.6.0 → transcrypto-1.8.0}/src/transcrypto/base.py +384 -520
  7. transcrypto-1.8.0/src/transcrypto/cli/__init__.py +3 -0
  8. transcrypto-1.8.0/src/transcrypto/cli/aeshash.py +368 -0
  9. transcrypto-1.8.0/src/transcrypto/cli/bidsecret.py +334 -0
  10. transcrypto-1.8.0/src/transcrypto/cli/clibase.py +303 -0
  11. transcrypto-1.8.0/src/transcrypto/cli/intmath.py +427 -0
  12. transcrypto-1.8.0/src/transcrypto/cli/publicalgos.py +877 -0
  13. transcrypto-1.8.0/src/transcrypto/constants.py +20085 -0
  14. {transcrypto-1.6.0 → transcrypto-1.8.0}/src/transcrypto/dsa.py +132 -99
  15. {transcrypto-1.6.0 → transcrypto-1.8.0}/src/transcrypto/elgamal.py +116 -84
  16. {transcrypto-1.6.0 → transcrypto-1.8.0}/src/transcrypto/modmath.py +88 -78
  17. transcrypto-1.8.0/src/transcrypto/profiler.py +242 -0
  18. {transcrypto-1.6.0 → transcrypto-1.8.0}/src/transcrypto/rsa.py +126 -90
  19. {transcrypto-1.6.0 → transcrypto-1.8.0}/src/transcrypto/sss.py +122 -70
  20. transcrypto-1.8.0/src/transcrypto/transcrypto.py +461 -0
  21. transcrypto-1.6.0/PKG-INFO +0 -1071
  22. transcrypto-1.6.0/pyproject.toml +0 -79
  23. transcrypto-1.6.0/setup.cfg +0 -4
  24. transcrypto-1.6.0/src/transcrypto/__init__.py +0 -0
  25. transcrypto-1.6.0/src/transcrypto/constants.py +0 -1921
  26. transcrypto-1.6.0/src/transcrypto/profiler.py +0 -189
  27. transcrypto-1.6.0/src/transcrypto/safetrans.py +0 -1228
  28. transcrypto-1.6.0/src/transcrypto/transcrypto.py +0 -1465
  29. transcrypto-1.6.0/src/transcrypto.egg-info/SOURCES.txt +0 -20
  30. transcrypto-1.6.0/src/transcrypto.egg-info/dependency_links.txt +0 -1
  31. transcrypto-1.6.0/src/transcrypto.egg-info/top_level.txt +0 -1
  32. {transcrypto-1.6.0 → transcrypto-1.8.0}/LICENSE +0 -0
  33. {transcrypto-1.6.0 → transcrypto-1.8.0}/src/transcrypto/py.typed +0 -0
@@ -1,20 +1,33 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: transcrypto
3
- Version: 1.6.0
3
+ Version: 1.8.0
4
4
  Summary: Basic crypto primitives, not intended for actual use, but as a companion to --Criptografia, Métodos e Algoritmos--
5
- Author-email: Daniel Balparda <balparda@github.com>
6
5
  License-Expression: Apache-2.0
7
- Project-URL: Homepage, https://github.com/balparda/transcrypto
8
- Project-URL: PyPI, https://pypi.org/project/transcrypto/
6
+ License-File: LICENSE
7
+ Keywords: cryptography,validation,encryption,signing,random-generation,prime-numbers,aes-encryption,decryption,rsa-cryptography,elgamal-encryption,dsa-algorithm,modular-mathematics,rsa,dsa,elgamal,aes,python,poetry,typer,rich,cli
8
+ Author: Daniel Balparda
9
+ Author-email: balparda@github.com
10
+ Requires-Python: >=3.12
9
11
  Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.12
10
13
  Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
11
15
  Classifier: Operating System :: OS Independent
12
16
  Classifier: Topic :: Utilities
13
17
  Classifier: Topic :: Security :: Cryptography
14
- Requires-Python: <4.0,>=3.13.5
18
+ Requires-Dist: cryptography (>=46.0)
19
+ Requires-Dist: gmpy2 (>=2.2)
20
+ Requires-Dist: platformdirs (>=4.5)
21
+ Requires-Dist: rich (>=14.3)
22
+ Requires-Dist: scipy (>=1.17)
23
+ Requires-Dist: typer (>=0.21)
24
+ Requires-Dist: zstandard (>=0.25)
25
+ Project-URL: Changelog, https://github.com/balparda/transcrypto/blob/main/CHANGELOG.md
26
+ Project-URL: Homepage, https://github.com/balparda/transcrypto
27
+ Project-URL: Issues, https://github.com/balparda/transcrypto/issues
28
+ Project-URL: PyPI, https://pypi.org/project/transcrypto/
29
+ Project-URL: Repository, https://github.com/balparda/transcrypto
15
30
  Description-Content-Type: text/markdown
16
- License-File: LICENSE
17
- Dynamic: license-file
18
31
 
19
32
  # TransCrypto
20
33
 
@@ -76,7 +89,7 @@ Started in July/2025, by Daniel Balparda. Since version 1.0.2 it is PyPI package
76
89
 
77
90
  ## License
78
91
 
79
- Copyright 2025 Daniel Balparda <balparda@github.com>
92
+ Copyright 2026 Daniel Balparda <balparda@github.com>
80
93
 
81
94
  Licensed under the ***Apache License, Version 2.0*** (the "License"); you may not use this file except in compliance with the License. You may obtain a [copy of the License here](http://www.apache.org/licenses/LICENSE-2.0).
82
95
 
@@ -94,7 +107,6 @@ All that being said, extreme care was taken that this is a good library with a s
94
107
 
95
108
  ## CLI Apps
96
109
 
97
- - [SafeTrans/`safetrans`](safetrans.md): Safe cryptographic operations;
98
110
  - [TransCrypto/`transcrypto`](transcrypto.md): Does all the operations but allows you to shoot yourself in the foot;
99
111
  - [Profiler/`profiler`](profiler.md): Measure transcrypto performance.
100
112
 
@@ -122,14 +134,18 @@ Known dependencies:
122
134
  #### Humanized Sizes (IEC binary)
123
135
 
124
136
  ```py
125
- from transcrypto import utils
137
+ from transcrypto import base
126
138
 
127
- utils.HumanizedBytes(512) # '512 B'
128
- utils.HumanizedBytes(2048) # '2.00 KiB'
129
- utils.HumanizedBytes(5 * 1024**3) # '5.00 GiB'
139
+ base.HumanizedBytes(512) # '512 B'
140
+ base.HumanizedBytes(2048) # '2.000 KiB'
141
+ base.HumanizedBytes(5 * 1024**3) # '5.000 GiB'
130
142
  ```
131
143
 
132
- Converts raw byte counts to binary-prefixed strings (`B`, `KiB`, `MiB`, `GiB`, `TiB`, `PiB`, `EiB`). Values under 1024 bytes are returned as integers with `B`; larger values use two decimals.
144
+ Converts raw byte counts to binary-prefixed strings (`B`, `KiB`, `MiB`, `GiB`, `TiB`, `PiB`, `EiB`).
145
+
146
+ - For integer inputs `<1024`, returns an integer count with `B` (e.g. `'512 B'`).
147
+ - For float inputs `<1024`, returns 3 decimals with `B` (e.g. `'51.200 B'`).
148
+ - For values `≥1024`, returns 3 decimals.
133
149
 
134
150
  - standard: 1 KiB = 1024 B, 1 MiB = 1024 KiB, …
135
151
  - errors: negative inputs raise `InputError`
@@ -138,43 +154,47 @@ Converts raw byte counts to binary-prefixed strings (`B`, `KiB`, `MiB`, `GiB`, `
138
154
 
139
155
  ```py
140
156
  # Base (unitless)
141
- utils.HumanizedDecimal(950) # '950'
142
- utils.HumanizedDecimal(1500) # '1.50 k'
157
+ base.HumanizedDecimal(950) # '950'
158
+ base.HumanizedDecimal(1500) # '1.500 k'
143
159
 
144
160
  # With a unit (trimmed and attached)
145
- utils.HumanizedDecimal(1500, ' Hz ') # '1.50 kHz'
146
- utils.HumanizedDecimal(0.123456, 'V') # '0.1235 V'
161
+ base.HumanizedDecimal(1500, unit=' Hz ') # '1.500 kHz'
162
+ base.HumanizedDecimal(0.123456, unit='V') # '123.456 mV'
147
163
 
148
164
  # Large magnitudes
149
- utils.HumanizedDecimal(3_200_000) # '3.20 M'
150
- utils.HumanizedDecimal(7.2e12, 'B/s') # '7.20 TB/s'
165
+ base.HumanizedDecimal(3_200_000) # '3.200 M'
166
+ base.HumanizedDecimal(7.2e12, unit='B/s') # '7.200 TB/s'
151
167
  ```
152
168
 
153
- Scales by powers of 1000 using SI prefixes (`k`, `M`, `G`, `T`, `P`, `E`). For values `<1000`, integers are shown as-is; small floats show four decimals. For scaled values, two decimals are used and the unit (if provided) is attached without a space (e.g., `kHz`).
169
+ Scales by powers of 1000 using SI prefixes to keep the displayed value in roughly `[1, 1000)` when possible.
170
+
171
+ - Supported large prefixes: `k`, `M`, `G`, `T`, `P`, `E`
172
+ - Supported small prefixes: `m`, `µ`, `n`, `p`, `f`, `a`
173
+ - Formatting uses 3 decimals for non-integer/unscaled values and for scaled values.
154
174
 
155
175
  - unit handling: `unit` is stripped; `<1000` values include a space before the unit (`'950 Hz'`)
156
- - errors: negative or non-finite inputs raise `InputError`
176
+ - errors: non-finite inputs raise `InputError` (negative values are supported and keep a leading `-`)
157
177
 
158
178
  #### Humanized Durations
159
179
 
160
180
  ```py
161
- utils.HumanizedSeconds(0) # '0.00 s'
162
- utils.HumanizedSeconds(0.000004) # '4.000 µs'
163
- utils.HumanizedSeconds(0.25) # '250.000 ms'
164
- utils.HumanizedSeconds(42) # '42.00 s'
165
- utils.HumanizedSeconds(3661) # '1.02 h'
166
- utils.HumanizedSeconds(172800) # '2.00 d'
181
+ base.HumanizedSeconds(0) # '0.000 s'
182
+ base.HumanizedSeconds(0.000004) # '4.000 µs'
183
+ base.HumanizedSeconds(0.25) # '250.000 ms'
184
+ base.HumanizedSeconds(42) # '42.000 s'
185
+ base.HumanizedSeconds(3661) # '1.017 h'
186
+ base.HumanizedSeconds(172800) # '2.000 d'
167
187
  ```
168
188
 
169
189
  Chooses an appropriate time unit based on magnitude and formats with fixed precision:
170
190
 
171
191
  - `< 1 ms`: microseconds with three decimals (`µs`)
172
192
  - `< 1 s`: milliseconds with three decimals (`ms`)
173
- - `< 60 s`: seconds with two decimals (`s`)
174
- - `< 60 min`: minutes with two decimals (`min`)
175
- - `< 24 h`: hours with two decimals (`h`)
176
- - `≥ 24 h`: days with two decimals (`d`)
177
- - special case: `0 → '0.00 s'`
193
+ - `< 60 s`: seconds with three decimals (`s`)
194
+ - `< 60 min`: minutes with three decimals (`min`)
195
+ - `< 24 h`: hours with three decimals (`h`)
196
+ - `≥ 24 h`: days with three decimals (`d`)
197
+ - special case: `0 → '0.000 s'`
178
198
  - errors: negative or non-finite inputs raise `InputError`
179
199
 
180
200
  #### Execution Timing
@@ -191,7 +211,7 @@ import time
191
211
  ```py
192
212
  with base.Timer('Block timing'):
193
213
  time.sleep(1.2)
194
- # → logs: "Block timing: 1.20 s" (default via logging.info)
214
+ # → logs: "Block timing: 1.200 s" (default via logging.info)
195
215
  ```
196
216
 
197
217
  Starts timing on entry, stops on exit, and reports elapsed time automatically.
@@ -204,7 +224,7 @@ def slow_function():
204
224
  time.sleep(0.8)
205
225
 
206
226
  slow_function()
207
- # → logs: "Function timing: 0.80 s"
227
+ # → logs: "Function timing: 0.800 s"
208
228
  ```
209
229
 
210
230
  Wraps a function so that each call is automatically timed.
@@ -215,17 +235,17 @@ Wraps a function so that each call is automatically timed.
215
235
  tm = base.Timer('Inline timing', emit_print=True)
216
236
  tm.Start()
217
237
  time.sleep(0.1)
218
- tm.Stop() # prints: "Inline timing: 0.10 s"
238
+ tm.Stop() # prints: "Inline timing: 0.100 s"
219
239
  ```
220
240
 
221
241
  Manual control over `Start()` and `Stop()` for precise measurement of custom intervals.
222
242
 
223
243
  ##### Key points
224
244
 
225
- - **Label**: required, shown in output; empty labels raise `InputError`
245
+ - **Label**: optional; if empty, output omits the label prefix
226
246
  - **Output**:
227
247
  - `emit_log=True` → `logging.info()` (default)
228
- - `emit_print=True` → direct `print()`
248
+ - `emit_print=True` → prints via `rich.console.Console().print()`
229
249
  - Both can be enabled
230
250
  - **Format**: elapsed time is shown using `HumanizedSeconds()`
231
251
  - **Safety**:
@@ -253,7 +273,7 @@ blob = base.Serialize(data)
253
273
  blob = base.Serialize(
254
274
  data,
255
275
  compress=9, # compression level (-22..22, default=3)
256
- key=my_symmetric_key # must implement SymmetricCrypto
276
+ key=my_encryptor # must implement `base.Encryptor` (e.g., `aes.AESKey`)
257
277
  )
258
278
 
259
279
  # Save directly to file
@@ -297,7 +317,7 @@ obj = base.DeSerialize(data=blob)
297
317
  obj = base.DeSerialize(file_path='/tmp/data.blob')
298
318
 
299
319
  # With decryption
300
- obj = base.DeSerialize(data=blob, key=my_symmetric_key)
320
+ obj = base.DeSerialize(data=blob, key=my_decryptor)
301
321
  ```
302
322
 
303
323
  Deserialization path:
@@ -313,7 +333,8 @@ data/file → (decrypt) → (decompress if Zstd) → unpickle
313
333
 
314
334
  - Exactly one of `data` or `file_path` must be provided.
315
335
  - `file_path` must exist; `data` must be at least 4 bytes.
316
- - Wrong key or corrupted data can raise `CryptoError`.
336
+ - Wrong key / authentication failure can raise `CryptoError`.
337
+ - Corrupted compressed blobs typically raise `zstandard.ZstdError` during decompression.
317
338
 
318
339
  #### Cryptographically Secure Randomness
319
340
 
@@ -388,8 +409,8 @@ The function is `O(log(min(a, b)))` and handles arbitrarily large integers. To f
388
409
 
389
410
  ```py
390
411
  >>> base.ExtendedGCD(462, 1071)
391
- (21, -2, 1)
392
- >>> 462 * -2 + 1071 * 1
412
+ (21, 7, -3)
413
+ >>> 462 * 7 + 1071 * (-3)
393
414
  21
394
415
  ```
395
416
 
@@ -535,13 +556,13 @@ Hashes a file from disk in streaming mode. By default uses SHA-256; `digest='sha
535
556
 
536
557
  #### Symmetric Encryption Interface
537
558
 
538
- `SymmetricCrypto` is an abstract base class that defines the **byte-in / byte-out** contract for symmetric ciphers.
559
+ `base.Encryptor` and `base.Decryptor` are runtime-checkable protocols that define the **byte-in / byte-out** contract for symmetric ciphers.
539
560
 
540
561
  - **Metadata handling** — if the algorithm uses a `nonce` or `tag`, the implementation must handle it internally (e.g., append it to ciphertext).
541
562
  - **AEAD modes** — if supported, `associated_data` must be authenticated; otherwise, a non-`None` value should raise `InputError`.
542
563
 
543
564
  ```py
544
- class MyAES(base.SymmetricCrypto):
565
+ class MyAES(base.Encryptor, base.Decryptor):
545
566
  def Encrypt(self, plaintext: bytes, *, associated_data=None) -> bytes:
546
567
  ...
547
568
  def Decrypt(self, ciphertext: bytes, *, associated_data=None) -> bytes:
@@ -555,8 +576,8 @@ Cryptographic objects all derive from the `CryptoKey` class and will all have so
555
576
  - Will be safe to log and print, i.e., implement safe `__str__()` and `__repr__()` methods (in actuality `repr` will be exactly the same as `str`). The `__str__()` should always fully print the public parts of the object and obfuscate the private ones. This obfuscation allows for some debugging, if needed, but if the secrets are "too short" then it can be defeated by brute force. For usual crypto defaults the obfuscation is fine. The obfuscation is the fist 4 bytes of the SHA-512 for the value followed by an ellipsis (e.g. `c9626f16…`).
556
577
  - It will have a `_DebugDump()` method that **does print secrets** and can be used for **debugging only**.
557
578
  - Can be easily serialized to `bytes` by the `blob` property and to base-64 encoded `str` by the `encoded` property.
558
- - Can be serialized encrypted to `bytes` by the `Blob(key=[SymmetricCrypto])` method and to encrypted base-64 encoded `str` by the `Encoded(key=[SymmetricCrypto])` method.
559
- - Can be instantiated back as an object from `str` or `bytes` using the `Load(data, key=[SymmetricCrypto] | None)` method. The `Load()` will decide how to build the object and will work universally with all the serialization options discussed above.
579
+ - Can be serialized encrypted to `bytes` by the `Blob(key=[base.Encryptor])` method and to encrypted base-64 encoded `str` by the `Encoded(key=[base.Encryptor])` method.
580
+ - Can be instantiated back as an object from `str` or `bytes` using the `Load(data, key=[base.Decryptor] | None)` method. The `Load()` will decide how to build the object and will work universally with all the serialization options discussed above.
560
581
 
561
582
  Example:
562
583
 
@@ -719,7 +740,7 @@ For real-world deployments, use a high-level library with authenticated encrypti
719
740
  from transcrypto import elgamal
720
741
 
721
742
  # Shared parameters (prime modulus, group base) for a group
722
- shared = elgamal.ElGamalSharedPublicKey.New(256)
743
+ shared = elgamal.ElGamalSharedPublicKey.NewShared(256)
723
744
  print(shared.prime_modulus)
724
745
  print(shared.group_base)
725
746
 
@@ -771,8 +792,8 @@ This is **raw DSA** over a prime field — **no hashing or padding**. You sign/v
771
792
  ```py
772
793
  from transcrypto import dsa
773
794
 
774
- # Shared parameters (p, q, g)
775
- shared = dsa.DSASharedPublicKey.New(p_bits=1024, q_bits=160)
795
+ # Shared parameters (p, q, g) - Safe Sign/Verify requires q > 512 bits
796
+ shared = dsa.DSASharedPublicKey.NewShared(2048, 520)
776
797
  print(shared.prime_modulus) # p
777
798
  print(shared.prime_seed) # q (q | p-1)
778
799
  print(shared.group_base) # g
@@ -808,11 +829,11 @@ assert pub.RawVerify(msg, sig)
808
829
 
809
830
  ```py
810
831
  # Generate primes (p, q) with q | (p-1); also returns m = (p-1)//q
811
- p, q, m = dsa.NBitRandomDSAPrimes(p_bits=1024, q_bits=160)
832
+ p, q, m = dsa.NBitRandomDSAPrimes(1024, 160)
812
833
  assert (p - 1) % q == 0
813
834
  ```
814
835
 
815
- Used internally by `DSASharedPublicKey.New()`.
836
+ Used internally by `DSASharedPublicKey.NewShared()`.
816
837
  Search breadth and retry caps are bounded; repeated failures raise `CryptoError`.
817
838
 
818
839
  #### Public Bidding
@@ -837,14 +858,14 @@ print(bid_pub == bid_pub_expected)
837
858
 
838
859
  <https://en.wikipedia.org/wiki/Shamir's_secret_sharing>
839
860
 
840
- This is the information-theoretic SSS but with no authentication or binding between share and secret. Malicious share injection is possible! Add MAC or digital signature in hostile settings. Use at least 128-bit modulus for non-toy deployments.
861
+ This is the information-theoretic SSS but with no authentication or binding between share and secret. Malicious share injection is possible! Add MAC or digital signature in hostile settings. Use at least 128-bit modulus for non-toy deployments; `MakeDataShares()` requires > 256 bits.
841
862
 
842
863
  ```py
843
864
  from transcrypto import sss
844
865
 
845
866
  # Generate parameters: at least 3 of 5 shares needed,
846
- # coefficients & modulus are 128-bit primes
847
- priv = sss.ShamirSharedSecretPrivate.New(minimum_shares=3, bit_length=128)
867
+ # coefficients & modulus are 264-bit primes (> 256 bits required for MakeDataShares)
868
+ priv = sss.ShamirSharedSecretPrivate.New(3, 264)
848
869
  pub = sss.ShamirSharedSecretPublic.Copy(priv) # what you publish
849
870
 
850
871
  print(f'threshold : {pub.minimum}')
@@ -874,8 +895,8 @@ A single share object looks like `sss.ShamirSharePrivate(minimum=3, modulus=...,
874
895
  # Safe Re-constructing the secret
875
896
  secret = b'xyz'
876
897
  five_shares = priv.MakeDataShares(secret, 5)
877
- subset = five_shares[:3] # any 3 distinct shares
878
- recovered = subset[0].RecoverData(subset) # each share has the encrypted data, so you ask it to join with the others
898
+ subset = five_shares[:3] # any 3 distinct shares
899
+ recovered = subset[0].RecoverData(subset[1:]) # each share has the encrypted data, pass other shares
879
900
  assert recovered == secret
880
901
 
881
902
  # Raw Re-constructing the secret
@@ -913,7 +934,7 @@ print(priv.RawVerifyShare(secret, tampered)) # ▶ False
913
934
 
914
935
  ### Setup
915
936
 
916
- If you want to develop for this project, first install python 3.13 and [Poetry](https://python-poetry.org/docs/cli/), but to get the versions you will need, we suggest you do it like this (*Linux*):
937
+ If you want to develop for this project, first install python 3.12 and [Poetry](https://python-poetry.org/docs/cli/), but to get the versions you will need, we suggest you do it like this (*Linux*):
917
938
 
918
939
  ```sh
919
940
  sudo apt-get update
@@ -922,10 +943,10 @@ sudo apt-get install git python3 python3-pip pipx python3-dev python3-venv build
922
943
 
923
944
  sudo add-apt-repository ppa:deadsnakes/ppa # install arbitrary python version
924
945
  sudo apt-get update
925
- sudo apt-get install python3.13
946
+ sudo apt-get install python3.12
926
947
 
927
948
  sudo apt-get remove python3-poetry
928
- python3.13 -m pipx ensurepath
949
+ python3.12 -m pipx ensurepath
929
950
  # re-open terminal
930
951
  pipx install poetry
931
952
  poetry --version # should be >=2.1
@@ -941,11 +962,11 @@ brew update
941
962
  brew upgrade
942
963
  brew cleanup -s
943
964
 
944
- brew install git python@3.13 # install arbitrary python version
965
+ brew install git python@3.12 # install arbitrary python version
945
966
 
946
967
  brew uninstall poetry
947
- python3.13 -m pip install --user pipx
948
- python3.13 -m pipx ensurepath
968
+ python3.12 -m pip install --user pipx
969
+ python3.12 -m pipx ensurepath
949
970
  # re-open terminal
950
971
  pipx install poetry
951
972
  poetry --version # should be >=2.1
@@ -960,7 +981,7 @@ Now install the project:
960
981
  git clone https://github.com/balparda/transcrypto.git transcrypto
961
982
  cd transcrypto
962
983
 
963
- poetry env use python3.13 # creates the venv
984
+ poetry env use python3.12 # creates the venv
964
985
  poetry sync # sync env to project's poetry.lock file
965
986
  poetry env info # no-op: just to check
966
987
 
@@ -998,7 +1019,7 @@ If you manually added a dependency to `pyproject.toml` you should ***very carefu
998
1019
 
999
1020
  ```sh
1000
1021
  rm -rf .venv .poetry poetry.lock
1001
- poetry env use python3.13
1022
+ poetry env use python3.12
1002
1023
  poetry install
1003
1024
  ```
1004
1025
 
@@ -1035,7 +1056,7 @@ poetry publish
1035
1056
  If you changed the CLI interface at all, in any tool, run:
1036
1057
 
1037
1058
  ```sh
1038
- ./tools/generate_docs.sh
1059
+ make docs
1039
1060
  ```
1040
1061
 
1041
1062
  You can find the 10 top slowest tests by running:
@@ -1069,3 +1090,4 @@ $ deactivate
1069
1090
  ```
1070
1091
 
1071
1092
  Hint: 85%+ is inside `MillerRabinIsPrime()`/`gmpy2.powmod()`...
1093
+