uuid-utils icon indicating copy to clipboard operation
uuid-utils copied to clipboard

Allow passing `datetime` to `uuid7(timestamp)` and add `UUID.date: datetime` getter

Open pirate opened this issue 11 months ago • 3 comments

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.

  • [x] support the UUIDv7-recommeded default of using 1ms precision for timestamp
  • [ ] support the python-ecosystem default of using 1µs precision for timestamp
  • [ ] document that UUIDv8 is recommended for all other precisions (e.g. 50ns,100nsetc.) (OR add a uuid7(timestamp=..., resolution: int=1_000_000) (nanoseconds) parameter?)

Can we add native 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.

image https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-7 image

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)
+
	 ...

Can a 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)

Timestamp precision should survive round-trip Encode/Decode

# 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:

  • 48 bits for milliseconds since epoch
  • 4 bits for version
  • 12 bits for µs
  • 2 bits for variant + 2 bits of rand
  • 12 bits for ns / rand
  • 48 bits of randomness

pirate avatar Dec 29 '24 03:12 pirate

Here's my proposed implementation: https://gist.github.com/pirate/7e44387c12f434a77072d50c52a3d18e

pirate avatar Dec 29 '24 16:12 pirate

This looks interesting, do you have the capacity to create a PR to take it from there?

aminalaee avatar May 22 '25 11:05 aminalaee

Unfortunatley I am a rust n00b and have never done rust/python binding stuff before. I could try to do it all on the python side but it seems like some of this logic would be btter suited in the core.

pirate avatar May 25 '25 22:05 pirate