Thanks for making this library and writing it in rust!
Currently this library expects uuid7(timestamp)
to get an integer milliseconds value, which doesn't match the seconds.msµs
precision that the python datetime
standard library uses for unix timestamps and datetime
objects.
uuid7(timestamp=..., resolution: int=1_000_000)
(nanoseconds) parameter?)datetime
obj and seconds.msµs
ts support to uuid7(timestamp)
?I propose uuid_utils.uuid7(timestamp=...)
be extended to accept int | float | datetime
as ms
or seconds.msµs
in order to align with python ecosystem expectations. This can be accomplished without breaking any existing behavior or adding any new non-determinism, because milliseconds-based timestamps are easily distinguished based on length. If a native python datetime
or timestamp float
with µs precision is passed, I think it is safe and expected to fill the optional uuid7 fields up to the same ~1µs resolution that the user provides.
While the 2022 draft only had millisecond precision, the most recent RFC draft as of 2024 has support for variable precision (1ms ~ 50ns) transparently in a single namespace by opportunistically using the leftmost bits of the randomness segment.
https://www.rfc-editor.org/rfc/rfc9562#monotonicity_counters
The default precision can stay at the millisecond level as recommended by UUIDv7, but for applications that work in the python ecosystem and/or need finer control, accepting datetime | float
could provide an escape hatch to access more precision.
Right now, calls in quick succession result in identical timestamps between separate uuid7s. It's unfortunate because the uuid7 has space for more precision in theory, it would be nice to be able to encode timstamps up to the same resolution that the standard library can. Apps could get slightly more monotonic (and can also get rid of redundant created_at
fields).
print(uuid7().timestamp, uuid7().timestamp) # 1735718399999, 1735718399999
# could be this:
print(uuid7().timestamp, uuid7().timestamp) # 1735718399.999123, 1735718399.999124
In order to not break backwards compatibility, all this could be accomplished by overloading the signature of uuid7
:
- def uuid7(timestamp: int | None = None):
+ def uuid7(timestamp: int | float | datetime | None = None):
+ timestamp = timestamp or datetime.now()
+ if isinstance(timestamp, (int, float)):
+ try:
+ # first try parsing ts as int/float seconds (the python stdlib default format)
+ timestamp = datetime.fromtimestamp(timestamp, UTC)
+ # will throw "ValueError: year 56964 is out of range" if ts is milliseconds
+ except ValueError:
+ # preserve existing uuid_utils.uuid7 default behavior (timestamp is milliseconds as int)
+ timestamp = datetime.fromtimestamp(timestamp/1000, UTC)
+
...
UUID().date
property be added to return the full microsecond-precision datetime
?>>> datetime.fromtimestamp(uuid7().timestamp, UTC) # off by /1000, not what python expects
ValueError: year 56963 is out of range
# ideally UUID.timestamp should return a python-standard seconds.msµs float like so:
>>> datetime.fromtimestamp(uuid7(1735718399999).timestamp) # aka 1735718399.999000
datetime.datetime(2024, 12, 31, 23, 59, 59, 999000) # (the default, ms precision)
>>> datetime.fromtimestamp(uuid7(1735718399.999123).timestamp)
datetime.datetime(2024, 12, 31, 23, 59, 59, 999123) # (µs precision when ts is passed)
# but in order to not break backwards compatibility, adding a new .date property is more realistic:
>>> uuid7(1735718399.999123).date
datetime.datetime(2024, 12, 31, 23, 59, 59, 999123)
>>> uuid7(1735718399.999123).date.timestamp() # allows getting python timestamp easily too
1735718399.999123
+ from datetime import UTC, datetime
class UUID:
...
+ @property
+ def date(self) -> datetime:
+ return datetime.fromtimestamp(self.timestamp/1000, UTC)
# example usage:
>>> ts_with_µs = 1735718399.999123 # sec.msµs float, same format as python stdlib datetime.timestamp()
>>> dt_with_µs = datetime.fromtimestamp(ts, UTC) # == datetime(2024, 12, 31, 23, 59, 59, 999123)
>>> uuid7_with_µs = uuid7(timestamp=dt_with_µs) # pass datetime with ms and/or µs
>>> assert uuid7_with_µs == uuid7(timestamp=ts_with_µs) # pass ts as int or float with µs
>>> after_dt = datetime.fromtimestamp(uuid7_with_µs.timestamp, UTC) # == datetime.datetime(2024, 12, 31, 23, 59, 59, 999123)
>>> after_ts = _with_microseconds.timestamp() # ms.µs should be identical to original
1735445361.908123
# ms & µs should survive encode/decode with full precision
>>> assert ts_with_µs == after_ts == dt_with_µs.timestamp() == after_dt.timestamp() == uuid7_with_µs.timestamp
The 128 bits in the UUID would be allocated as follows:
Pay now to fund the work behind this issue.
Get updates on progress being made.
Maintainer is rewarded once the issue is completed.
You're funding impactful open source efforts
You want to contribute to this effort
You want to get funding like this too