A sqlalchemy model mixn, GlobalIdMixin
which provides a global roughly time-ordered identifier and an obfuscated version of the global id for public consumption.
Makes use of python-ulid, PL/sql ULID and hashids-python.
A sqlalchemy model mixn, GlobalIdMixin
which provides a global roughly time-ordered identifier and an obfuscated version of the global id for public consumption.
Makes use of python-ulid, PL/sql ULID and hashids-python.
import typing as t | |
import sys | |
from random import randint | |
from collections import namedtuple | |
from hashids import Hashids | |
DEFAULT_MIN_HASH_LENGTH = 12 | |
PublicHash = namedtuple("PublicHash", "public_id salt") | |
def generate_hashid( | |
ident: int, min_length: int = DEFAULT_MIN_HASH_LENGTH, salt: t.Optional[int] = None | |
) -> t.Tuple[str, int]: | |
if salt is None: | |
salt = randint(1, sys.maxsize) | |
return PublicHash( | |
Hashids(salt=str(salt), min_length=min_length).encode(ident), salt | |
) |
import typing as t | |
from datetime import date | |
import sqlalchemy as sa | |
import ulid | |
from sqlalchemy.dialects import postgresql | |
from sqlalchemy.ext.declarative import declared_attr | |
from sqlalchemy.ext.hybrid import hybrid_property | |
from sqlalchemy.ext.indexable import index_property | |
from sqlalchemy.events import event | |
from sqlalchemy_utils import get_columns | |
from dateutil.relativedelta import relativedelta | |
from dateutil.rrule import rruleset, rrulestr | |
from dateutil.parser import parse as dt_parse | |
from pydantic import BaseModel | |
from ulid_type import ULIDType | |
from hashid_util import generate_hashid | |
class GlobalIdMixin(object): | |
""" | |
Add an auto-generated, globally unique, time orderable id column | |
based on ULID. | |
""" | |
__func_schema__ = "util" | |
@declared_attr | |
def global_id(cls): | |
return sa.Column( | |
ULIDType(), | |
nullable=True, | |
unique=True, | |
default=ulid.ULID, | |
server_default=sa.text(f'("{cls.__func_schema__}".generate_ulid_uuid())'), | |
) | |
@declared_attr | |
def public_id(cls): | |
return sa.Column(sa.UnicodeText, nullable=True, unique=True) | |
@declared_attr | |
def public_id_salt(cls): | |
return sa.Column(sa.BigInteger(), default=None, nullable=True) | |
def init_public_id(self, salt: t.Optional[int] = None) -> None: | |
if salt is None and self.public_id_salt is not None: | |
salt = self.public_id_salt | |
public_id, salt = generate_hashid(int(self.global_id), salt=salt) | |
self.public_id = public_id | |
self.public_id_salt = salt | |
def _global_id_objects(iter_): | |
for obj in iter_: | |
if isinstance(obj, GlobalIdMixin) and obj.public_id is None: | |
yield obj | |
def enable_global_ids(session): | |
@event.listens_for(session, "before_flush") | |
def before_flush(session, flush_context, instances): | |
for obj in _global_id_objects(session.dirty): | |
obj.init_public_id() | |
for obj in _global_id_objects(session.new): | |
obj.init_public_id() |
from __future__ import absolute_import | |
import uuid | |
import ulid | |
from sqlalchemy import types, util | |
from sqlalchemy.dialects import postgresql | |
from sqlalchemy_utils.types.scalar_coercible import ScalarCoercible | |
from ulid import ULID | |
class ULIDType(types.TypeDecorator, ScalarCoercible): | |
""" | |
Stores a ULID in the database as a native UUID column type | |
but can use TEXT if needed. | |
:: | |
from .lib.sqlalchemy_types import ULIDType | |
class User(Base): | |
__tablename__ = 'user' | |
# Pass `force_text=True` to fallback TEXT instead of UUID column | |
id = sa.Column(ULIDType(force_text=False), primary_key=True) | |
""" | |
impl = postgresql.UUID(as_uuid=True) | |
python_type = ulid.ULID | |
def __init__(self, force_text=False, **kwargs): | |
""" | |
:param force_text: Store ULID as TEXT instead of UUID. | |
""" | |
self.force_text = force_text | |
def __repr__(self): | |
return util.generic_repr(self) | |
def load_dialect_impl(self, dialect): | |
if self.force_text: | |
return dialect.type_descriptor(types.UnicodeText) | |
return dialect.type_descriptor(self.impl) | |
@staticmethod | |
def _coerce(value): | |
if not value: | |
return None | |
if isinstance(value, str): | |
try: | |
value = ulid.ULID.from_str(value) | |
except (TypeError, ValueError): | |
value = ulid.ULID.from_hex(value) | |
return value | |
if isinstance(value, uuid.UUID): | |
return ulid.ULID.from_bytes(value.bytes) | |
if not isinstance(value, ULID): | |
return ulid.ULID.from_bytes(value) | |
return value | |
def process_bind_param(self, value, dialect): | |
if value is None: | |
return value | |
if not isinstance(value, ulid.ULID): | |
value = self._coerce(value) | |
return str(value.to_uuid()) | |
def process_result_value(self, value, dialect): | |
if value is None: | |
return value | |
return self._coerce(value) |