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

Inline API documentation + ModuleType refactoring #133

Merged
merged 22 commits into from
Sep 27, 2023

Conversation

kalaspuff
Copy link
Owner

@kalaspuff kalaspuff commented Sep 25, 2023

Inline API documentation

NAME

utcnow - Package for formatting arbitrary timestamps as strict RFC 3339.

DESCRIPTION

Timestamps as RFC 3339 (Date & Time on the Internet) formatted strings with conversion functionality from other timestamp formats or for timestamps on other timezones. Additionally converts timestamps from datetime objects and other common date utilities.

# Transforms the input value to a timestamp string in RFC 3339 format.
utcnow.rfc3339_timestamp(value, modifier)

# Transforms the input value to a datetime object.
utcnow.as_datetime(value, modifier)

# Transforms the input value to a float value representing unixtime.
utcnow.as_unixtime(value, modifier)

# Transforms the input value to a google.protobuf.Timestamp message.
utcnow.as_protobuf(value, modifier)

value — A value representing a timestamp in any of the allowed input formats, or "now" if left unset.

modifier — An optional modifier to be added to the Unix timestamp of the value. Defaults to 0. Can be specified in seconds (int or float) or as string, for example "+10d" (10 days => 864000 seconds). Can also be set to a negative value, for example "-1h" (1 hour => -3600 seconds).

Examples:

A few examples of transforming an arbitrary timestamp value to a RFC 3339 timestamp string.

>>> import utcnow

>>> utcnow.rfc3339_timestamp("2023-09-07 02:18:00")
"2023-09-07T02:18:00.000000Z"

>>> utcnow.rfc3339_timestamp("2023-09-07 02:18:00", "+7d")
"2023-09-14T02:18:00.000000Z"

>>> utcnow.rfc3339_timestamp("2023-09-07 02:18:00+02:00")
"2023-09-07T00:18:00.000000Z"

>>> utcnow.rfc3339_timestamp(1693005993.285967)
"2023-08-25T23:26:33.285967Z"

>>> utcnow.rfc3339_timestamp()
"2023-09-07T01:04:38.091041Z"  # current time

Returned timestamps follow RFC 3339 (Date and Time on the Internet: Timestamps): https://tools.ietf.org/html/rfc3339.

Timestamps are converted to UTC timezone which we'll note in the timestamp with the "Z" syntax instead of the also accepted "+00:00". "Z" stands for UTC+0 or "Zulu time" and refers to the zone description of zero hours.

Timestamps are expressed as a date-time (not a Python datetime object), including the full date (the "T" between the date and the time is optional in RFC 3339 (but not in ISO 8601) and usually describes the beginning of the time part.

Timestamps are 27 characters long in the format: "YYYY-MM-DDTHH:mm:ss.ffffffZ". 4 digit year, 2 digit month, 2 digit days, "T", 2 digit hours, 2 digit minutes, 2 digit seconds, 6 fractional second digits (microseconds -> nanoseconds), followed by the timezone identifier for UTC: "Z".

The library is specified to return timestamps with 6 fractional second digits, which means timestamps down to the microsecond level. Having a six-digit fraction of a second is currently the most common way that timestamps are shown at this date.

See also:

# Transforms the input value to a string representing a date (YYYY-mm-dd) without timespec or indicated timezone.
# An optional `tz` argument can be used to return the date in the specific timezone.
utcnow.as_date_string(value, tz)

# Returns a string representing today's date (YYYY-mm-dd) without timespec or indicated timezone.
# An optional `tz` argument can be used to return today's date in the specific timezone.
utcnow.today(tz)

# Calculate the time difference between two timestamps.
utcnow.timediff(begin, end, unit)

Precision in modifier and utcnow.timediff

Added modifier and timediff support for milliseconds, microseconds and nanoseconds (although nanosecond precision will currently not use native nanoseconds, and will return microseconds * 1000).

Common modifier multipliers:

  • "s" (seconds)
  • "m" (minutes)
  • "h" (hours)
  • "d" (days)

Precision modifier multiplicers (new):

  • "ms" (milliseconds)
  • "us" (microseconds)
  • "ns" (nanoseconds)

Deprecation warnings

Added deprecation warnings to all other non-standard transformation functions (that will still work for the time being) – for example utcnow.as_str, utcnow.get_unixtime or utcnow.datetime, etc.


Refactoring

Major refactoring of the library to actually use a ModuleType instead of creating a class object (that was used as a faked module) when importing utcnow.

Previously

>>> import utcnow

>>> type(utcnow)
<class 'utcnow._module'>

>>> len(dir(utcnow))
197 (exported items from module / class)

After refactoring

>>> import utcnow

>>> type(utcnow)
<class 'module'>

>>> len(dir(utcnow))
23 (exported items from module)

Freeze the current time in utcnow with utcnow.synchronizer

There's a context manager available at utcnow.synchronizer to freeze the current time of utcnow to a specific value of your choice or to the current time when entering the context manager.

This has been added to accomodate for the use-case of needing to fetch the current time in different places as part of a call chain, where it's also either difficult or you're unable to pass the initial timestamp value as an argument down the chain, but you want to receive the exact same timestamp from utcnow to be returned for each call, although some microseconds would have passed since last call.

timestamp_before = utcnow.get()

with utcnow.synchronizer:
    # the current time (fetched through utcnow) is now frozen to the time when the
    # context manager was opened.
    timestamp_in_context = utcnow.get()

    # even when sleeping or awaiting, the time will stay frozen for as long as
    # we haven't exited the context.
    sleep(1)
    timestamp_1s_later = utcnow.get()  # same value as timestamp_in_context

timestamp_after = utcnow.get()

# timestamp_before      -> '2023-09-25T22:53:04.733040Z'
# timestamp_in_context  -> '2023-09-25T22:53:04.733076Z' (timestamp_before + 36µs)
# timestamp_1s_later    -> '2023-09-25T22:53:04.733076Z' (timestamp_before + 36µs)
# timestamp_after       -> '2023-09-25T22:53:05.738224Z' (timestamp_before + ~1s)

The utcnow.synchronizer(value, modifier) can also be initialized with a specific timestamp value to freeze the current time to, instead of the current time when entering the context manager. The same kind of arguments as for utcnow.rfc3339_timestamp() (utcnow.get()) can be used also for utcnow.synchronizer.

with utcnow.synchronizer("2000-01-01"):
    # the current time (fetched through utcnow) is now frozen to UTC midnight,
    # new years eve 2000.

    timestamp_in_context = utcnow.get()
    # '2000-01-01T00:00:00.000000Z'

    datetime_in_context = utcnow.as_datetime()
    # datetime.datetime(2000, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)

    unixtime_in_context = utcnow.as_unixtime()
    # 946684800.0

    timestamp_in_context_plus_24h = utcnow.get("now", "+24h")
    # '2000-01-02T00:00:00.000000Z'

An example of how utcnow.synchronizer can be used to freeze the creation time of an object and its children

A common use-case for utcnow.synchronizer is to freeze the creation time of an object and its children, for example when creating a bunch of objects in a chunk, if we are expected to apply the exact same creation time of all objects in the chunk using the same value.

SomeModel is a simple class that stores created_at when the object is initiated. Objects of the type may also contain a list of items, which in pratice is children of the same type. If creating a parent SomeModel object with two children stored to its items list all together in a chunk (for example as part of a storage API request), we may want to use the same timestamp for all three objects (the parent and the children).

class SomeModel:
    _created_at: str
    items: List[SomeModel]

    def __init__(self, items: Optional[List[SomeModel]] = None) -> None:
        self._created_at = utcnow.rfc3339_timestamp()
        self.items = items if items is not None else []

    @property
    def created_at(self) -> str:
        return self._created_at

    def __repr__(self) -> str:
        base_ = f"{type(self).__name__} [object at {hex(id(self))}], created_at='{self._created_at}'"
        if self.items:
            return f"<{base_}, items={self.items}>"
        return f"<{base_}>"


# without freezing the current time, the timestamps would be different for each item and
# the parent although they were created in the same chunk - this may be desired in a bunch
# of cases, but not always.

a = SomeModel(items=[
    SomeModel(),
    SomeModel(),
])
# a = <SomeModel [object at 0x103a01350], created_at='2023-09-25T23:35:50.371100Z', items=[
#     <SomeModel [object at 0x1039ff590], created_at='2023-09-25T23:35:50.371078Z'>,
#     <SomeModel [object at 0x103a01290], created_at='2023-09-25T23:35:50.371095Z'>
# ]>

with utcnow.synchronizer:
    b = SomeModel(items=[
        SomeModel(),
        SomeModel(),
    ])
# b = <SomeModel [object at 0x103a01350], created_at='2023-09-25T23:35:50.371100Z', items=[
#     <SomeModel [object at 0x1039ff590], created_at='2023-09-25T23:35:50.371100Z'>,
#     <SomeModel [object at 0x103a01290], created_at='2023-09-25T23:35:50.371100Z'>
# ]>

It's not possible to chain utcnow.synchronizer context managers to freeze the current time to different values at different points in the call chain. If a utcnow.synchronizer context is already opened a second attempt to create or open a context will result in a raised exception.

with utcnow.synchronizer:
    # this is ok
    with utcnow.synchronizer:
        # we'll never get here
        ...

# Traceback (most recent call last):
#   File "<stdin>", line 2, in <module>
#   File ".../.../.../utcnow/__init__.py", line 245, in __enter__
#     raise RuntimeError("'utcnow.synchronizer' context cannot be nested (library time already synchronized)")
# RuntimeError: 'utcnow.synchronizer' context cannot be nested (library time already synchronized)

@codecov-commenter
Copy link

codecov-commenter commented Sep 27, 2023

Codecov Report

All modified lines are covered by tests ✅

Comparison is base (dbd6977) 100.00% compared to head (1a819cf) 100.00%.
Report is 4 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff            @@
##            master      #133   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            8         8           
  Lines          549       568   +19     
=========================================
+ Hits           549       568   +19     
Files Coverage Δ
utcnow/__init__.py 100.00% <100.00%> (ø)
utcnow/interface.py 100.00% <100.00%> (ø)

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@kalaspuff kalaspuff merged commit 14c1b9b into master Sep 27, 2023
10 checks passed
@kalaspuff kalaspuff deleted the feature/refactoring branch September 27, 2023 11:24
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

Successfully merging this pull request may close these issues.

2 participants