Git Product home page Git Product logo

python-ulid's Introduction


ulid


A ULID is a universally unique lexicographically sortable identifier. It is

  • 128-bit compatible with UUID
  • 1.21e+24 unique ULIDs per millisecond
  • Lexicographically sortable!
  • Canonically encoded as a 26 character string, as opposed to the 36 character UUID
  • Uses Crockford's base32 for better efficiency and readability (5 bits per character)
  • Case insensitive
  • No special characters (URL safe)

In general the structure of a ULID is as follows:

 01AN4Z07BY      79KA1307SR9X4MV3
|----------|    |----------------|
 Timestamp          Randomness
   48bits             80bits

For more information have a look at the original specification.

Installation

Use pip to install the library

$ pip install python-ulid

to include Pydantic support install the optional dependency like so

$ pip install python-ulid[pydantic]

Basic Usage

Create a new ULID object from the current timestamp

>>> from ulid import ULID
>>> ULID()
ULID(01E75HZVW36EAZKMF1W7XNMSB4)

or use one of the named constructors

>>> import time, datetime
>>> ULID.from_timestamp(time.time())
ULID(01E75J1MKKWMGG0N5MBHFMRC84)
>>> ULID.from_datetime(datetime.datetime.now())
ULID(01E75J2XBK390V2XRH44EHC10X)

There are several options for encoding the ULID object (e.g. string, hex, int, bytes, UUID):

>>> str(ulid)
'01BTGNYV6HRNK8K8VKZASZCFPE'
>>> ulid.hex
'015ea15f6cd1c56689a373fab3f63ece'
>>> int(ulid)
1820576928786795198723644692628913870
>>> bytes(ulid)
b'\x01^\xa1_l\xd1\xc5f\x89\xa3s\xfa\xb3\xf6>\xce'
>>> ulid.to_uuid()
UUID('015ea15f-6cd1-c566-89a3-73fab3f63ece')

It is also possible to directly access the timestamp component of a ULID, either in UNIX epoch or as datetime.datetime

>>> ulid.timestamp
1505945939.153
>>> ulid.datetime
datetime.datetime(2017, 9, 20, 22, 18, 59, 153000, tzinfo=datetime.timezone.utc)

Pydantic integration

The ULID class can be directly used for the popular data validation library Pydantic like so

from pydantic import BaseModel
from ulid import ULID


class Model(BaseModel):
  ulid: ULID

model = Model(ulid="DX89370400440532013000")  # OK
model = Model(ulid="not-a-ulid")  # Raises ValidationError

Command line interface

The package comes with a CLI interface that can be invoked either by the script name ulid or as python module python -m ulid. The CLI allows you to generate, inspect and convert ULIDs, e.g.

$ ulid build
01HASFKBN8SKZTSVVS03K5AMMS

$ ulid build --from-datetime=2023-09-23T10:20:30
01HB0J0F5GCKEXNSWVAD5PEAC1

$ ulid show 01HASFKBN8SKZTSVVS03K5AMMS
ULID:      01HASFKBN8SKZTSVVS03K5AMMS
Hex:       018ab2f9aea8ccffacef7900e6555299
Int:       2049395013039097460549394558635823769
Timestamp: 1695219822.248
Datetime:  2023-09-20 14:23:42.248000+00:00

There are several flags to select specific output formats for the show command, e.g.

$ ulid show --datetime 01HASFKBN8SKZTSVVS03K5AMMS
2023-09-20 14:23:42.248000+00:00

The special character - allows to read values from stdin so that they can be piped. E.g.

$ echo 01HASFKBN8SKZTSVVS03K5AMMS | ulid show --uuid -
018ab2f9-aea8-4cff-acef-7900e6555299

$ date --iso-8601 | python -m ulid build --from-datetime -
01HAT9PVR02T3S13XB48S7GEHE

For a full overview of flags for the build and show commands use the --help option (e.g. ulid show --help).

Other implementations

python-ulid's People

Contributors

avihais12344 avatar bendykst avatar ericnelson12 avatar johnpaulett avatar mdomke avatar musicinmybrain avatar simonw avatar somnam avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

python-ulid's Issues

Inaccurate Pydantic JSON Schema when Field is Optional

When using ULID in Pydantic Models within a FastAPI app with using default of None to make the field optional, the generated JSON schema is only generated with "group_id": {"type": "null"} rather than "group_id": {"anyOf": [{"type": "string"}, {"type": "null"}]}.

Pydantic Schema:

class UserUpdateRequest(BaseModel):
    first_name: str | None = Field(max_length=128, default=None)
    last_name: str | None = Field(max_length=128, default=None)
    is_active: bool | None = None
    is_admin: bool | None = None
    email: EmailStr | None = Field(max_length=128, default=None)
    password: str | None = Field(max_length=128, default=None)
    group_id: ULID | None = None
    employee_id: str | None = Field(max_length=16, default=None)

I believe this can be fixed by modifying core_schema.union_schema() in init.py to include core_schema.str_schema() like so:

    @classmethod
    def __get_pydantic_core_schema__(cls, source: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
        from pydantic_core import core_schema

        return core_schema.no_info_wrap_validator_function(
            cls._pydantic_validate,
            core_schema.union_schema(
                [
                    core_schema.is_instance_schema(ULID),
                    core_schema.no_info_plain_validator_function(ULID),
                ]
            ),
            serialization=core_schema.to_string_ser_schema(
                when_used="json-unless-none",
            ),
        )

to

    @classmethod
    def __get_pydantic_core_schema__(cls, source: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
        from pydantic_core import core_schema

        return core_schema.no_info_wrap_validator_function(
            cls._pydantic_validate,
            core_schema.union_schema(
                [
                    core_schema.is_instance_schema(ULID),
                    core_schema.no_info_plain_validator_function(ULID),
                    core_schema.str_schema(),
                ]
            ),
            serialization=core_schema.to_string_ser_schema(
                when_used="json-unless-none",
            ),
        )

I'm not familiar enough with Pydantic to know if this has unintended effects or not but it appears to work correctly in my app and allows for correct generation of types with a typescript generator from the generated OpenAPI JSON. Any feedback is appreciated.

Ability to use the lib as a Python module returning a ULID

Like so:

$ python -m ulid
01BTGNYV6HRNK8K8VKZASZCFPE

I guess implementing a __main__ which returns ULID.from_datetime(datetime.now()) by default.

It would ease the generation when calling from a Makefile for instance. My current alternative is the less elegant:

$ python -c 'from ulid import ULID;print(str(ULID()))'

ULID can't be serialized with Pydantic's dump model to json

Hello,
I see this package has support for Pydantic. I want to use the model_dump_json so I could serialize my model to json in my webserver. But I can't serialize ULID. It seems like an implementation error.

How to reproduce:

  • Pip freeze:

    annotated-types==0.6.0
    pydantic==2.6.4
    pydantic_core==2.16.3
    python-ulid==2.3.0
    typing_extensions==4.10.0
  • Code:

    from pydantic import BaseModel
    from pydantic_core import PydanticSerializationError
    from ulid import ULID
    
    
    class MyModel(BaseModel):
        my_id: ULID
    
    my_test = MyModel(my_id=ULID())
    
    try:
        print(f"{my_test.model_dump_json()=}")
    except PydanticSerializationError as e:
        print(f"{e=}")
    
    print(f"{my_test.model_dump()=}")

    The output would be:

    e=PydanticSerializationError(Error serializing to JSON: PydanticSerializationError: Unable to serialize unknown type: <class 'ulid.ULID'>)
    my_test.model_dump()={'my_id': ULID(01HT4RBZKNQ9DFXZQ8DW02Z27Y)}

    We can see ha model dump is supported, but not model dump json.

question: Is there a way to validate whether a 26 char string conforms to a valid ULID?

To verify whether a received string conforms to a valid ULID, I use the ULID.from_str method:

    try:
        return ULID.from_str(value)
    except ValueError:
        return None

Occasionally, a string of 26 characters may not be a valid ULID but may match the ULID length. I initially assumed such values would trigger a ValueError. However, it seems that any string consisting of 26 ASCII chars is accepted by from_str, resulting in ULID instances created from random data, i.e.:

>>> ULID.from_str('notavalidulidnotavalidulid')
ULID(7ZTAVFZZFZZZDQZTAVFZZFZZZD)

>>> ULID.from_str('DHCKWI*$HF$%MCNSKDW(#@_SCK')
ULID(5HCKZZZZHFZZMCNSKDZZZZZSCK)

Is this the intended behavior?

Additionally in some cases, the year part of the timestamp extracted from such values surpasses the maximum allowed by the datetime class, leading to a year is out of range error when inspecting the datetime:

>>> ULID.from_str('notavalidulidnotavalidulid').datetime
ValueError: year 10883 is out of range

>>> ULID.from_str("".join(random.choices(string.ascii_letters, k=26))).datetime
ValueError: year 10889 is out of range

>>> ULID.from_str('Z' * 26).datetime
ValueError: year 10889 is out of range

Perhaps the decoded timestamp part could be validated whether it exceeds the maximum value accepted by datetime.datetime?

Multiple conflicting command-line tools called ulid exist

Both this project and https://github.com/oklog/ulid/ build executables called ulid. The purpose of this issue is to open a discussion about possibly renaming one or both of them so that they can co-exist without workarounds.

This is motivated by the Fedora Linux case; we already have a golang-github-oklog-ulid package that installs /usr/bin/ulid, and since https://github.com/pydantic/pydantic-extra-types added a dependency on https://github.com/mdomke/python-ulid/, we are now working on a python-python-ulid package as well. A quick survey suggests that we may be the first Linux distribution to package both projects. We have some guidelines about how to deal with binary name conflicts, and approaching the relevant upstreams about a possible renaming is the first step.

Ideally, one or both projects would rename the ulid binary to something more unique, so that distributions don’t have to deal with conflicting packages or choose their own way to rename binaries. The issue #13 may or may not also be relevant, depending on the response.

Maintenance?

Is this library maintained? Should we switch to another python ULID library? This PR has been open for almost a year without any feedback.

I'd be willing help out with maintaining this.

UUIDs created with ULID don't specify RFC_4122 variant

I'm creating UUIDs with ULID for use in an application. As part of some data validation libraries in that application, these UUIDs must be RFC_4122 compliant.

However, some of the UUIDs created with ULID fail this check:

import uuid
from ulid import ULID


ulid = ULID.from_str('01GJMN8DEMHMCJKXM3MADM3J45')
uuid_value = ulid.to_uuid()
print(uuid_value.variant == uuid.RFC_4122)  # False
for uuid_created_with_uuid in (uuid.uuid1(), uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org'), uuid.uuid4(),
                               uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org')):
    print(uuid_created_with_uuid.variant == uuid.RFC_4122)  # True for all of them

This bit uuid_value.variant == uuid.RFC_4122 is what the data validation library does to check that a UUID is RFC_4122 compliant. It would be great to have that supported with ULID if possible.

Possible confusion with the “ulid” package

Since

there is the possibility of user confusion.

Is there a possibility of finding a truly unique name for this library in order to reduce the possibility of confusion?

The Fedora Linux packaging guidelines say:

If the importable module name and the project name do not match, users frequently end up confused. In this case, packagers SHOULD ensure that upstream is aware of the problem and (especially for new packages where renaming is feasible) strive to get the package renamed. The Python SIG is available for assistance.

Implement __hash__

Suggest you implement hash to allow use as a dictionary key.

I am getting the following issue using ULID as a custom type in a SqlAlchemy Model.

if key not in self._dict:
  TypeError: unhashable type: 'ULID'

Maybe add something like this?

def __hash__(self):
  return hash(self.bytes)

or

def __hash__(self):
  return int(self)

Though, that might be a longer integer than expected for a hash value?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.