Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
1 of 3 tasks
pirate opened this issue Dec 29, 2024 · 1 comment
Open
1 of 3 tasks

Comments

@pirate
Copy link

pirate commented Dec 29, 2024

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.

  • 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 pirate changed the title Questions about UUID().timestamp and uuid7(timestmap) Questions about UUID().timestamp and uuid7(timestmap) Dec 29, 2024
@pirate pirate changed the title Questions about UUID().timestamp and uuid7(timestmap) Allow passing datetime to uuid7(timestamp) and add UUID.date: datetime getter Dec 29, 2024
@pirate
Copy link
Author

pirate commented Dec 29, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant