├── .coveragerc ├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── perftest_ulid2.py ├── setup.cfg ├── setup.py ├── test_ulid2.py ├── tox.ini └── ulid2 ├── __init__.py ├── __init__.pyi └── py.typed /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | setup.py 4 | perftest_ulid2.py 5 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | Build: 11 | runs-on: ubuntu-20.04 12 | strategy: 13 | matrix: 14 | python-version: 15 | - '2.7' 16 | - '3.6' 17 | - '3.7' 18 | - '3.8' 19 | - '3.9' 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: 'Set up Python ${{ matrix.python-version }}' 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: '${{ matrix.python-version }}' 26 | - run: pip install pytest pytest-cov 27 | - run: py.test -vvv --cov . 28 | - run: python perftest_ulid2.py 29 | - uses: codecov/codecov-action@v2 30 | Lint: 31 | runs-on: ubuntu-20.04 32 | steps: 33 | - uses: actions/checkout@v2 34 | - uses: actions/setup-python@v2 35 | with: 36 | python-version: '3.10' 37 | - run: pip install flake8 isort mypy 38 | - run: flake8 ulid2 39 | - run: isort --check ulid2 40 | - run: mypy --strict ulid2 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *$py.class 2 | *,cover 3 | *.egg 4 | *.egg-info/ 5 | *.log 6 | *.manifest 7 | *.mo 8 | *.pot 9 | *.py[cod] 10 | *.so 11 | *.spec 12 | .cache 13 | .coverage 14 | .coverage.* 15 | .eggs/ 16 | .env 17 | .hypothesis/ 18 | .idea 19 | .installed.cfg 20 | .ipynb_checkpoints 21 | .Python 22 | .python-version 23 | .ropeproject 24 | .scrapy 25 | .spyderproject 26 | .tox/ 27 | .venv/ 28 | .webassets-cache 29 | __pycache__/ 30 | build/ 31 | celerybeat-schedule 32 | coverage.xml 33 | develop-eggs/ 34 | dist/ 35 | docs/_build/ 36 | downloads/ 37 | eggs/ 38 | env/ 39 | ENV/ 40 | htmlcov/ 41 | instance/ 42 | lib/ 43 | lib64/ 44 | local_settings.py 45 | nosetests.xml 46 | parts/ 47 | pip-delete-this-directory.txt 48 | pip-log.txt 49 | sdist/ 50 | target/ 51 | var/ 52 | venv/ 53 | wheels/ 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016-2022 Valohai 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include ulid2/py.typed 2 | include ulid2/__init__.pyi 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ulid2 2 | ===== 3 | 4 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) 5 | [![Test](https://github.com/valohai/ulid2/actions/workflows/test.yml/badge.svg)](https://github.com/valohai/ulid2/actions/workflows/test.yml) 6 | [![codecov](https://codecov.io/gh/valohai/ulid2/branch/master/graph/badge.svg)](https://codecov.io/gh/valohai/ulid2) 7 | 8 | [ULID (Universally Unique Lexicographically Sortable Identifier)][ulid] encoding and 9 | decoding for Python. 10 | 11 | * Python 2.7 and 3.x compatible 12 | * Supports binary ULIDs 13 | * Supports bidirectional conversion of base32 ULIDs 14 | * Supports expressing ULIDs as UUIDs 15 | 16 | Usage 17 | ----- 18 | 19 | ### Generating ULIDs 20 | 21 | * Use `ulid2.generate_binary_ulid()` to generate a raw binary ULID 22 | * Use `ulid2.generate_ulid_as_uuid()` to generate an ULID as an `uuid.UUID` 23 | * Use `ulid2.generate_ulid_as_base32()` to generate an ULID as ASCII 24 | 25 | These functions accept optional arguments: 26 | 27 | * `timestamp`: a `datetime.datetime` or integer UNIX timestamp to base the ULID on. 28 | * `monotonic`: boolean; whether to attempt to ensure ULIDs are monotonically increasing. Monotonic behavior is not guaranteed when used from multiple threads. 29 | 30 | ### Parsing ULIDs 31 | 32 | * Use `ulid2.get_ulid_time(ulid)` to get the time from an ULID (in any format) 33 | 34 | ### Converting ULIDs 35 | 36 | * Use `ulid2.ulid_to_base32(ulid)` to convert an ULID to its ASCII representation 37 | * Use `ulid2.ulid_to_uuid(ulid)` to convert an ULID to its UUID representation 38 | * Use `ulid2.ulid_to_binary(ulid)` to convert an ULID to its binary representation 39 | 40 | ### Base32 41 | 42 | * Use `ulid2.encode_ulid_base32(binary)` to convert 16 bytes to 26 ASCII characters 43 | * Use `ulid2.decode_ulid_base32(ascii)` to convert 26 ASCII characters to 16 bytes 44 | 45 | Django compatibility 46 | -------------------- 47 | 48 | As `ulid2` is capable of expressing ULIDs as Python UUIDs, it's 49 | directly compatible with Django's UUIDFields. For instance, to ULID-ify a model's 50 | primary key, simply 51 | 52 | ```python 53 | from django.db import models 54 | from ulid2 import generate_ulid_as_uuid 55 | 56 | class MyModel(models.Model): 57 | id = models.UUIDField(default=generate_ulid_as_uuid, primary_key=True) 58 | ``` 59 | 60 | and you're done! 61 | 62 | 63 | Why the 2 in the name? 64 | ---------------------- 65 | 66 | `ulid` is already taken by [mdipierro's implementation][mdi]. :) 67 | 68 | Prior Art 69 | --------- 70 | 71 | * [NUlid](https://github.com/RobThree/NUlid) (MIT License) 72 | * [oklog/ulid](https://github.com/oklog/ulid) 73 | 74 | [ulid]: https://github.com/alizain/ulid 75 | [mdi]: https://github.com/mdipierro/ulid 76 | -------------------------------------------------------------------------------- /perftest_ulid2.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import timeit 4 | 5 | import ulid2 6 | 7 | 8 | def perftest(): 9 | tmr = timeit.Timer(lambda: ulid2.generate_ulid_as_uuid()) 10 | n_iterations = 300000 11 | time_taken = tmr.timeit(n_iterations) 12 | return int(n_iterations / time_taken) 13 | 14 | 15 | def main(): 16 | results = [] 17 | for x in range(5): 18 | ops_per_sec = perftest() 19 | print(x + 1, " ... ", ops_per_sec) 20 | results.append(ops_per_sec) 21 | n_results = len(results) 22 | mean = sum(results) / n_results 23 | median = sorted(results)[n_results // 2] 24 | print("mean ops/sec ", mean) 25 | print("median ops/sec", median) 26 | 27 | 28 | if __name__ == '__main__': 29 | main() 30 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | max-line-length = 109 6 | max-complexity = 10 7 | 8 | [tool:pytest] 9 | norecursedirs = bower_components node_modules .git venv* .tox 10 | 11 | [isort] 12 | profile = black 13 | multi_line_output = 3 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name='ulid2', 7 | version='0.3.0', 8 | description='ULID encoding/decoding for Python', 9 | author='Aarni Koskela', 10 | author_email='akx@iki.fi', 11 | url='https://github.com/valohai/ulid2', 12 | packages=['ulid2'], 13 | license='MIT', 14 | include_package_data=True, 15 | ) 16 | -------------------------------------------------------------------------------- /test_ulid2.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import pytest 4 | 5 | from ulid2 import ( 6 | InvalidULID, 7 | decode_ulid_base32, 8 | generate_binary_ulid, 9 | generate_ulid_as_base32, 10 | generate_ulid_as_uuid, 11 | get_ulid_time, 12 | get_ulid_timestamp, 13 | ulid_to_base32, 14 | ulid_to_binary, 15 | ulid_to_uuid, 16 | ) 17 | 18 | 19 | @pytest.mark.parametrize('generator', [ 20 | generate_ulid_as_base32, 21 | generate_ulid_as_uuid, 22 | ]) 23 | def test_ulid_time_monotonic(generator): 24 | last = None 25 | for time in [ 26 | '2002-08-14 10:10:10', 27 | '2008-01-01 08:24:40', 28 | '2013-12-01 10:10:10', 29 | '2016-07-07 14:12:10', 30 | '2016-07-07 14:13:10', 31 | '2016-07-07 14:13:10', 32 | '2016-07-07 14:13:10', 33 | '2016-07-07 14:13:10', 34 | ]: 35 | dt = datetime.datetime.strptime(time, '%Y-%m-%d %H:%M:%S') 36 | ulid = generator(dt, monotonic=True) 37 | if last: 38 | assert ulid > last 39 | last = ulid 40 | 41 | 42 | def test_ulid_not_monotonic_if_flag_false(): 43 | some_unordered_epoch_ulids = [generate_ulid_as_base32(timestamp=0, monotonic=False) for _ in range(100)] 44 | assert sorted(some_unordered_epoch_ulids) != some_unordered_epoch_ulids 45 | 46 | 47 | def test_ulid_sanity(): 48 | # https://github.com/RobThree/NUlid/blob/master/NUlid.Tests/UlidTests.cs#L14 49 | assert generate_ulid_as_base32(1469918176.385).startswith('01ARYZ6S41') 50 | 51 | 52 | def test_ulid_base32_length(): 53 | assert len(generate_ulid_as_base32()) == 26 54 | 55 | 56 | def test_ulid_binary_length(): 57 | assert len(generate_binary_ulid()) == 128 / 8 58 | 59 | 60 | def test_get_time(): 61 | dt = datetime.datetime(2010, 1, 1, 15, 11, 13) 62 | ulid = generate_ulid_as_base32(dt) 63 | assert get_ulid_time(ulid) == dt 64 | 65 | 66 | def test_conversion_roundtrip(): 67 | ulid = generate_binary_ulid() 68 | encoded = ulid_to_base32(ulid) 69 | uuid = ulid_to_uuid(ulid) 70 | assert ulid_to_binary(uuid) == ulid_to_binary(ulid) 71 | assert ulid_to_binary(encoded) == ulid_to_binary(ulid) 72 | 73 | 74 | def test_invalid(): 75 | with pytest.raises(InvalidULID): # invalid length (low-level) 76 | decode_ulid_base32('What is this') 77 | with pytest.raises(InvalidULID): # invalid length (high-level) 78 | ulid_to_binary('What is this') 79 | with pytest.raises(InvalidULID): # invalid characters 80 | ulid_to_binary('6' + '~' * 25) 81 | with pytest.raises(InvalidULID): # invalid type 82 | ulid_to_binary(8.7) # type: ignore[arg-type] 83 | with pytest.raises(InvalidULID): # out of range 84 | ulid_to_binary('8' + '0' * 25) 85 | with pytest.raises(InvalidULID): # out of range 86 | ulid_to_binary('G' + '0' * 25) 87 | with pytest.raises(InvalidULID): # out of range 88 | ulid_to_binary('R' + '0' * 25) 89 | 90 | 91 | def test_parses_largest_possible_ulid(): 92 | assert int(get_ulid_timestamp('7ZZZZZZZZZZZZZZZZZZZZZZZZZ') * 1000) == 2 ** 48 - 1 93 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py3{6,7,8,9} 3 | 4 | [testenv] 5 | commands = py.test -v {posargs} 6 | deps = 7 | pytest 8 | -------------------------------------------------------------------------------- /ulid2/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import calendar 4 | import datetime 5 | import os 6 | import struct 7 | import sys 8 | import time 9 | import uuid 10 | 11 | __all__ = [ 12 | 'encode_ulid_base32', 13 | 'decode_ulid_base32', 14 | 'get_ulid_time', 15 | 'get_ulid_timestamp', 16 | 'generate_binary_ulid', 17 | 'generate_ulid_as_uuid', 18 | 'generate_ulid_as_base32', 19 | 'ulid_to_base32', 20 | 'ulid_to_uuid', 21 | 'ulid_to_binary', 22 | ] 23 | 24 | py3 = (sys.version_info[0] == 3) 25 | text_type = (str if py3 else unicode) # noqa: F821 26 | 27 | 28 | class InvalidULID(ValueError): 29 | pass 30 | 31 | 32 | if py3: 33 | _to_binary = bytes 34 | else: 35 | def _to_binary(byte_list): 36 | return bytes(b''.join(chr(b) for b in byte_list)) 37 | 38 | 39 | # Unrolled and optimized ULID Base32 encoding/decoding 40 | # implementations based on NUlid: 41 | # https://github.com/RobThree/NUlid/blob/5f2678b4d/NUlid/Ulid.cs#L159 42 | 43 | _decode_table = [ 44 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 45 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 46 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 47 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 48 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 49 | 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF, 50 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 51 | 0x0F, 0x10, 0x11, 0xFF, 0x12, 0x13, 0xFF, 0x14, 0x15, 0xFF, 52 | 0x16, 0x17, 0x18, 0x19, 0x1A, 0xFF, 0x1B, 0x1C, 0x1D, 0x1E, 53 | 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0A, 0x0B, 0x0C, 54 | 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0xFF, 0x12, 0x13, 0xFF, 0x14, 55 | 0x15, 0xFF, 0x16, 0x17, 0x18, 0x19, 0x1A, 0xFF, 0x1B, 0x1C, 56 | 0x1D, 0x1E, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 57 | ] 58 | _symbols = '0123456789ABCDEFGHJKMNPQRSTVWXYZ' 59 | 60 | 61 | def encode_ulid_base32(binary): 62 | """ 63 | Encode 16 binary bytes into a 26-character long base32 string. 64 | :param binary: Bytestring or list of bytes 65 | :return: ASCII string of 26 characters 66 | :rtype: str 67 | """ 68 | assert len(binary) == 16 69 | 70 | if not py3 and isinstance(binary, str): 71 | binary = [ord(b) for b in binary] 72 | 73 | symbols = _symbols 74 | return ''.join([ 75 | symbols[(binary[0] & 224) >> 5], 76 | symbols[binary[0] & 31], 77 | symbols[(binary[1] & 248) >> 3], 78 | symbols[((binary[1] & 7) << 2) | ((binary[2] & 192) >> 6)], 79 | symbols[(binary[2] & 62) >> 1], 80 | symbols[((binary[2] & 1) << 4) | ((binary[3] & 240) >> 4)], 81 | symbols[((binary[3] & 15) << 1) | ((binary[4] & 128) >> 7)], 82 | symbols[(binary[4] & 124) >> 2], 83 | symbols[((binary[4] & 3) << 3) | ((binary[5] & 224) >> 5)], 84 | symbols[binary[5] & 31], 85 | symbols[(binary[6] & 248) >> 3], 86 | symbols[((binary[6] & 7) << 2) | ((binary[7] & 192) >> 6)], 87 | symbols[(binary[7] & 62) >> 1], 88 | symbols[((binary[7] & 1) << 4) | ((binary[8] & 240) >> 4)], 89 | symbols[((binary[8] & 15) << 1) | ((binary[9] & 128) >> 7)], 90 | symbols[(binary[9] & 124) >> 2], 91 | symbols[((binary[9] & 3) << 3) | ((binary[10] & 224) >> 5)], 92 | symbols[binary[10] & 31], 93 | symbols[(binary[11] & 248) >> 3], 94 | symbols[((binary[11] & 7) << 2) | ((binary[12] & 192) >> 6)], 95 | symbols[(binary[12] & 62) >> 1], 96 | symbols[((binary[12] & 1) << 4) | ((binary[13] & 240) >> 4)], 97 | symbols[((binary[13] & 15) << 1) | ((binary[14] & 128) >> 7)], 98 | symbols[(binary[14] & 124) >> 2], 99 | symbols[((binary[14] & 3) << 3) | ((binary[15] & 224) >> 5)], 100 | symbols[binary[15] & 31], 101 | ]) 102 | 103 | 104 | def decode_ulid_base32(encoded): 105 | """ 106 | Decode a 26-character long base32 string into the original 16 bytes. 107 | :param encoded: 26-character long string 108 | :return: 16 bytes 109 | """ 110 | if len(encoded) != 26: 111 | raise InvalidULID('base32 ulid is %d characters long, expected 26' % len(encoded)) 112 | 113 | if not all(c in _symbols for c in encoded): 114 | raise InvalidULID('invalid characters in base32 ulid') 115 | 116 | b = [ord(c) for c in encoded] 117 | 118 | if b[0] < 48 or b[0] > 55: 119 | # See https://github.com/oklog/ulid/issues/9: 120 | # Technically, a 26-character Base32 encoded string can contain 130 bits of information, 121 | # whereas a ULID must only contain 128 bits. 122 | # Therefore, the largest valid ULID encoded in Base32 is 7ZZZZZZZZZZZZZZZZZZZZZZZZZ, 123 | # which corresponds to an epoch time of 281474976710655 or 2 ^ 48 - 1. 124 | raise InvalidULID('base32 ulid is out of range (starts with %s; accepted are 01234567)' % encoded[0]) 125 | 126 | tab = _decode_table 127 | binary = [(c & 0xFF) for c in [ 128 | ((tab[b[0]] << 5) | tab[b[1]]), 129 | ((tab[b[2]] << 3) | (tab[b[3]] >> 2)), 130 | ((tab[b[3]] << 6) | (tab[b[4]] << 1) | (tab[b[5]] >> 4)), 131 | ((tab[b[5]] << 4) | (tab[b[6]] >> 1)), 132 | ((tab[b[6]] << 7) | (tab[b[7]] << 2) | (tab[b[8]] >> 3)), 133 | ((tab[b[8]] << 5) | tab[b[9]]), 134 | ((tab[b[10]] << 3) | (tab[b[11]] >> 2)), 135 | ((tab[b[11]] << 6) | (tab[b[12]] << 1) | (tab[b[13]] >> 4)), 136 | ((tab[b[13]] << 4) | (tab[b[14]] >> 1)), 137 | ((tab[b[14]] << 7) | (tab[b[15]] << 2) | (tab[b[16]] >> 3)), 138 | ((tab[b[16]] << 5) | tab[b[17]]), 139 | ((tab[b[18]] << 3) | tab[b[19]] >> 2), 140 | ((tab[b[19]] << 6) | (tab[b[20]] << 1) | (tab[b[21]] >> 4)), 141 | ((tab[b[21]] << 4) | (tab[b[22]] >> 1)), 142 | ((tab[b[22]] << 7) | (tab[b[23]] << 2) | (tab[b[24]] >> 3)), 143 | ((tab[b[24]] << 5) | tab[b[25]]), 144 | ]] 145 | return _to_binary(binary) 146 | 147 | 148 | def get_ulid_timestamp(ulid): 149 | """ 150 | Get the time from an ULID as an UNIX timestamp. 151 | 152 | :param ulid: An ULID (either as UUID, base32 ULID or binary) 153 | :return: UNIX timestamp 154 | :rtype: float 155 | """ 156 | ts_bytes = ulid_to_binary(ulid)[:6] 157 | ts_bytes = b'\0\0' + ts_bytes 158 | assert len(ts_bytes) == 8 159 | return (struct.unpack(b'!Q', ts_bytes)[0] / 1000.) 160 | 161 | 162 | def get_ulid_time(ulid): 163 | """ 164 | Get the time from an ULID as a `datetime.datetime`. 165 | 166 | :param ulid: An ULID (either as UUID, base32 ULID or binary) 167 | :return: Datetime 168 | :rtype: datetime.datetime 169 | """ 170 | timestamp = get_ulid_timestamp(ulid) 171 | return datetime.datetime.utcfromtimestamp(timestamp) 172 | 173 | 174 | _last_entropy = None 175 | _last_timestamp = None 176 | 177 | 178 | def generate_binary_ulid(timestamp=None, monotonic=False): 179 | """ 180 | Generate the bytes for an ULID. 181 | 182 | :param timestamp: An optional timestamp override. 183 | If `None`, the current time is used. 184 | :type timestamp: int|float|datetime.datetime|None 185 | :param monotonic: Attempt to ensure ULIDs are monotonically increasing. 186 | Monotonic behavior is not guaranteed when used from multiple threads. 187 | :type monotonic: bool 188 | :return: Bytestring of length 16. 189 | :rtype: bytes 190 | """ 191 | global _last_entropy, _last_timestamp 192 | if timestamp is None: 193 | timestamp = time.time() 194 | elif isinstance(timestamp, datetime.datetime): 195 | timestamp = calendar.timegm(timestamp.utctimetuple()) 196 | 197 | ts = int(timestamp * 1000.0) 198 | ts_bytes = struct.pack(b'!Q', ts)[2:] 199 | entropy = os.urandom(10) 200 | if monotonic and _last_timestamp == ts and _last_entropy is not None: 201 | while entropy < _last_entropy: 202 | entropy = os.urandom(10) 203 | _last_entropy = entropy 204 | _last_timestamp = ts 205 | return ts_bytes + entropy 206 | 207 | 208 | def generate_ulid_as_uuid(timestamp=None, monotonic=False): 209 | """ 210 | Generate an ULID, but expressed as an UUID. 211 | 212 | :param timestamp: An optional timestamp override. 213 | If `None`, the current time is used. 214 | :type timestamp: int|float|datetime.datetime|None 215 | :param monotonic: Attempt to ensure ULIDs are monotonically increasing. 216 | Monotonic behavior is not guaranteed when used from multiple threads. 217 | :type monotonic: bool 218 | :return: UUID containing ULID data. 219 | :rtype: uuid.UUID 220 | """ 221 | return uuid.UUID(bytes=generate_binary_ulid(timestamp, monotonic=monotonic)) 222 | 223 | 224 | def generate_ulid_as_base32(timestamp=None, monotonic=False): 225 | """ 226 | Generate an ULID, formatted as a base32 string of length 26. 227 | 228 | :param timestamp: An optional timestamp override. 229 | If `None`, the current time is used. 230 | :type timestamp: int|float|datetime.datetime|None 231 | :param monotonic: Attempt to ensure ULIDs are monotonically increasing. 232 | Monotonic behavior is not guaranteed when used from multiple threads. 233 | :type monotonic: bool 234 | :return: ASCII string 235 | :rtype: str 236 | """ 237 | return encode_ulid_base32(generate_binary_ulid(timestamp, monotonic=monotonic)) 238 | 239 | 240 | def ulid_to_base32(ulid): 241 | """ 242 | Convert an ULID to its base32 representation. 243 | 244 | :param ulid: An ULID (either as UUID, base32 ULID or binary) 245 | :return: ASCII string 246 | :rtype: str 247 | """ 248 | return encode_ulid_base32(ulid_to_binary(ulid)) 249 | 250 | 251 | def ulid_to_uuid(ulid): 252 | """ 253 | Convert an ULID to its UUID representation. 254 | 255 | :param ulid: An ULID (either as UUID, base32 ULID or binary) 256 | :return: UUID 257 | :rtype: uuid.UUID 258 | """ 259 | return uuid.UUID(bytes=ulid_to_binary(ulid)) 260 | 261 | 262 | def ulid_to_binary(ulid): 263 | """ 264 | Convert an ULID to its binary representation. 265 | 266 | :param ulid: An ULID (either as UUID, base32 ULID or binary) 267 | :return: Bytestring of length 16 268 | :rtype: bytes 269 | """ 270 | if isinstance(ulid, uuid.UUID): 271 | return ulid.bytes 272 | if isinstance(ulid, (text_type, bytes)) and len(ulid) == 26: 273 | return decode_ulid_base32(ulid) 274 | if isinstance(ulid, (bytes, bytearray)) and len(ulid) == 16: 275 | return ulid 276 | raise InvalidULID('can not convert ulid %r to binary' % ulid) 277 | -------------------------------------------------------------------------------- /ulid2/__init__.pyi: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Optional, Union 3 | from uuid import UUID 4 | 5 | AnyULID = Union[UUID, str, bytes, bytearray] 6 | AnyTime = Union[int, float, datetime] 7 | 8 | class InvalidULID(ValueError): ... 9 | 10 | def encode_ulid_base32(binary: bytes) -> str: ... 11 | def decode_ulid_base32(encoded: str) -> bytes: ... 12 | def get_ulid_timestamp(ulid: AnyULID) -> float: ... 13 | def get_ulid_time(ulid: AnyULID) -> datetime: ... 14 | def generate_binary_ulid(timestamp: Optional[AnyTime] = ..., monotonic: bool = ...) -> bytes: ... 15 | def generate_ulid_as_uuid(timestamp: Optional[AnyTime] = ..., monotonic: bool = ...) -> UUID: ... 16 | def generate_ulid_as_base32(timestamp: Optional[AnyTime] = ..., monotonic: bool = ...) -> str: ... 17 | def ulid_to_base32(ulid: AnyULID) -> str: ... 18 | def ulid_to_uuid(ulid: AnyULID) -> UUID: ... 19 | def ulid_to_binary(ulid: AnyULID) -> bytes: ... 20 | -------------------------------------------------------------------------------- /ulid2/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valohai/ulid2/bd33464f35f6090bc9d1b0e15fd3a0ed6293a6c1/ulid2/py.typed --------------------------------------------------------------------------------