edq.util.time
1import datetime 2import re 3import time 4import typing 5 6PRETTY_SHORT_FORMAT: str = '%Y-%m-%d %H:%M' 7""" 8The format string for a pretty timestamp. 9See: https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior 10""" 11 12DEFAULT_EMBEDDED_PATTERN: str = r'<timestamp:(-?\d+|nil)>' 13""" A regex for matching an embedded timestamp. """ 14 15UTC: datetime.timezone = datetime.timezone.utc 16""" A shortcut for the UTC timezone. """ 17 18UNIXTIME_THRESHOLD_SECS: int = int(1e10) 19""" Epoch time guessing threshold for seconds. """ 20 21UNIXTIME_THRESHOLD_MSECS: int = int(1e13) 22""" Epoch time guessing threshold for milliseconds. """ 23 24UNIXTIME_THRESHOLD_USECS: int = int(1e16) 25""" Epoch time guessing threshold for nanoseconds. """ 26 27_testing_timezone: typing.Union[datetime.timezone, None] = None # pylint: disable=invalid-name 28""" A timezone to use for testing. """ 29 30def set_testing_local_timezone(timezone: typing.Union[datetime.timezone, None] = UTC): 31 """ 32 Force the local timezone to be a specific value (UTC by default). 33 This will only affect this package (e.g., the stdlib will not be affected). 34 """ 35 36 global _testing_timezone # pylint: disable=global-statement 37 _testing_timezone = timezone 38 39class Duration(int): 40 """ 41 A Duration represents some length in time in milliseconds. 42 """ 43 44 def to_secs(self) -> float: 45 """ Convert the duration to float seconds. """ 46 47 return self / 1000.0 48 49 def to_msecs(self) -> int: 50 """ Convert the duration to integer milliseconds. """ 51 52 return self 53 54class Timestamp(int): 55 """ 56 A Timestamp represent a moment in time (sometimes called "datetimes"). 57 Timestamps are internally represented by the number of milliseconds since the 58 (Unix Epoch)[https://en.wikipedia.org/wiki/Unix_time]. 59 This is sometimes referred to as "Unix Time". 60 Since Unix Time is in UTC, timestamps do not need to carry timestamp information with them. 61 62 Note that timestamps are just integers with some decoration, 63 so they respond to all normal int functionality. 64 """ 65 66 def sub(self, other: 'Timestamp') -> Duration: 67 """ Return a new duration that is the difference of this and the given duration. """ 68 69 return Duration(self - other) 70 71 def to_pytime(self, timezone: typing.Union[datetime.timezone, None] = None) -> datetime.datetime: 72 """ Convert this timestamp to a Python datetime in the given timezone (local by default). """ 73 74 if (timezone is None): 75 timezone = get_local_timezone() 76 77 return datetime.datetime.fromtimestamp(self / 1000, timezone) 78 79 def to_local_pytime(self) -> datetime.datetime: 80 """ Convert this timestamp to a Python datetime in the system timezone. """ 81 82 return self.to_pytime(timezone = get_local_timezone()) 83 84 def pretty(self, short: bool = False, timezone: typing.Union[datetime.timezone, None] = None) -> str: 85 """ 86 Get a "pretty" string representation of this timestamp. 87 There is no guarantee that this representation can be parsed back to its original form. 88 89 If no timezone is provided, the system's local timezone will be used. 90 """ 91 92 if (timezone is None): 93 timezone = get_local_timezone() 94 95 pytime = self.to_pytime(timezone = timezone) 96 97 if (short): 98 return pytime.strftime(PRETTY_SHORT_FORMAT) 99 100 return pytime.isoformat(timespec = 'milliseconds') 101 102 @staticmethod 103 def from_pytime(pytime: datetime.datetime) -> 'Timestamp': 104 """ Convert a Python datetime to a timestamp. """ 105 106 return Timestamp(int(pytime.timestamp() * 1000)) 107 108 @staticmethod 109 def now() -> 'Timestamp': 110 """ Get a Timestamp that represents the current moment. """ 111 112 return Timestamp(time.time() * 1000) 113 114 @staticmethod 115 def convert_embedded( 116 text: str, 117 embedded_pattern: str = DEFAULT_EMBEDDED_PATTERN, 118 pretty: bool = False, 119 short: bool = True, 120 timezone: typing.Union[datetime.timezone, None] = None, 121 ) -> str: 122 """ 123 Look for any timestamps embedded in the text and replace them. 124 """ 125 126 while True: 127 match = re.search(embedded_pattern, text) 128 if (match is None): 129 break 130 131 initial_text = match.group(0) 132 timestamp_text = match.group(1) 133 134 timestamp = Timestamp() 135 if (timestamp_text != 'nil'): 136 timestamp = Timestamp(int(timestamp_text)) 137 138 replacement_text = str(timestamp) 139 if (pretty): 140 replacement_text = timestamp.pretty(short = short, timezone = timezone) 141 142 text = text.replace(initial_text, replacement_text) 143 144 return text 145 146 @staticmethod 147 def guess(value: typing.Any) -> 'Timestamp': 148 """ 149 Try to parse a timestamp out of a value. 150 Empty values will get zero timestamps. 151 Purely digit strings will be converted to ints and treated as UNIX times. 152 Floats will be considered UNIX epoch seconds and converted to milliseconds. 153 Other strings will be attempted to be parsed with datetime.fromisoformat(). 154 """ 155 156 raw_value = value 157 158 # Empty timestamp. 159 if (value is None): 160 return Timestamp(0) 161 162 # Check for already parsed timestamps. 163 if (isinstance(value, Timestamp)): 164 return value 165 166 # Floats are assumed to be epoch seconds. 167 if (isinstance(value, float)): 168 value = int(1000 * value) 169 170 # At this point, we only want to be dealing with strings or ints. 171 if (not isinstance(value, (int, str))): 172 value = str(value) 173 174 # Check for string specifics. 175 if (isinstance(value, str)): 176 # Check for empty strings. 177 value = value.strip() 178 if (len(value) == 0): 179 return Timestamp(0) 180 181 # Check for digit or float strings. 182 if (re.match(r'^\d+\.\d+$', value) is not None): 183 value = int(1000 * float(value)) 184 elif (re.match(r'^\d+$', value) is not None): 185 value = int(value) 186 187 if (isinstance(value, int)): 188 # Use reasonable thresholds to guess the units of the value (sec, msec, usec, nsec). 189 if (value < UNIXTIME_THRESHOLD_SECS): 190 # Time is in seconds. 191 return Timestamp(value * 1000) 192 elif (value < UNIXTIME_THRESHOLD_MSECS): 193 # Time is in milliseconds. 194 return Timestamp(value) 195 elif (value < UNIXTIME_THRESHOLD_USECS): 196 # Time is in microseconds. 197 return Timestamp(value / 1000) 198 else: 199 # Time is in nanoseconds. 200 return Timestamp(value / 1000 / 1000) 201 202 # Try to convert from an ISO string. 203 204 # Parse out some cases that Python <= 3.10 cannot deal with. 205 # This will remove fractional seconds. 206 value = re.sub(r'Z$', '+00:00', value) 207 value = re.sub(r'(\d\d:\d\d)(\.\d+)', r'\1', value) 208 209 try: 210 value = datetime.datetime.fromisoformat(value) 211 except Exception as ex: 212 raise ValueError(f"Failed to parse timestamp string '{raw_value}'.") from ex 213 214 return Timestamp.from_pytime(value) 215 216def get_local_timezone() -> datetime.timezone: 217 """ Get the local (system) timezone or raise an exception. """ 218 219 if (_testing_timezone is not None): 220 return _testing_timezone 221 222 local_timezone = datetime.datetime.now().astimezone().tzinfo 223 if ((local_timezone is None) or (not isinstance(local_timezone, datetime.timezone))): 224 raise ValueError("Could not discover local timezone.") 225 226 return local_timezone
The format string for a pretty timestamp. See: https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
A regex for matching an embedded timestamp.
A shortcut for the UTC timezone.
Epoch time guessing threshold for seconds.
Epoch time guessing threshold for milliseconds.
Epoch time guessing threshold for nanoseconds.
31def set_testing_local_timezone(timezone: typing.Union[datetime.timezone, None] = UTC): 32 """ 33 Force the local timezone to be a specific value (UTC by default). 34 This will only affect this package (e.g., the stdlib will not be affected). 35 """ 36 37 global _testing_timezone # pylint: disable=global-statement 38 _testing_timezone = timezone
Force the local timezone to be a specific value (UTC by default). This will only affect this package (e.g., the stdlib will not be affected).
40class Duration(int): 41 """ 42 A Duration represents some length in time in milliseconds. 43 """ 44 45 def to_secs(self) -> float: 46 """ Convert the duration to float seconds. """ 47 48 return self / 1000.0 49 50 def to_msecs(self) -> int: 51 """ Convert the duration to integer milliseconds. """ 52 53 return self
A Duration represents some length in time in milliseconds.
55class Timestamp(int): 56 """ 57 A Timestamp represent a moment in time (sometimes called "datetimes"). 58 Timestamps are internally represented by the number of milliseconds since the 59 (Unix Epoch)[https://en.wikipedia.org/wiki/Unix_time]. 60 This is sometimes referred to as "Unix Time". 61 Since Unix Time is in UTC, timestamps do not need to carry timestamp information with them. 62 63 Note that timestamps are just integers with some decoration, 64 so they respond to all normal int functionality. 65 """ 66 67 def sub(self, other: 'Timestamp') -> Duration: 68 """ Return a new duration that is the difference of this and the given duration. """ 69 70 return Duration(self - other) 71 72 def to_pytime(self, timezone: typing.Union[datetime.timezone, None] = None) -> datetime.datetime: 73 """ Convert this timestamp to a Python datetime in the given timezone (local by default). """ 74 75 if (timezone is None): 76 timezone = get_local_timezone() 77 78 return datetime.datetime.fromtimestamp(self / 1000, timezone) 79 80 def to_local_pytime(self) -> datetime.datetime: 81 """ Convert this timestamp to a Python datetime in the system timezone. """ 82 83 return self.to_pytime(timezone = get_local_timezone()) 84 85 def pretty(self, short: bool = False, timezone: typing.Union[datetime.timezone, None] = None) -> str: 86 """ 87 Get a "pretty" string representation of this timestamp. 88 There is no guarantee that this representation can be parsed back to its original form. 89 90 If no timezone is provided, the system's local timezone will be used. 91 """ 92 93 if (timezone is None): 94 timezone = get_local_timezone() 95 96 pytime = self.to_pytime(timezone = timezone) 97 98 if (short): 99 return pytime.strftime(PRETTY_SHORT_FORMAT) 100 101 return pytime.isoformat(timespec = 'milliseconds') 102 103 @staticmethod 104 def from_pytime(pytime: datetime.datetime) -> 'Timestamp': 105 """ Convert a Python datetime to a timestamp. """ 106 107 return Timestamp(int(pytime.timestamp() * 1000)) 108 109 @staticmethod 110 def now() -> 'Timestamp': 111 """ Get a Timestamp that represents the current moment. """ 112 113 return Timestamp(time.time() * 1000) 114 115 @staticmethod 116 def convert_embedded( 117 text: str, 118 embedded_pattern: str = DEFAULT_EMBEDDED_PATTERN, 119 pretty: bool = False, 120 short: bool = True, 121 timezone: typing.Union[datetime.timezone, None] = None, 122 ) -> str: 123 """ 124 Look for any timestamps embedded in the text and replace them. 125 """ 126 127 while True: 128 match = re.search(embedded_pattern, text) 129 if (match is None): 130 break 131 132 initial_text = match.group(0) 133 timestamp_text = match.group(1) 134 135 timestamp = Timestamp() 136 if (timestamp_text != 'nil'): 137 timestamp = Timestamp(int(timestamp_text)) 138 139 replacement_text = str(timestamp) 140 if (pretty): 141 replacement_text = timestamp.pretty(short = short, timezone = timezone) 142 143 text = text.replace(initial_text, replacement_text) 144 145 return text 146 147 @staticmethod 148 def guess(value: typing.Any) -> 'Timestamp': 149 """ 150 Try to parse a timestamp out of a value. 151 Empty values will get zero timestamps. 152 Purely digit strings will be converted to ints and treated as UNIX times. 153 Floats will be considered UNIX epoch seconds and converted to milliseconds. 154 Other strings will be attempted to be parsed with datetime.fromisoformat(). 155 """ 156 157 raw_value = value 158 159 # Empty timestamp. 160 if (value is None): 161 return Timestamp(0) 162 163 # Check for already parsed timestamps. 164 if (isinstance(value, Timestamp)): 165 return value 166 167 # Floats are assumed to be epoch seconds. 168 if (isinstance(value, float)): 169 value = int(1000 * value) 170 171 # At this point, we only want to be dealing with strings or ints. 172 if (not isinstance(value, (int, str))): 173 value = str(value) 174 175 # Check for string specifics. 176 if (isinstance(value, str)): 177 # Check for empty strings. 178 value = value.strip() 179 if (len(value) == 0): 180 return Timestamp(0) 181 182 # Check for digit or float strings. 183 if (re.match(r'^\d+\.\d+$', value) is not None): 184 value = int(1000 * float(value)) 185 elif (re.match(r'^\d+$', value) is not None): 186 value = int(value) 187 188 if (isinstance(value, int)): 189 # Use reasonable thresholds to guess the units of the value (sec, msec, usec, nsec). 190 if (value < UNIXTIME_THRESHOLD_SECS): 191 # Time is in seconds. 192 return Timestamp(value * 1000) 193 elif (value < UNIXTIME_THRESHOLD_MSECS): 194 # Time is in milliseconds. 195 return Timestamp(value) 196 elif (value < UNIXTIME_THRESHOLD_USECS): 197 # Time is in microseconds. 198 return Timestamp(value / 1000) 199 else: 200 # Time is in nanoseconds. 201 return Timestamp(value / 1000 / 1000) 202 203 # Try to convert from an ISO string. 204 205 # Parse out some cases that Python <= 3.10 cannot deal with. 206 # This will remove fractional seconds. 207 value = re.sub(r'Z$', '+00:00', value) 208 value = re.sub(r'(\d\d:\d\d)(\.\d+)', r'\1', value) 209 210 try: 211 value = datetime.datetime.fromisoformat(value) 212 except Exception as ex: 213 raise ValueError(f"Failed to parse timestamp string '{raw_value}'.") from ex 214 215 return Timestamp.from_pytime(value)
A Timestamp represent a moment in time (sometimes called "datetimes"). Timestamps are internally represented by the number of milliseconds since the (Unix Epoch)[https://en.wikipedia.org/wiki/Unix_time]. This is sometimes referred to as "Unix Time". Since Unix Time is in UTC, timestamps do not need to carry timestamp information with them.
Note that timestamps are just integers with some decoration, so they respond to all normal int functionality.
67 def sub(self, other: 'Timestamp') -> Duration: 68 """ Return a new duration that is the difference of this and the given duration. """ 69 70 return Duration(self - other)
Return a new duration that is the difference of this and the given duration.
72 def to_pytime(self, timezone: typing.Union[datetime.timezone, None] = None) -> datetime.datetime: 73 """ Convert this timestamp to a Python datetime in the given timezone (local by default). """ 74 75 if (timezone is None): 76 timezone = get_local_timezone() 77 78 return datetime.datetime.fromtimestamp(self / 1000, timezone)
Convert this timestamp to a Python datetime in the given timezone (local by default).
80 def to_local_pytime(self) -> datetime.datetime: 81 """ Convert this timestamp to a Python datetime in the system timezone. """ 82 83 return self.to_pytime(timezone = get_local_timezone())
Convert this timestamp to a Python datetime in the system timezone.
85 def pretty(self, short: bool = False, timezone: typing.Union[datetime.timezone, None] = None) -> str: 86 """ 87 Get a "pretty" string representation of this timestamp. 88 There is no guarantee that this representation can be parsed back to its original form. 89 90 If no timezone is provided, the system's local timezone will be used. 91 """ 92 93 if (timezone is None): 94 timezone = get_local_timezone() 95 96 pytime = self.to_pytime(timezone = timezone) 97 98 if (short): 99 return pytime.strftime(PRETTY_SHORT_FORMAT) 100 101 return pytime.isoformat(timespec = 'milliseconds')
Get a "pretty" string representation of this timestamp. There is no guarantee that this representation can be parsed back to its original form.
If no timezone is provided, the system's local timezone will be used.
103 @staticmethod 104 def from_pytime(pytime: datetime.datetime) -> 'Timestamp': 105 """ Convert a Python datetime to a timestamp. """ 106 107 return Timestamp(int(pytime.timestamp() * 1000))
Convert a Python datetime to a timestamp.
109 @staticmethod 110 def now() -> 'Timestamp': 111 """ Get a Timestamp that represents the current moment. """ 112 113 return Timestamp(time.time() * 1000)
Get a Timestamp that represents the current moment.
115 @staticmethod 116 def convert_embedded( 117 text: str, 118 embedded_pattern: str = DEFAULT_EMBEDDED_PATTERN, 119 pretty: bool = False, 120 short: bool = True, 121 timezone: typing.Union[datetime.timezone, None] = None, 122 ) -> str: 123 """ 124 Look for any timestamps embedded in the text and replace them. 125 """ 126 127 while True: 128 match = re.search(embedded_pattern, text) 129 if (match is None): 130 break 131 132 initial_text = match.group(0) 133 timestamp_text = match.group(1) 134 135 timestamp = Timestamp() 136 if (timestamp_text != 'nil'): 137 timestamp = Timestamp(int(timestamp_text)) 138 139 replacement_text = str(timestamp) 140 if (pretty): 141 replacement_text = timestamp.pretty(short = short, timezone = timezone) 142 143 text = text.replace(initial_text, replacement_text) 144 145 return text
Look for any timestamps embedded in the text and replace them.
147 @staticmethod 148 def guess(value: typing.Any) -> 'Timestamp': 149 """ 150 Try to parse a timestamp out of a value. 151 Empty values will get zero timestamps. 152 Purely digit strings will be converted to ints and treated as UNIX times. 153 Floats will be considered UNIX epoch seconds and converted to milliseconds. 154 Other strings will be attempted to be parsed with datetime.fromisoformat(). 155 """ 156 157 raw_value = value 158 159 # Empty timestamp. 160 if (value is None): 161 return Timestamp(0) 162 163 # Check for already parsed timestamps. 164 if (isinstance(value, Timestamp)): 165 return value 166 167 # Floats are assumed to be epoch seconds. 168 if (isinstance(value, float)): 169 value = int(1000 * value) 170 171 # At this point, we only want to be dealing with strings or ints. 172 if (not isinstance(value, (int, str))): 173 value = str(value) 174 175 # Check for string specifics. 176 if (isinstance(value, str)): 177 # Check for empty strings. 178 value = value.strip() 179 if (len(value) == 0): 180 return Timestamp(0) 181 182 # Check for digit or float strings. 183 if (re.match(r'^\d+\.\d+$', value) is not None): 184 value = int(1000 * float(value)) 185 elif (re.match(r'^\d+$', value) is not None): 186 value = int(value) 187 188 if (isinstance(value, int)): 189 # Use reasonable thresholds to guess the units of the value (sec, msec, usec, nsec). 190 if (value < UNIXTIME_THRESHOLD_SECS): 191 # Time is in seconds. 192 return Timestamp(value * 1000) 193 elif (value < UNIXTIME_THRESHOLD_MSECS): 194 # Time is in milliseconds. 195 return Timestamp(value) 196 elif (value < UNIXTIME_THRESHOLD_USECS): 197 # Time is in microseconds. 198 return Timestamp(value / 1000) 199 else: 200 # Time is in nanoseconds. 201 return Timestamp(value / 1000 / 1000) 202 203 # Try to convert from an ISO string. 204 205 # Parse out some cases that Python <= 3.10 cannot deal with. 206 # This will remove fractional seconds. 207 value = re.sub(r'Z$', '+00:00', value) 208 value = re.sub(r'(\d\d:\d\d)(\.\d+)', r'\1', value) 209 210 try: 211 value = datetime.datetime.fromisoformat(value) 212 except Exception as ex: 213 raise ValueError(f"Failed to parse timestamp string '{raw_value}'.") from ex 214 215 return Timestamp.from_pytime(value)
Try to parse a timestamp out of a value. Empty values will get zero timestamps. Purely digit strings will be converted to ints and treated as UNIX times. Floats will be considered UNIX epoch seconds and converted to milliseconds. Other strings will be attempted to be parsed with datetime.fromisoformat().
217def get_local_timezone() -> datetime.timezone: 218 """ Get the local (system) timezone or raise an exception. """ 219 220 if (_testing_timezone is not None): 221 return _testing_timezone 222 223 local_timezone = datetime.datetime.now().astimezone().tzinfo 224 if ((local_timezone is None) or (not isinstance(local_timezone, datetime.timezone))): 225 raise ValueError("Could not discover local timezone.") 226 227 return local_timezone
Get the local (system) timezone or raise an exception.