Skip to content

Commit e13339c

Browse files
committed
before library released
1 parent 98862b8 commit e13339c

File tree

7 files changed

+366
-0
lines changed

7 files changed

+366
-0
lines changed

.gitignore

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# C extensions
7+
*.so
8+
9+
# Distribution / packaging
10+
.Python
11+
env/
12+
build/
13+
develop-eggs/
14+
dist/
15+
downloads/
16+
eggs/
17+
.eggs/
18+
lib/
19+
lib64/
20+
parts/
21+
sdist/
22+
var/
23+
wheels/
24+
*.egg-info/
25+
.installed.cfg
26+
*.egg
27+
.tox/
28+
29+
# Installer logs
30+
pip-log.txt
31+
pip-delete-this-directory.txt
32+
33+
# Unit test / coverage reports
34+
htmlcov/
35+
.tox/
36+
.coverage
37+
.coverage.*
38+
.cache
39+
nosetests.xml
40+
coverage.xml
41+
*,cover
42+
.hypothesis/
43+
.pytest_cache/
44+
45+
# virtualenv
46+
venv/
47+
48+
# ignore
49+
.ignore/
50+
.DS_Store
51+
52+
optimus-primes/

.travis.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
language: python
2+
install:
3+
- pip install tox
4+
- pip install -e .
5+
matrix:
6+
include:
7+
- python: 3.8
8+
env:
9+
- TOX_ENV=py38
10+
- python: 3.9
11+
env:
12+
- TOX_ENV=py39
13+
script: tox -e $TOX_ENV

README.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# python-optimus
2+
This is based fully on [pjebs/optimus-go](https://github.com/pjebs/optimus-go) for Go which is based on [jenssegers/optimus](https://github.com/jenssegers/optimus) for PHP which is based on Knuth's Integer Hashing (Multiplicative Hashing) from his book [The Art Of Computer Programming, Vol. 3, 2nd Edition](https://archive.org/details/B-001-001-250/page/n535/mode/2up), Section 6.4, Page 516.
3+
4+
With this library, you can transform your internal id's to obfuscated integers based on Knuth's integer hash. It is similar to Hashids, but will generate integers instead of random strings. It is also super fast.
5+
6+
This library supports both 32 and 64 bits integers, although in Python you don't have that differentiation between int32 and int64, even bigint or bignum is the same since [PEP 237](https://www.python.org/dev/peps/pep-0237/). The reason you need a bitlength is that the algorithm itself works on a fixed bitlength. By default this library uses 64 bits.
7+
8+
## Python Support
9+
10+
So far it's only tested on Python 3.8 and Python 3.9
11+
12+
## Installation
13+
14+
pip install python-optimus
15+
16+
## Usage
17+
18+
Basic usage:
19+
20+
```
21+
from optimus_ids import Optimus
22+
my_optimus = Optimus(
23+
prime=<your prime number>
24+
)
25+
my_int_id = <some id you have>
26+
my_int_id_hashed = my_optimus.encode(my_int_id)
27+
assert my_int_id == my_optimus.decode(my_int_id_hashed)
28+
```
29+
30+
The caveat with the usage above is that every time you create your `Optimus` instance it will have a random component, even with using the same prime, so a proper usage should be like this:
31+
32+
```
33+
from optimus_ids import Optimus
34+
my_optimus = Optimus(
35+
prime=<your prime number>,
36+
random=<some random number>
37+
)
38+
my_int_id = <some id you have>
39+
my_int_id_hashed = my_optimus.encode(my_int_id)
40+
assert my_int_id == my_optimus.decode(my_int_id_hashed)
41+
42+
To generate a random number you could do this:
43+
```
44+
45+
```
46+
from optimus_ids import rand_n, MAX_64_INT # use 32 instead of 64 if you want to
47+
my_random_number = rand_n(MAX_64_INT - 1)
48+
```
49+
50+
You can also generate an `Optimus` intance and then keep its `prime`, `inverese` and `random` properties stored, so you can always configure a new instance with the same components, or even pickle it:
51+
52+
```
53+
from optimus_ids import generate, Optimus
54+
my_optimus = generate()
55+
56+
# store the following variables or pickle the my_optimus variable
57+
prime = my_optimus.prime
58+
inverse = my_optimus.inverse
59+
random = my_optimus.random
60+
bitlength = my_optimus.bitlength
61+
62+
# create a new instance with the same parameters or unpickle an instance
63+
my_other_optimus = Optimus(
64+
prime=prime,
65+
inverse=inverse,
66+
random=random,
67+
bitlength=bitlength,
68+
)
69+
assert my_optimus.encode(42) == my_other_optimus.encode(42)
70+
assert my_optimus.decode(my_other_optimus.encode(42)) == my_other_optimus.decode(my_optimus.encode(42))
71+
```
72+
73+
**NOTE** for the generate function to work, it needs data, the data is large, and not available with the package, the data should be downloaded from [here](https://github.com/pjebs/optimus-go-primes) and the path to it is passed to the `generate` function. By default it expects the data to be in a folder called `optimus-primes` in the current working directory.
74+
75+
```
76+
├── your-app.py
77+
├── ...
78+
└── optimus-primes
79+
   ├── p1.txt
80+
   ├── p2.txt
81+
   ├── ...
82+
   └── p50.txt
83+
```

optimus_ids/__init__.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import secrets
2+
3+
from typing import Union
4+
from pathlib import Path
5+
6+
MAX_64_INT = 2 ** 63 - 1
7+
MAX_32_INT = 2 ** 31 - 1
8+
9+
10+
class Optimus:
11+
prime: int
12+
inverse: int
13+
random: int
14+
max_int: int
15+
16+
def __init__(self,
17+
prime: int,
18+
inverse: int = None,
19+
random: int = None,
20+
bitlength: int = 64):
21+
if bitlength not in (32, 64):
22+
raise ValueError('bitlength can only be 32 or 64')
23+
self.max_int = bitlength == 32 and MAX_32_INT or MAX_64_INT
24+
self.prime = prime
25+
if inverse is None:
26+
inverse = mod_inverse(prime, self.max_int)
27+
self.inverse = inverse
28+
if random is None:
29+
random = rand_n(self.max_int - 1)
30+
self.random = random
31+
32+
def encode(self, n: int) -> int:
33+
return ((n * self.prime) & self.max_int) ^ self.random
34+
35+
def decode(self, n: int) -> int:
36+
return ((n ^ self.random) * self.inverse) & self.max_int
37+
38+
39+
def mod_inverse(n: int, p: int) -> int:
40+
return pow(n, -1, p + 1)
41+
42+
43+
def generate(
44+
path_to_primes: Union[str, Path] = 'optimus-primes',
45+
bitlength: int = 64
46+
) -> Optimus:
47+
if bitlength not in (32, 64):
48+
raise ValueError('bitlength can only be 32 or 64')
49+
if isinstance(path_to_primes, str):
50+
path_to_primes = Path(path_to_primes)
51+
if not (path_to_primes.exists() and path_to_primes.is_dir()):
52+
raise ValueError(
53+
f'{str(path_to_primes)} does not exist or is not a directory'
54+
)
55+
n = rand_n(50)
56+
input_file = str(path_to_primes.joinpath(f'p{n}.txt').absolute())
57+
line_number = rand_n(1_000_000)
58+
with open(input_file, 'r') as f:
59+
for i in range(line_number):
60+
f.readline()
61+
return Optimus(int(f.readline().strip()), bitlength=bitlength)
62+
63+
64+
def rand_n(n: int) -> int:
65+
return secrets.randbelow(n) + 1

setup.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env python
2+
# coding=utf-8
3+
4+
import os
5+
6+
from setuptools import setup, find_packages
7+
8+
with open("README.md", "r", encoding="utf-8") as fh:
9+
long_description = fh.read()
10+
11+
setup(
12+
name="python-optimus",
13+
version="1.0.0",
14+
author="Abdullah Diab",
15+
author_email="[email protected]",
16+
maintainer="Abdullah Diab",
17+
maintainer_email="[email protected]",
18+
description="Transform internal id's to "
19+
"obfuscated integers using Knuth's integer hash",
20+
long_description=long_description,
21+
long_description_content_type="text/markdown",
22+
url="https://github.com/mpcabd/python-optimus",
23+
packages=find_packages(),
24+
classifiers=[
25+
"Programming Language :: Python :: 3",
26+
"License :: OSI Approved :: MIT License",
27+
"Operating System :: OS Independent",
28+
],
29+
python_requires='>=3.8',
30+
platforms="ALL",
31+
license="MIT",
32+
keywords="hashing hashids optimus knuth",
33+
)

tests/test-optimus.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import itertools
2+
import unittest
3+
import secrets
4+
import pathlib
5+
6+
import optimus_ids
7+
8+
9+
class TestOptimus(unittest.TestCase):
10+
11+
def test_mod_inverse(self):
12+
self.assertEqual(
13+
optimus_ids.mod_inverse(309779747, optimus_ids.MAX_32_INT),
14+
49560203
15+
)
16+
17+
def test_validation(self):
18+
with self.assertRaises(ValueError):
19+
o = optimus_ids.Optimus(prime=5, bitlength=16)
20+
21+
with self.assertRaises(ValueError):
22+
o = optimus_ids.generate(bitlength=16)
23+
24+
with self.assertRaises(ValueError):
25+
optimus_ids.generate(path_to_primes='invalid-path')
26+
27+
def test_generation(self):
28+
path = pathlib.Path('optimus-primes')
29+
if not (path.exists() and path.is_dir()):
30+
self.skipTest('optimus-primes not available for generation')
31+
32+
o64 = optimus_ids.generate()
33+
o32 = optimus_ids.generate(bitlength=32)
34+
35+
self.assertEqual(o64.decode(o64.encode(42)), 42)
36+
self.assertEqual(o32.decode(o32.encode(42)), 42)
37+
38+
def test_encoding(self):
39+
optimus_instances = [
40+
optimus_ids.Optimus(
41+
prime=309779747,
42+
inverse=49560203,
43+
random=57733611,
44+
bitlength=32
45+
),
46+
optimus_ids.Optimus(
47+
prime=684934207,
48+
inverse=1505143743,
49+
random=846034763,
50+
bitlength=32
51+
),
52+
optimus_ids.Optimus(
53+
prime=743534599,
54+
inverse=1356791223,
55+
random=1336232185,
56+
bitlength=32
57+
),
58+
optimus_ids.Optimus(
59+
prime=54661037,
60+
inverse=1342843941,
61+
random=576322863,
62+
bitlength=32
63+
),
64+
optimus_ids.Optimus(
65+
prime=198194831,
66+
inverse=229517423,
67+
random=459462336,
68+
bitlength=32
69+
),
70+
optimus_ids.Optimus(
71+
prime=198194831,
72+
random=459462336,
73+
bitlength=32
74+
),
75+
]
76+
77+
for i in range(5):
78+
o = optimus_instances[i]
79+
c = 10
80+
h = 100
81+
y = list(itertools.chain(
82+
range(c),
83+
(secrets.randbelow(optimus_ids.MAX_32_INT - 2 * c) + c
84+
for j in range(h)),
85+
range(
86+
optimus_ids.MAX_32_INT,
87+
optimus_ids.MAX_32_INT - c - 1,
88+
-1
89+
)
90+
))
91+
for n in y:
92+
encoded = o.encode(n)
93+
decoded = o.decode(encoded)
94+
with self.subTest(i=i, n=n):
95+
self.assertEqual(decoded, n)
96+
97+
98+
if __name__ == '__main__':
99+
unittest.main()

tox.ini

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# tox (https://tox.readthedocs.io/) is a tool for running tests
2+
# in multiple virtualenvs. This configuration file will run the
3+
# test suite on all supported python versions. To use it, "pip install tox"
4+
# and then run "tox" from this directory.
5+
6+
[tox]
7+
envlist = py38,py39
8+
9+
[testenv]
10+
allowlist_externals =
11+
ln
12+
deps =
13+
pytest
14+
commands =
15+
python -m pytest
16+
commands_pre =
17+
- ln -s {toxinidir}/optimus-primes {toxworkdir}/optimus-primes
18+
19+
[pytest]
20+
testpaths = tests
21+
python_files = test*.py

0 commit comments

Comments
 (0)