├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── nanoid ├── __init__.py ├── algorithm.py ├── generate.py ├── method.py ├── non_secure_generate.py └── resources.py ├── requirements.txt ├── setup.py └── test ├── __init__.py ├── algorithm_test.py ├── generate_test.py ├── method_test.py ├── nanoid_test.py ├── non_secure_test.py └── url_test.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Python CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-python/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers` 11 | - image: circleci/python:3.6.1 12 | 13 | # Specify service dependencies here if necessary 14 | # CircleCI maintains a library of pre-built images 15 | # documented at https://circleci.com/docs/2.0/circleci-images/ 16 | # - image: circleci/postgres:9.4 17 | 18 | working_directory: ~/repo 19 | 20 | steps: 21 | - checkout 22 | 23 | # Download and cache dependencies 24 | - restore_cache: 25 | keys: 26 | - v1-dependencies-{{ checksum "requirements.txt" }} 27 | # fallback to using the latest cache if no exact match is found 28 | - v1-dependencies- 29 | 30 | - run: 31 | name: install dependencies 32 | command: | 33 | python3 -m venv venv 34 | . venv/bin/activate 35 | pip install -r requirements.txt 36 | 37 | - save_cache: 38 | paths: 39 | - ./venv 40 | key: v1-dependencies-{{ checksum "requirements.txt" }} 41 | 42 | # run tests! 43 | # this example uses Django's built-in test-runner 44 | # other common Python testing frameworks include pytest and nose 45 | # https://pytest.org 46 | # https://nose.readthedocs.io 47 | - run: 48 | name: run tests 49 | command: | 50 | . venv/bin/activate 51 | py.test test 52 | flake8 --max-line-length=99 nanoid test setup.py 53 | 54 | - store_artifacts: 55 | path: test-reports 56 | destination: test-reports 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/linux,python,windows,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=linux,python,windows,visualstudiocode 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### Python ### 21 | # Byte-compiled / optimized / DLL files 22 | __pycache__/ 23 | *.py[cod] 24 | *$py.class 25 | 26 | # C extensions 27 | *.so 28 | 29 | # Distribution / packaging 30 | .Python 31 | build/ 32 | develop-eggs/ 33 | dist/ 34 | downloads/ 35 | eggs/ 36 | .eggs/ 37 | lib/ 38 | lib64/ 39 | parts/ 40 | sdist/ 41 | var/ 42 | wheels/ 43 | *.egg-info/ 44 | .installed.cfg 45 | *.egg 46 | MANIFEST 47 | 48 | # PyInstaller 49 | # Usually these files are written by a python script from a template 50 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 51 | *.manifest 52 | *.spec 53 | 54 | # Installer logs 55 | pip-log.txt 56 | pip-delete-this-directory.txt 57 | 58 | # Unit test / coverage reports 59 | htmlcov/ 60 | .tox/ 61 | .nox/ 62 | .coverage 63 | .coverage.* 64 | .cache 65 | nosetests.xml 66 | coverage.xml 67 | *.cover 68 | .hypothesis/ 69 | .pytest_cache/ 70 | 71 | # Translations 72 | *.mo 73 | *.pot 74 | 75 | # Django stuff: 76 | *.log 77 | local_settings.py 78 | db.sqlite3 79 | 80 | # Flask stuff: 81 | instance/ 82 | .webassets-cache 83 | 84 | # Scrapy stuff: 85 | .scrapy 86 | 87 | # Sphinx documentation 88 | docs/_build/ 89 | 90 | # PyBuilder 91 | target/ 92 | 93 | # Jupyter Notebook 94 | .ipynb_checkpoints 95 | 96 | # IPython 97 | profile_default/ 98 | ipython_config.py 99 | 100 | # pyenv 101 | .python-version 102 | 103 | # celery beat schedule file 104 | celerybeat-schedule 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | 136 | ### Python Patch ### 137 | .venv/ 138 | 139 | ### Python.VirtualEnv Stack ### 140 | # Virtualenv 141 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 142 | [Bb]in 143 | [Ii]nclude 144 | [Ll]ib 145 | [Ll]ib64 146 | [Ll]ocal 147 | [Ss]cripts 148 | pyvenv.cfg 149 | pip-selfcheck.json 150 | 151 | ### VisualStudioCode ### 152 | .vscode/* 153 | !.vscode/settings.json 154 | !.vscode/tasks.json 155 | !.vscode/launch.json 156 | !.vscode/extensions.json 157 | 158 | ### VisualStudioCode Patch ### 159 | # Ignore all local history of files 160 | .history 161 | 162 | ### Windows ### 163 | # Windows thumbnail cache files 164 | Thumbs.db 165 | ehthumbs.db 166 | ehthumbs_vista.db 167 | 168 | # Dump file 169 | *.stackdump 170 | 171 | # Folder config file 172 | [Dd]esktop.ini 173 | 174 | # Recycle Bin used on file shares 175 | $RECYCLE.BIN/ 176 | 177 | # Windows Installer files 178 | *.cab 179 | *.msi 180 | *.msix 181 | *.msm 182 | *.msp 183 | 184 | # Windows shortcuts 185 | *.lnk 186 | 187 | # End of https://www.gitignore.io/api/linux,python,windows,visualstudiocode 188 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | 4 | Copyright (c) 2018 Paul Yuan 5 | Copyright (c) 2018 Dair Aidarkhanov 6 | Copyright (c) 2018 Andrey Sitnik 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README.md 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nano ID 2 | 3 | [![CircleCI](https://circleci.com/gh/puyuan/py-nanoid/tree/master.svg?style=svg)](https://circleci.com/gh/puyuan/py-nanoid/tree/master) 4 | 5 | A tiny, secure, URL-friendly, unique string ID generator for Python. 6 | 7 | * __Safe__. It uses cryptographically strong random APIs and tests distribution of symbols. 8 | * __Compact__. It uses a larger alphabet than UUID (A-Za-z0-9_-). So ID size was reduced from 36 to 21 symbols. 9 | 10 | 11 | ## Installation 12 | 13 | ```sh 14 | pip install nanoid 15 | ``` 16 | 17 | 18 | ## Usage 19 | 20 | ### Normal 21 | 22 | The main module uses URL-friendly symbols (A-Za-z0-9_-) and returns an ID with 21 characters (to have a collision probability similar to UUID v4). 23 | 24 | ```python 25 | from nanoid import generate 26 | 27 | generate() # => NDzkGoTCdRcaRyt7GOepg 28 | ``` 29 | 30 | Symbols `-,.()` are not encoded in the URL. If used at the end of a link they could be identified as a punctuation symbol. 31 | 32 | If you want to reduce ID length (and increase collisions probability), you can pass the length as an argument. 33 | 34 | ```python 35 | from nanoid import generate 36 | 37 | generate(size=10) # => "IRFa-VaY2b" 38 | ``` 39 | 40 | Don’t forget to check the safety of your ID length in ID [collision probability calculator](https://zelark.github.io/nano-id-cc/). 41 | 42 | 43 | ## Custom Alphabet or Length 44 | 45 | If you want to change the ID's alphabet or length you can use the internal generate module. 46 | 47 | ```python 48 | from nanoid import generate 49 | 50 | generate('1234567890abcdef', 10) # => "4f9zd13a42" 51 | ``` 52 | 53 | Non-secure API is also available: 54 | 55 | ```python 56 | from nanoid import non_secure_generate 57 | 58 | non_secure_generate('1234567890abcdef', 10) 59 | ``` 60 | 61 | 62 | ## Tools 63 | 64 | * [ID size calculator](https://zelark.github.io/nano-id-cc/) to choice smaller ID size depends on your case. 65 | nanoid-dictionary with popular alphabets to use with nanoid/generate. 66 | * [`nanoid-dictionary`](https://pypi.org/project/nanoid-dictionary/) with popular alphabets to use. 67 | 68 | 69 | ## Other Programming Languages 70 | 71 | * [C#](https://github.com/codeyu/nanoid-net) 72 | * [Clojure and ClojureScript](https://github.com/zelark/nano-id) 73 | * [Crystal](https://github.com/mamantoha/nanoid.cr) 74 | * [Dart](https://github.com/pd4d10/nanoid) 75 | * [Go](https://github.com/matoous/go-nanoid) 76 | * [Elixir](https://github.com/railsmechanic/nanoid) 77 | * [Haskell](https://github.com/4e6/nanoid-hs) 78 | * [Java](https://github.com/aventrix/jnanoid) 79 | * [JavaScript](https://github.com/ai/nanoid) 80 | * [Nim](https://github.com/icyphox/nanoid.nim) 81 | * [PHP](https://github.com/hidehalo/nanoid-php) 82 | * [Ruby](https://github.com/radeno/nanoid.rb) 83 | * [Rust](https://github.com/nikolay-govorov/nanoid) 84 | * [Swift](https://github.com/antiflasher/NanoID) 85 | 86 | 87 | ## Changelog 88 | - v2.0.0 89 | - Replace ~ to - in default alphabet. 90 | - Add non-secure fast generator. 91 | - Reduce default characters from 22 to 21. 92 | - v0.3.0 93 | - Fix array out of bound error. 94 | 95 | 96 | ## Credits 97 | 98 | - Andrey Sitnik for [Nano ID](https://github.com/ai/nanoid). 99 | - [Dair Aidarkhanov](https://github.com/aidarkhanov) for main contribution to v2.0, and adding test cases. 100 | - Aleksandr Zhuravlev for [ID collision probability](https://zelark.github.io/nano-id-cc/). 101 | -------------------------------------------------------------------------------- /nanoid/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from nanoid.generate import generate 4 | from nanoid.non_secure_generate import non_secure_generate 5 | 6 | __all__ = ['generate', 'non_secure_generate'] 7 | -------------------------------------------------------------------------------- /nanoid/algorithm.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from __future__ import division 3 | 4 | from os import urandom 5 | 6 | 7 | def algorithm_generate(random_bytes): 8 | return bytearray(urandom(random_bytes)) 9 | -------------------------------------------------------------------------------- /nanoid/generate.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | from __future__ import division 4 | 5 | from nanoid.algorithm import algorithm_generate 6 | from nanoid.method import method 7 | from nanoid.resources import alphabet, size 8 | 9 | 10 | def generate(alphabet=alphabet, size=size): 11 | return method(algorithm_generate, alphabet, size) 12 | -------------------------------------------------------------------------------- /nanoid/method.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from __future__ import division 3 | 4 | from math import ceil, log 5 | 6 | 7 | def method(algorithm, alphabet, size): 8 | alphabet_len = len(alphabet) 9 | 10 | mask = 1 11 | if alphabet_len > 1: 12 | mask = (2 << int(log(alphabet_len - 1) / log(2))) - 1 13 | step = int(ceil(1.6 * mask * size / alphabet_len)) 14 | 15 | id = '' 16 | while True: 17 | random_bytes = algorithm(step) 18 | 19 | for i in range(step): 20 | random_byte = random_bytes[i] & mask 21 | if random_byte < alphabet_len: 22 | if alphabet[random_byte]: 23 | id += alphabet[random_byte] 24 | 25 | if len(id) == size: 26 | return id 27 | -------------------------------------------------------------------------------- /nanoid/non_secure_generate.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | from __future__ import division 4 | 5 | from random import random 6 | 7 | from nanoid.resources import alphabet, size 8 | 9 | 10 | def non_secure_generate(alphabet=alphabet, size=size): 11 | alphabet_len = len(alphabet) 12 | 13 | id = '' 14 | for _ in range(size): 15 | id += alphabet[int(random() * alphabet_len) | 0] 16 | return id 17 | 18 | 19 | if __name__ == '__main__': 20 | print(non_secure_generate()) 21 | -------------------------------------------------------------------------------- /nanoid/resources.py: -------------------------------------------------------------------------------- 1 | alphabet = '_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' 2 | size = 21 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==3.10.0 2 | flake8==3.6.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open("README.md", "r") as f: 4 | long_description = f.read() 5 | 6 | setup( 7 | name='nanoid', 8 | version='2.0.0', 9 | author='Paul Yuan', 10 | author_email='puyuan1@gmail.com', 11 | description='A tiny, secure, URL-friendly, unique string ID generator for Python', 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url='https://github.com/puyuan/py-nanoid', 15 | license='MIT', 16 | packages=['nanoid'], 17 | classifiers=[ 18 | 'License :: OSI Approved :: MIT License', 19 | 'Programming Language :: Python', 20 | 'Operating System :: OS Independent', 21 | 'Topic :: Utilities' 22 | ], 23 | zip_safe=False 24 | ) 25 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puyuan/py-nanoid/99e5b478c450f42d713b6111175886dccf16f156/test/__init__.py -------------------------------------------------------------------------------- /test/algorithm_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from nanoid.algorithm import algorithm_generate 4 | 5 | 6 | class TestAlgorithm(TestCase): 7 | def test_generates_random_buffers(self): 8 | numbers = {} 9 | random_bytes = algorithm_generate(10000) 10 | self.assertEqual(len(random_bytes), 10000) 11 | for i in range(len(random_bytes)): 12 | if not numbers.get(random_bytes[i]): 13 | numbers[random_bytes[i]] = 0 14 | numbers[random_bytes[i]] += 1 15 | self.assertEqual(type(random_bytes[i]), int) 16 | self.assertLessEqual(random_bytes[i], 255) 17 | self.assertGreaterEqual(random_bytes[i], 0) 18 | 19 | def test_generates_small_random_buffers(self): 20 | self.assertEqual(len(algorithm_generate(10)), 10) 21 | -------------------------------------------------------------------------------- /test/generate_test.py: -------------------------------------------------------------------------------- 1 | from sys import maxsize 2 | from unittest import TestCase 3 | 4 | from nanoid import generate 5 | 6 | 7 | class TestGenerate(TestCase): 8 | def test_has_flat_distribution(self): 9 | count = 100 * 1000 10 | length = 5 11 | alphabet = 'abcdefghijklmnopqrstuvwxyz' 12 | 13 | chars = {} 14 | for _ in range(count): 15 | id = generate(alphabet, length) 16 | for j in range(len(id)): 17 | char = id[j] 18 | if not chars.get(char): 19 | chars[char] = 0 20 | chars[char] += 1 21 | 22 | self.assertEqual(len(chars.keys()), len(alphabet)) 23 | 24 | max = 0 25 | min = maxsize 26 | for k in chars: 27 | distribution = (chars[k] * len(alphabet)) / float((count * length)) 28 | if distribution > max: 29 | max = distribution 30 | if distribution < min: 31 | min = distribution 32 | self.assertLessEqual(max - min, 0.05) 33 | 34 | def test_has_no_collisions(self): 35 | count = 100 * 1000 36 | used = {} 37 | for _ in range(count): 38 | id = generate() 39 | self.assertIsNotNone(id in used) 40 | used[id] = True 41 | 42 | def test_has_options(self): 43 | count = 100 * 1000 44 | for _ in range(count): 45 | self.assertEqual(generate('a', 5), 'aaaaa') 46 | self.assertEqual(len(generate(alphabet="12345a", size=3)), 3) 47 | -------------------------------------------------------------------------------- /test/method_test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from unittest import TestCase 4 | 5 | from nanoid.method import method 6 | from nanoid.resources import size 7 | 8 | 9 | class TestMethod(TestCase): 10 | def test_generates_random_string(self): 11 | sequence = [2, 255, 3, 7, 7, 7, 7, 7, 0, 1] 12 | 13 | def rand(size=size): 14 | random_bytes = [] 15 | for i in range(0, size, len(sequence)): 16 | random_bytes += sequence[0:size-i] 17 | return random_bytes 18 | self.assertEqual(method(rand, 'abcde', 4), 'cdac') 19 | -------------------------------------------------------------------------------- /test/nanoid_test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from sys import maxsize 4 | from unittest import TestCase 5 | 6 | from nanoid import generate, non_secure_generate 7 | from nanoid.resources import alphabet 8 | 9 | 10 | class TestNanoID(TestCase): 11 | def test_flat_distribution(self): 12 | count = 100 * 1000 13 | length = 5 14 | alphabet = 'abcdefghijklmnopqrstuvwxyz' 15 | 16 | chars = {} 17 | for _ in range(count): 18 | id = generate(alphabet, length) 19 | for j in range(len(id)): 20 | char = id[j] 21 | if not chars.get(char): 22 | chars[char] = 0 23 | chars[char] += 1 24 | 25 | self.assertEqual(len(chars.keys()), len(alphabet)) 26 | 27 | max = 0 28 | min = maxsize 29 | for k in chars: 30 | distribution = (chars[k] * len(alphabet)) / float((count * length)) 31 | if distribution > max: 32 | max = distribution 33 | if distribution < min: 34 | min = distribution 35 | self.assertLessEqual(max - min, 0.05) 36 | 37 | def test_generates_url_friendly_id(self): 38 | for _ in range(10): 39 | id = generate() 40 | self.assertEqual(len(id), 21) 41 | for j in range(len(id)): 42 | self.assertIn(id[j], alphabet) 43 | 44 | def test_has_no_collisions(self): 45 | count = 100 * 1000 46 | used = {} 47 | for _ in range(count): 48 | id = generate() 49 | self.assertIsNotNone(id in used) 50 | used[id] = True 51 | 52 | def test_has_options(self): 53 | self.assertEqual(generate('a', 5), 'aaaaa') 54 | 55 | def test_non_secure_ids(self): 56 | for i in range(10000): 57 | nanoid = non_secure_generate() 58 | self.assertEqual(len(nanoid), 21) 59 | 60 | def test_non_secure_short_ids(self): 61 | for i in range(10000): 62 | nanoid = non_secure_generate("12345a", 3) 63 | self.assertEqual(len(nanoid), 3) 64 | 65 | def test_short_secure_ids(self): 66 | for i in range(10000): 67 | nanoid = generate("12345a", 3) 68 | self.assertEqual(len(nanoid), 3) 69 | -------------------------------------------------------------------------------- /test/non_secure_test.py: -------------------------------------------------------------------------------- 1 | from sys import maxsize 2 | from unittest import TestCase 3 | 4 | from nanoid import non_secure_generate 5 | from nanoid.resources import alphabet 6 | 7 | 8 | class TestNonSecure(TestCase): 9 | def test_changes_id_length(self): 10 | self.assertEqual(len(non_secure_generate(size=10)), 10) 11 | 12 | def test_generates_url_friendly_id(self): 13 | for _ in range(10): 14 | id = non_secure_generate() 15 | self.assertEqual(len(id), 21) 16 | for j in range(len(id)): 17 | self.assertNotEqual(alphabet.find(id[j]), -1) 18 | 19 | def test_has_flat_distribution(self): 20 | count = 100 * 1000 21 | length = len(non_secure_generate()) 22 | 23 | chars = {} 24 | for _ in range(count): 25 | id = non_secure_generate() 26 | for j in range(len(id)): 27 | char = id[j] 28 | if not chars.get(char): 29 | chars[char] = 0 30 | chars[char] += 1 31 | 32 | self.assertEqual(len(chars.keys()), len(alphabet)) 33 | 34 | max = 0 35 | min = maxsize 36 | for k in chars: 37 | distribution = (chars[k] * len(alphabet)) / float((count * length)) 38 | if distribution > max: 39 | max = distribution 40 | if distribution < min: 41 | min = distribution 42 | self.assertLessEqual(max - min, 0.05) 43 | 44 | def test_has_no_collisions(self): 45 | count = 100 * 1000 46 | used = {} 47 | for _ in range(count): 48 | id = non_secure_generate() 49 | self.assertIsNotNone(id in used) 50 | used[id] = True 51 | -------------------------------------------------------------------------------- /test/url_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from nanoid.resources import alphabet 4 | 5 | 6 | class TestURL(TestCase): 7 | def test_has_no_duplicates(self): 8 | for i in range(len(alphabet)): 9 | self.assertEqual(alphabet.rindex(alphabet[i]), i) 10 | 11 | def test_is_string(self): 12 | self.assertEqual(type(alphabet), str) 13 | --------------------------------------------------------------------------------