├── .flake8 ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── ksuid ├── __init__.py ├── base62.py ├── ksuid.py └── utils.py ├── requirements-dev.txt ├── setup.py └── tests ├── ksuid_test.py └── test_ksuid_serialization_support.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .idea, __pycache__, log, __init__.py, env 3 | max-line-length = 150 4 | max-complexity = 10 5 | filename = *.py 6 | format = default 7 | import-order-style = google 8 | inline-quotes = " 9 | ignore = 10 | # E221 multiple spaces before operator 11 | E221, 12 | # whitespace before ':' 13 | E203, 14 | # whitespace before ')' 15 | E202, 16 | # multiple spaces after ',' 17 | E241, 18 | # unexpected spaces around keyword / parameter equals 19 | E251, 20 | #comparison to True should be 'if cond is True:' or 'if cond (it's needed for SQLAlchemy) 21 | E712 22 | 23 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X system files 2 | .DS_Store 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-delete-this-directory.txt 37 | pip-log.txt 38 | pip-selfcheck.json 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Python and Django stuff: 55 | .python-version 56 | *.log 57 | 58 | # Sphinx documentation 59 | docs/_build/ 60 | 61 | # PyBuilder 62 | target/ 63 | 64 | # PyBuild 65 | .pybuild/ 66 | 67 | # Node and NPM 68 | npm*.log* 69 | 70 | # IntelliJ IDEA and alike 71 | .idea/ 72 | *.iml 73 | 74 | # Visual Studio and alike 75 | .vscode/ 76 | bin/ 77 | obj/ 78 | bower_components/ 79 | typings/ 80 | node_modules/ 81 | local/ 82 | static/ 83 | 84 | # Robots and Selenium artefacts 85 | integration/*.html 86 | integration/*.xml 87 | */integration-tests/*.html 88 | */integration-tests/*.xml 89 | 90 | 91 | # Miscellaneous 92 | log.txt 93 | tmp.txt 94 | mytest.sh 95 | chromedriver.exe 96 | aja_testit.bat 97 | /.vs/Balticsys/v14/.suo 98 | /web.config 99 | /web.debug.config 100 | /ptvs_virtualenv_proxy.py 101 | /obj/Debug/web.debug.config 102 | /obj/Debug/web.config 103 | /obj/Debug/visualstudio_py_util.py 104 | /obj/Debug/visualstudio_py_repl.py 105 | /obj/Debug/visualstudio_py_debugger.py 106 | /obj/Debug/deployment-updated-orig-prefix.txt 107 | /obj/Debug/Balticsys.pyproj.FileListAbsolute.txt 108 | /obj/Debug/attach_server.py 109 | /obj/Debug/__main__.py 110 | /obj/Debug/__init__.py 111 | /Balticsys.pyproj.user 112 | /.vs/config/applicationhost.config 113 | ui_tests/dev_runs/history/ 114 | tests_all_dev.bat 115 | server/web.debug.config 116 | server/web.config 117 | server/.vs 118 | server/py3env 119 | server/python3 120 | sp-test-data/* 121 | # Vagrant 122 | .vagrant/ 123 | infrastructure/dev/.vagrant 124 | infrastructure/alpha/archives 125 | infrastructure/empty_deb/debs/ 126 | 127 | # ctags 128 | tags 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Samuel Resendez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ENV = env 2 | PYBIN = $(ENV)/scripts 3 | PYTHON = $(PYBIN)/python 4 | PIP = $(PYBIN)/pip 5 | PYTEST = $(PYTHON) -m pytest 6 | COVERAGE = $(PYTHON) -m coverage 7 | PYFLAKE8 = $(PYTHON) -m flake8 8 | TESTDIR = tests 9 | MODULE_NAME = ksuid 10 | DEV_CONFIG_FILE = ./conf/reqlog-dev.conf 11 | 12 | SHELL=C:/Windows/System32/cmd.exe 13 | VIRTUALENV_APP_P27 = d:/Python27/python.exe -m virtualenv 14 | 15 | .PHONY: environ 16 | environ: clean requirements-dev.txt 17 | virtualenv $(ENV) 18 | $(PIP) install -r requirements-dev.txt 19 | 20 | .PHONY: environ_p27 21 | environ_p27: clean requirements-dev.txt 22 | $(VIRTUALENV_APP_P27) $(ENV) 23 | $(PIP) install -r requirements-dev.txt 24 | 25 | .PHONY: help 26 | help: 27 | @echo "make # create virtual env and setup dependencies" 28 | @echo "make tests # run tests" 29 | @echo "make coverage # run tests with coverage report" 30 | @echo "make lint # check linting" 31 | @echo "make flake8 # alias for `make lint`" 32 | @echo "make clean # remove more or less everything created by make" 33 | 34 | .PHONY: tests 35 | tests: 36 | $(PYTEST) $(TESTDIR) -vv 37 | 38 | .PHONY: coverage 39 | coverage: 40 | $(PYTEST) $(TESTDIR) -vv --cov=$(MODULE_NAME) 41 | $(COVERAGE) html 42 | 43 | .PHONY: lint 44 | lint: 45 | $(PYFLAKE8) 46 | 47 | .PHONY: flake8 48 | flake8: 49 | $(PYFLAKE8) 50 | 51 | .PHONY: clean 52 | clean: 53 | if exist $(ENV) rd $(ENV) /q /s 54 | if exist reqlog.egg-info rd reqlog.egg-info /q /s 55 | if exist .coverage del .coverage 56 | del /S *.pyc 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KSUID 2 | 3 | ## What is a ksuid? 4 | 5 | A ksuid is a K sorted UID. In other words, a KSUID also stores a date component, so that ksuids can be approximately 6 | sorted based on the time they were created. 7 | 8 | 9 | ## Quick overview 10 | 11 | A ksuid is composed of two components: the date time, which is stored as the first four bytes of the uid, along with a randomly 12 | generated payload of 16, for a total of 20 bytes. 13 | 14 | ## A few advantages 15 | 16 | 1. The potential number of IDs available is greater than even that of UUID4 (which accepts 122 bits). KSUIDs are about 64 times larger than that. Coupled with the timestamp, the likelihood of getting 2 identical Ksuids is exceedingly low; 17 | 2. Enables sorting of UIDs in a sensible fashion, based on their timestamp; 18 | 3. Supports Base62 encoding - can be serialized to Base62 and vice versa 19 | 20 | 21 | ## Installation 22 | 23 | The ksuid library can be installed via pip: 24 | 25 | ```pip install ksuid``` 26 | 27 | ** Note: currently only tested for Python 3.x ** 28 | ## Sample Usage: 29 | 30 | ```python 31 | >>> from ksuid import ksuid 32 | >>> x = ksuid() 33 | >>> print(x) 34 | >>> '05cbd3454355fe1e1f11c85bb2c1e3e2f7c93525 ' 35 | >>> x.getTimestamp() 36 | >>> 1497243973 37 | >>> x.getDatetime() 38 | >>> datetime.date(2017, 6, 11) 39 | >>> x 40 | >>> 41 | >>> x.bytes() 42 | >>> b'\x05\xcb\xd7\xd0\xc6\xcb\x98i\xeb\xa0}\xfa\x0f\x87\xf1\xf1\xe8\xa1\x83\x9e' 43 | ``` 44 | 45 | ## Base62 support 46 | 47 | ```python 48 | >>> import ksuid 49 | >>> uid = ksuid.ksuid() 50 | >>> print(uid) 51 | >>> '0607ac1e7955e3d6a5da87c8dae2e1825c8ddfc9' 52 | >>> v = uid.toBase62() 53 | >>> print(v) 54 | >>> 'rLIliIsDsLNj1b4tN1T3TZGC1B' 55 | ``` 56 | 57 | ## Serialization to bytes support 58 | 59 | ```python 60 | >>> import ksuid 61 | >>> uid = ksuid.ksuid() 62 | >>> print(uid) 63 | >>> '0607ac7351a48bb5e2f63b68094e465010496ff6' 64 | >>> v = uid.toBytes() 65 | >>> print(v) 66 | >>> b'\x06\x07\xacsQ\xa4\x8b\xb5\xe2\xf6;h\tNFP\x10Io\xf6' 67 | ``` 68 | 69 | ## Developing 70 | 71 | First of all you need `make` utility for a little bit comfortable usage. 72 | 73 | You can execute `make help` to view all available commands. 74 | 75 | Please, run `make` to install virtual environment for testing and development. 76 | 77 | Supported commands: 78 | 79 | * `make` - create virtual env and setup dependencies; 80 | * `make tests` - run tests; 81 | * `make coverage` - run tests with coverage report; 82 | * `make lint` - check linting; 83 | * `make flake8` - alias for `make lint` 84 | * `make clean` - remove more or less everything created by make 85 | 86 | ## Credit, where credit is due 87 | 88 | This library is largely inspired by the go version, found here: 89 | https://github.com/segmentio/ksuid 90 | 91 | -------------------------------------------------------------------------------- /ksuid/__init__.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | import datetime 3 | import os 4 | import time 5 | 6 | try: 7 | from ksuid.ksuid import * 8 | except ImportError: 9 | from .ksuid import * 10 | 11 | -------------------------------------------------------------------------------- /ksuid/base62.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | base62 4 | ~~~~~~ 5 | 6 | Originated from http://blog.suminb.com/archives/558 7 | """ 8 | __title__ = "base62" 9 | __author__ = "Sumin Byeon" 10 | __email__ = "suminb@gmail.com" 11 | __version__ = "0.3.2" 12 | 13 | CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 14 | BASE = 62 15 | 16 | 17 | def bytes_to_int(s, byteorder="big", signed=False): 18 | """Converts a byte array to an integer value. 19 | 20 | Python 3 comes with a built-in function to do this, but we would like to 21 | keep our code Python 2 compatible. 22 | """ 23 | 24 | try: 25 | return int.from_bytes(s, byteorder, signed=signed) 26 | except AttributeError: 27 | # For Python 2.x 28 | if byteorder != "big" or signed: 29 | raise NotImplementedError() 30 | 31 | # NOTE: This won't work if a generator is given 32 | n = len(s) 33 | ds = (x << (8 * (n - 1 - i)) for i, x in enumerate(bytearray(s))) 34 | 35 | return sum(ds) 36 | 37 | 38 | def encode(n, minlen=1): 39 | """Encodes a given integer ``n``.""" 40 | 41 | chs = [] 42 | while n > 0: 43 | r = n % BASE 44 | n //= BASE 45 | 46 | chs.append(CHARSET[r]) 47 | 48 | if len(chs) > 0: 49 | chs.reverse() 50 | else: 51 | chs.append("0") 52 | 53 | s = "".join(chs) 54 | s = CHARSET[0] * max(minlen - len(s), 0) + s 55 | return s 56 | 57 | 58 | def encodebytes(s): 59 | """Encodes a bytestring into a base62 string. 60 | 61 | :param s: A byte array 62 | """ 63 | 64 | _check_bytes_type(s) 65 | return encode(bytes_to_int(s)) 66 | 67 | 68 | def decode(b): 69 | """Decodes a base62 encoded value ``b``.""" 70 | 71 | if b.startswith("0z"): 72 | b = b[2:] 73 | 74 | l, i, v = len(b), 0, 0 75 | for x in b: 76 | v += _value(x) * (BASE ** (l - (i + 1))) 77 | i += 1 78 | 79 | return v 80 | 81 | 82 | def decodebytes(s): 83 | """Decodes a string of base62 data into a bytes object. 84 | 85 | :param s: A string to be decoded in base62 86 | :rtype: bytes 87 | """ 88 | 89 | decoded = decode(s) 90 | buf = bytearray() 91 | while decoded > 0: 92 | buf.append(decoded & 0xff) 93 | decoded //= 256 94 | buf.reverse() 95 | 96 | return bytes(buf) 97 | 98 | 99 | def _value(ch): 100 | """Decodes an individual digit of a base62 encoded string.""" 101 | 102 | try: 103 | return CHARSET.index(ch) 104 | except ValueError: 105 | raise ValueError("base62: Invalid character (%s)" % ch) 106 | 107 | 108 | def _check_bytes_type(s): 109 | """Checks if the input is in an appropriate type.""" 110 | 111 | if (not isinstance(s, bytes)) and (not isinstance(s, bytearray)): 112 | msg = "expected bytes-like object, not %s" % s.__class__.__name__ 113 | raise TypeError(msg) 114 | -------------------------------------------------------------------------------- /ksuid/ksuid.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import sys 4 | import time 5 | 6 | from .base62 import decodebytes, encodebytes 7 | from .utils import int_from_bytes, int_to_bytes 8 | 9 | # Used instead of zero(January 1, 1970), so that the lifespan of KSUIDs 10 | # will be considerably longer 11 | EPOCH_TIME = 1400000000 12 | 13 | TIME_STAMP_LENGTH = 4 # number bytes storing the timestamp 14 | BODY_LENGTH = 16 # Number of bytes consisting of the UUID 15 | 16 | 17 | class ksuid(): 18 | """ The primary classes that encompasses ksuid functionality. 19 | When given optional timestamp argument, the ksuid will be generated 20 | with the given timestamp. Else the current time is used. 21 | """ 22 | 23 | def __init__(self, timestamp=None): 24 | payload = os.urandom(BODY_LENGTH) # generates the payload 25 | if timestamp is None: 26 | currTime = int(time.time()) 27 | else: 28 | currTime = timestamp 29 | # Note, this code may throw an overflow exception, far into the future 30 | byteEncoding = int_to_bytes(int(currTime - EPOCH_TIME), 4, "big") 31 | 32 | self.__uid = list(byteEncoding) + list(payload) 33 | 34 | def getDatetime(self): 35 | """ getDatetime() returns a python date object which represents the approximate time 36 | that the ksuid was created """ 37 | 38 | unixTime = self.getTimestamp() 39 | return datetime.datetime.fromtimestamp(unixTime) 40 | 41 | def getTimestamp(self): 42 | """ Returns the value of the timestamp, as a unix timestamp""" 43 | 44 | unixTime = int_from_bytes( 45 | self.__uid[:TIME_STAMP_LENGTH], byteorder="big") 46 | return unixTime + EPOCH_TIME 47 | 48 | # Returns the payload without the unix timestamp 49 | def getPayload(self): 50 | """ Returns the value of the payload, with the timestamp encoded portion removed 51 | Returns: 52 | list : An array of integers, that represent the bytes used to encode the UID """ 53 | 54 | return self.__uid[TIME_STAMP_LENGTH:] 55 | 56 | def bytes(self): 57 | """ Returns the UID as raw bytes """ 58 | 59 | if sys.version_info[0] >= 3: 60 | return bytes(self.__uid) 61 | 62 | return bytes("".join(self.__uid)) 63 | 64 | def toBytes(self): 65 | return self.bytes() 66 | 67 | @staticmethod 68 | def fromBytes(value): 69 | """initializes KSUID from bytes""" 70 | if len(value) != TIME_STAMP_LENGTH + BODY_LENGTH: 71 | raise Exception("Wrong bytearray length") 72 | 73 | res = ksuid() 74 | res.__uid = value 75 | return res 76 | 77 | def toBase62(self): 78 | """ encodes KSUID with Base62 encoding """ 79 | return encodebytes(self.bytes()) 80 | 81 | @staticmethod 82 | def fromBase62(data): 83 | """ decodes KSUID from base62 encoding """ 84 | v = decodebytes(data) 85 | return ksuid.fromBytes(v) 86 | 87 | def __str__(self): 88 | """ Creates a string representation of the Ksuid from the bytelist """ 89 | if sys.version_info[0] >= 3: 90 | return "".join(list(map(lambda x: format(x, "02x"), self.__uid))) 91 | 92 | return "".join(x.encode("hex") for x in self.__uid) 93 | -------------------------------------------------------------------------------- /ksuid/utils.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import sys 3 | 4 | 5 | def sortKSUID(ksuidList): 6 | """ 7 | sorts a list of ksuids by their date (recent in the front) 8 | """ 9 | return sorted(ksuidList, key=lambda x: x.getTimestamp(), reverse=False) 10 | 11 | 12 | def int_to_bytes(n, length, byteorder="big"): 13 | if sys.version_info[0] >= 3: 14 | return int(n).to_bytes(length, byteorder=byteorder) 15 | 16 | h = "%x" % n 17 | s = ("0" * (len(h) % 2) + h).zfill(length * 2).decode("hex") 18 | return s if byteorder == "big" else s[::-1] 19 | 20 | 21 | def int_from_bytes(s, byteorder="big", signed=False): 22 | if sys.version_info[0] >= 3: 23 | return int.from_bytes(s, byteorder, signed=signed) 24 | 25 | if isinstance(s, list): 26 | s = "".join(s) 27 | 28 | return int(codecs.encode(s, "hex"), 16) 29 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | flake8==3.3.0 2 | flake8-import-order==0.12 3 | flake8-quotes==0.9.0 4 | pytest==3.1.2 5 | pytest-mock==1.5.0 6 | pytest-catchlog==1.2.2 7 | pytest-cov==2.4.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup(name="ksuid", 5 | version="1.3", 6 | description="A small python package for creating ksuids", 7 | url="https://github.com/Samuel-Resendez/KSUID", 8 | author="Samuel Resendez", 9 | author_email="samresendez1@gmail.com", 10 | license="MIT", 11 | packages=["ksuid"], 12 | zip_safe=False 13 | ) 14 | -------------------------------------------------------------------------------- /tests/ksuid_test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import unittest 3 | 4 | from ksuid import ksuid 5 | 6 | from ksuid.utils import sortKSUID 7 | 8 | 9 | class KSUIDTests(unittest.TestCase): 10 | def setUp(self): 11 | self.ksList = [] 12 | for i in range(1000): 13 | self.ksList.append(ksuid()) 14 | 15 | self.ksuid1 = ksuid() 16 | self.ksuid2 = ksuid() 17 | 18 | def testTimeStamp(self): 19 | self.assertTrue(self.ksuid1.getTimestamp() <= self.ksuid2.getTimestamp()) 20 | self.assertTrue(datetime.datetime.now().day == self.ksuid1.getDatetime().day) 21 | 22 | def testSort(self): 23 | self.assertTrue(len(self.ksList) > 0) 24 | ksList = sortKSUID(self.ksList) 25 | 26 | for index in range(len(ksList) - 1): 27 | self.assertTrue(ksList[index].getTimestamp() >= ksList[index + 1].getTimestamp()) 28 | 29 | def testStringFunction(self): 30 | for val in self.ksList: 31 | self.assertTrue(str(val) == str(val)) 32 | 33 | def testDifferentUIDs(self): 34 | for val, index in enumerate(self.ksList): 35 | for val2, index2 in enumerate(self.ksList): 36 | if index == index2: 37 | continue 38 | self.assertTrue(str(val2) != str(val)) 39 | 40 | 41 | if __name__ == "__main__": 42 | unittest.main() 43 | -------------------------------------------------------------------------------- /tests/test_ksuid_serialization_support.py: -------------------------------------------------------------------------------- 1 | import string 2 | import time 3 | 4 | from ksuid import ksuid 5 | 6 | 7 | def test_converts_to_base62_and_returns_value_with_numbers_and_letters(): 8 | valid_symbols = string.digits + string.ascii_letters 9 | 10 | uid = ksuid() 11 | value = uid.toBase62() 12 | 13 | assert value is not None 14 | assert len(value) > 0 15 | 16 | for v in value: 17 | assert v in valid_symbols 18 | 19 | 20 | def test_successfully_converts_to_base62_and_vice_versa(): 21 | uid1 = ksuid() 22 | serialized = uid1.toBase62() 23 | 24 | uid2 = ksuid.fromBase62(serialized) 25 | 26 | assert str(uid1) == str(uid2) 27 | assert uid1.bytes() == uid2.bytes() 28 | assert uid1.toBytes() == uid2.toBytes() 29 | assert uid1.toBase62() == uid2.toBase62() 30 | 31 | 32 | def test_successfully_converts_to_bytes_and_vice_versa(): 33 | uid1 = ksuid() 34 | serialized = uid1.toBytes() 35 | 36 | uid2 = ksuid.fromBytes(serialized) 37 | 38 | assert str(uid1) == str(uid2) 39 | assert uid1.bytes() == uid2.bytes() 40 | assert uid1.toBytes() == uid2.toBytes() 41 | assert uid1.toBase62() == uid2.toBase62() 42 | 43 | 44 | def test_base62_orderable(): 45 | list = [] 46 | for _ in range(1000): 47 | list.append(ksuid()) 48 | 49 | sorted_list = sorted(list, key=lambda x: x.toBase62(), reverse=True) 50 | 51 | for index in range(len(list) - 1): 52 | assert sorted_list[index].getTimestamp() >= sorted_list[index + 1].getTimestamp() 53 | 54 | 55 | def test_measure_1s_and_compare_ksuids(): 56 | DELAY_INTERVAL_SECS = 1 57 | 58 | u1 = ksuid() 59 | time.sleep(DELAY_INTERVAL_SECS) 60 | u2 = ksuid() 61 | 62 | assert u1.toBase62() < u2.toBase62() 63 | 64 | assert u2.getTimestamp() > u1.getTimestamp() 65 | assert (u2.getTimestamp() - u1.getTimestamp()) >= 1 66 | 67 | d2 = u2.getDatetime() 68 | d1 = u1.getDatetime() 69 | assert d2 > d1 70 | assert (d2 - d1).total_seconds() >= 1 71 | 72 | assert u1.getPayload() != u2.getPayload() 73 | 74 | 75 | def test_integration_test(): 76 | uid1 = ksuid() 77 | bu = uid1.toBytes() 78 | 79 | assert bu is not None 80 | 81 | uid2 = ksuid.fromBytes(bu) 82 | 83 | assert str(uid1) == str(uid2) 84 | assert uid1.bytes() == uid2.bytes() 85 | assert uid1.toBytes() == uid2.toBytes() 86 | assert uid1.toBase62() == uid2.toBase62() 87 | 88 | b62 = uid1.toBase62() 89 | uid3 = ksuid.fromBase62(b62) 90 | 91 | assert str(uid1) == str(uid3) 92 | assert uid1.bytes() == uid2.bytes() 93 | assert uid1.toBytes() == uid3.toBytes() 94 | assert uid1.toBase62() == uid3.toBase62() 95 | 96 | bs = uid1.bytes() 97 | assert bs is not None 98 | 99 | 100 | def test_bulk_test_for_base62_with_delays(): 101 | list = [] 102 | for _ in range(100): 103 | list.append(ksuid()) 104 | time.sleep(0.01) 105 | 106 | sorted_list = sorted(list, key=lambda x: x.toBase62(), reverse=True) 107 | 108 | for index in range(len(list) - 1): 109 | assert sorted_list[index].getTimestamp() >= sorted_list[index + 1].getTimestamp() 110 | --------------------------------------------------------------------------------