dfdatetime 20260730__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dfdatetime/filetime.py ADDED
@@ -0,0 +1,164 @@
1
+ """FILETIME timestamp implementation."""
2
+
3
+ import decimal
4
+
5
+ from dfdatetime import definitions
6
+ from dfdatetime import factory
7
+ from dfdatetime import interface
8
+
9
+
10
+ class FiletimeEpoch(interface.DateTimeEpoch):
11
+ """FILETIME epoch."""
12
+
13
+ def __init__(self):
14
+ """Initializes a FILETIME epoch."""
15
+ super().__init__(1601, 1, 1)
16
+
17
+
18
+ class Filetime(interface.DateTimeValues):
19
+ """FILETIME timestamp.
20
+
21
+ The FILETIME timestamp is a 64-bit integer that contains the number
22
+ of 100th nano seconds since 1601-01-01 00:00:00.
23
+
24
+ Do not confuse this with the FILETIME structure that consists of
25
+ 2 x 32-bit integers and is presumed to be unsigned.
26
+
27
+ Attributes:
28
+ is_local_time (bool): True if the date and time value is in local time.
29
+ """
30
+
31
+ _EPOCH = FiletimeEpoch()
32
+
33
+ # The difference between January 1, 1601 and January 1, 1970 in seconds.
34
+ _FILETIME_TO_POSIX_BASE = 11644473600
35
+
36
+ def __init__(self, precision=None, time_zone_offset=None, timestamp=None):
37
+ """Initializes a FILETIME timestamp.
38
+
39
+ Args:
40
+ precision (Optional[str]): precision of the date and time value, which
41
+ should be one of the PRECISION_VALUES in definitions.
42
+ time_zone_offset (Optional[int]): time zone offset in number of minutes
43
+ from UTC or None if not set.
44
+ timestamp (Optional[int]): FILETIME timestamp.
45
+ """
46
+ super().__init__(
47
+ precision=precision or definitions.PRECISION_100_NANOSECONDS,
48
+ time_zone_offset=time_zone_offset,
49
+ )
50
+ self._timestamp = timestamp
51
+
52
+ @property
53
+ def timestamp(self):
54
+ """int: FILETIME timestamp or None if not set."""
55
+ return self._timestamp
56
+
57
+ def _GetNormalizedTimestamp(self):
58
+ """Retrieves the normalized timestamp.
59
+
60
+ Returns:
61
+ decimal.Decimal: normalized timestamp, which contains the number of
62
+ seconds since January 1, 1970 00:00:00 and a fraction of second used
63
+ for increased precision, or None if the normalized timestamp cannot be
64
+ determined.
65
+ """
66
+ if self._normalized_timestamp is None:
67
+ if (
68
+ self._timestamp is not None
69
+ and self._timestamp >= 0
70
+ and self._timestamp <= self._UINT64_MAX
71
+ ):
72
+ self._normalized_timestamp = (
73
+ decimal.Decimal(self._timestamp) / self._100_NANOSECONDS_PER_SECOND
74
+ )
75
+ self._normalized_timestamp -= self._FILETIME_TO_POSIX_BASE
76
+
77
+ if self._time_zone_offset:
78
+ self._normalized_timestamp -= self._time_zone_offset * 60
79
+
80
+ return self._normalized_timestamp
81
+
82
+ def CopyFromDateTimeString(self, time_string):
83
+ """Copies a FILETIME timestamp from a date and time string.
84
+
85
+ Args:
86
+ time_string (str): date and time value formatted as:
87
+ YYYY-MM-DD hh:mm:ss.######[+-]##:##
88
+
89
+ Where # are numeric digits ranging from 0 to 9 and the seconds
90
+ fraction can be either 3, 6 or 9 digits. The time of day, seconds
91
+ fraction and time zone offset are optional. The default time zone
92
+ is UTC.
93
+
94
+ Raises:
95
+ ValueError: if the time string is invalid or not supported.
96
+ """
97
+ date_time_values = self._CopyDateTimeFromString(time_string)
98
+
99
+ year = date_time_values.get("year", 0)
100
+ month = date_time_values.get("month", 0)
101
+ day_of_month = date_time_values.get("day_of_month", 0)
102
+ hours = date_time_values.get("hours", 0)
103
+ minutes = date_time_values.get("minutes", 0)
104
+ seconds = date_time_values.get("seconds", 0)
105
+ nanoseconds = date_time_values.get("nanoseconds", 0)
106
+ time_zone_offset = date_time_values.get("time_zone_offset")
107
+
108
+ if year < 1601:
109
+ raise ValueError(f"Year value not supported: {year!s}.")
110
+
111
+ nanoseconds, _ = divmod(nanoseconds, 100)
112
+
113
+ timestamp = self._GetNumberOfSecondsFromElements(
114
+ year, month, day_of_month, hours, minutes, seconds
115
+ )
116
+ timestamp += self._FILETIME_TO_POSIX_BASE
117
+ timestamp *= self._100_NANOSECONDS_PER_SECOND
118
+ timestamp += nanoseconds
119
+
120
+ self._normalized_timestamp = None
121
+ self._timestamp = timestamp
122
+ self._time_zone_offset = time_zone_offset
123
+
124
+ def CopyToDateTimeString(self):
125
+ """Copies the FILETIME timestamp to a date and time string.
126
+
127
+ Returns:
128
+ str: date and time value formatted as: "YYYY-MM-DD hh:mm:ss.#######" or
129
+ None if the timestamp is missing or invalid.
130
+ """
131
+ if (
132
+ self._timestamp is None
133
+ or self._timestamp < 0
134
+ or self._timestamp > self._UINT64_MAX
135
+ ):
136
+ return None
137
+
138
+ timestamp, fraction_of_second = divmod(
139
+ self._timestamp, self._100_NANOSECONDS_PER_SECOND
140
+ )
141
+ number_of_days, hours, minutes, seconds = self._GetTimeValues(timestamp)
142
+
143
+ year, month, day_of_month = self._GetDateValuesWithEpoch(
144
+ number_of_days, self._EPOCH
145
+ )
146
+ return (
147
+ f"{year:04d}-{month:02d}-{day_of_month:02d} "
148
+ f"{hours:02d}:{minutes:02d}:{seconds:02d}.{fraction_of_second:07d}"
149
+ )
150
+
151
+ def CopyToSerializableDict(self):
152
+ """Copies the date time value to a serializable dictionary.
153
+
154
+ Returns:
155
+ dict[str, object]: serializable dictionary.
156
+ """
157
+ serializable_dict = self._CreateSerializableDict()
158
+
159
+ serializable_dict["timestamp"] = self._timestamp
160
+
161
+ return serializable_dict
162
+
163
+
164
+ factory.Factory.RegisterDateTimeValues(Filetime)
@@ -0,0 +1,234 @@
1
+ """Golang time.Time timestamp implementation."""
2
+
3
+ import decimal
4
+ import struct
5
+
6
+ from dfdatetime import definitions
7
+ from dfdatetime import factory
8
+ from dfdatetime import interface
9
+
10
+
11
+ class GolangTimeEpoch(interface.DateTimeEpoch):
12
+ """Golang time.Time epoch."""
13
+
14
+ def __init__(self):
15
+ """Initializes a Golang time.Time epoch."""
16
+ super().__init__(1, 1, 1)
17
+
18
+
19
+ class GolangTime(interface.DateTimeValues):
20
+ """Golang time.Time timestamp.
21
+
22
+ A Golang time.Time timestamp contains the number of nanoseconds since
23
+ January 1, 1 UTC. Depending on the version of the timestamp, the time
24
+ zone is stored in minutes or seconds relative to UTC.
25
+
26
+ A serialized version 1 Golang time.Time timestamp is a 15 byte value
27
+ that consists of:
28
+
29
+ * byte 0 - version as an 8-bit integer.
30
+ * bytes 1-8 - number of seconds since January 1, 1 as a big-endian signed
31
+ integer.
32
+ * bytes 9-12 - fraction of second, number of nanoseconds as a big-endian
33
+ signed integer.
34
+ * bytes 13-14 - time zone offset in minutes as a 16-bit big-endian integer,
35
+ where -1 represents UTC.
36
+
37
+ A serialized version 2 Golang time.Time timestamp is a 16 byte value
38
+ that consists of:
39
+
40
+ * byte 0 - version as an 8-bit integer.
41
+ * bytes 1-8 - number of seconds since January 1, 1 as a big-endian signed
42
+ integer.
43
+ * bytes 9-12 - fraction of second, number of nanoseconds as a big-endian
44
+ signed integer.
45
+ * bytes 13-14 - time zone offset in minutes as a 16-bit big-endian integer,
46
+ where -1 represents UTC.
47
+ * byte 15 - time zone offset in seconds as an 8-bit integer.
48
+
49
+ Attributes:
50
+ is_local_time (bool): True if the date and time value is in local time
51
+ """
52
+
53
+ # The delta between January 1, 1970 (unix epoch) and January 1, 1
54
+ # (Golang epoch).
55
+ _GOLANG_TO_POSIX_BASE = (
56
+ (1969 * 365) + (1969 // 4) - (1969 // 100) + (1969 // 400)
57
+ ) * definitions.SECONDS_PER_DAY
58
+
59
+ _EPOCH = GolangTimeEpoch()
60
+
61
+ def __init__(self, golang_timestamp=None, precision=None):
62
+ """Initializes a Golang time.Time timestamp.
63
+
64
+ Args:
65
+ golang_timestamp (Optional[bytes]): the Golang time.Time timestamp.
66
+ precision (Optional[str]): precision of the date and time value, which
67
+ should be one of the PRECISION_VALUES in definitions.
68
+ """
69
+ number_of_seconds, nanoseconds, time_zone_offset = (None, None, None)
70
+ if golang_timestamp is not None:
71
+ number_of_seconds, nanoseconds, time_zone_offset = self._GetNumberOfSeconds(
72
+ golang_timestamp
73
+ )
74
+
75
+ super().__init__(
76
+ precision=precision or definitions.PRECISION_1_NANOSECOND,
77
+ time_zone_offset=time_zone_offset,
78
+ )
79
+ self._golang_timestamp = golang_timestamp
80
+ self._nanoseconds = nanoseconds
81
+ self._number_of_seconds = number_of_seconds
82
+
83
+ @property
84
+ def golang_timestamp(self):
85
+ """bytes: Golang time.Time timestamp or None if not set."""
86
+ return self._golang_timestamp
87
+
88
+ def _GetNormalizedTimestamp(self):
89
+ """Retrieves the normalized timestamp.
90
+
91
+ Returns:
92
+ decimal.Decimal: normalized timestamp, which contains the number of
93
+ seconds since January 1, 1970 00:00:00 and a fraction of second used
94
+ for increased precision, or None if the normalized timestamp cannot be
95
+ determined.
96
+ """
97
+ if self._normalized_timestamp is None:
98
+ if (
99
+ self._number_of_seconds is not None
100
+ and self._number_of_seconds >= self._GOLANG_TO_POSIX_BASE
101
+ and self._nanoseconds is not None
102
+ and self._nanoseconds >= 0
103
+ ):
104
+ self._normalized_timestamp = decimal.Decimal(
105
+ self._number_of_seconds - GolangTime._GOLANG_TO_POSIX_BASE
106
+ )
107
+ if self._nanoseconds is not None and self._nanoseconds >= 0:
108
+ self._normalized_timestamp += (
109
+ decimal.Decimal(self._nanoseconds)
110
+ / definitions.NANOSECONDS_PER_SECOND
111
+ )
112
+
113
+ if self._time_zone_offset:
114
+ self._normalized_timestamp -= self._time_zone_offset * 60
115
+
116
+ return self._normalized_timestamp
117
+
118
+ def _GetNumberOfSeconds(self, golang_timestamp):
119
+ """Retrieves the number of seconds from a Golang time.Time timestamp.
120
+
121
+ Args:
122
+ golang_timestamp (bytes): the Golang time.Time timestamp.
123
+
124
+ Returns:
125
+ tuple[int, int, int]: number of seconds since January 1, 1 00:00:00,
126
+ fraction of second in nanoseconds and time zone offset in minutes.
127
+
128
+ Raises:
129
+ ValueError: if the Golang time.Time timestamp could not be parsed.
130
+ """
131
+ byte_size = len(golang_timestamp)
132
+ if byte_size < 15:
133
+ raise ValueError("Unsupported Golang time.Time timestamp.")
134
+
135
+ version = golang_timestamp[0]
136
+ if version not in (1, 2):
137
+ raise ValueError(
138
+ f"Unsupported Golang time.Time timestamp version: {version:d}."
139
+ )
140
+
141
+ if (version == 1 and byte_size != 15) or (version == 2 and byte_size != 16):
142
+ raise ValueError("Unsupported Golang time.Time timestamp.")
143
+
144
+ try:
145
+ number_of_seconds, nanoseconds, time_zone_offset = struct.unpack(
146
+ ">qih", golang_timestamp[1:15]
147
+ )
148
+ # TODO: add support for version 2 time zone offset in seconds
149
+
150
+ except (TypeError, struct.error) as exception:
151
+ raise ValueError(
152
+ f"Unable to unpacked Golang time.Time timestamp with error: "
153
+ f"{exception!s}"
154
+ )
155
+
156
+ # A time zone offset of -1 minute is a special representation for UTC.
157
+ if time_zone_offset == -1:
158
+ time_zone_offset = 0
159
+
160
+ return number_of_seconds, nanoseconds, time_zone_offset
161
+
162
+ def CopyFromDateTimeString(self, time_string):
163
+ """Copies a date time value from a date and time string.
164
+
165
+ Args:
166
+ time_string (str): date and time value formatted as:
167
+ YYYY-MM-DD hh:mm:ss.######[+-]##:##
168
+
169
+ Where # are numeric digits ranging from 0 to 9 and the seconds
170
+ fraction can be either 3, 6 or 9 digits. The time of day, seconds
171
+ fraction and time zone offset are optional. The default time zone
172
+ is UTC.
173
+
174
+ Raises:
175
+ ValueError: if the time string is invalid or not supported.
176
+ """
177
+ date_time_values = self._CopyDateTimeFromString(time_string)
178
+ year = date_time_values.get("year", 0)
179
+ month = date_time_values.get("month", 0)
180
+ day_of_month = date_time_values.get("day_of_month", 0)
181
+ hours = date_time_values.get("hours", 0)
182
+ minutes = date_time_values.get("minutes", 0)
183
+ seconds = date_time_values.get("seconds", 0)
184
+ nanoseconds = date_time_values.get("nanoseconds", 0)
185
+ time_zone_offset = date_time_values.get("time_zone_offset")
186
+
187
+ if year < 0:
188
+ raise ValueError(f"Year value not supported: {year!s}.")
189
+
190
+ seconds = self._GetNumberOfSecondsFromElements(
191
+ year, month, day_of_month, hours, minutes, seconds
192
+ )
193
+ seconds += self._GOLANG_TO_POSIX_BASE
194
+
195
+ self._normalized_timestamp = None
196
+ self._number_of_seconds = seconds
197
+ self._nanoseconds = nanoseconds
198
+ self._time_zone_offset = time_zone_offset
199
+
200
+ def CopyToDateTimeString(self):
201
+ """Copies the Golang time value to a date and time string.
202
+
203
+ Returns:
204
+ str: date and time value formatted as: "YYYY-MM-DD hh:mm:ss.######" or
205
+ None if the timestamp cannot be copied to a date and time string.
206
+ """
207
+ if self._number_of_seconds is None or self._number_of_seconds < 0:
208
+ return None
209
+
210
+ number_of_days, hours, minutes, seconds = self._GetTimeValues(
211
+ self._number_of_seconds
212
+ )
213
+ year, month, day_of_month = self._GetDateValuesWithEpoch(
214
+ number_of_days, self._EPOCH
215
+ )
216
+ return (
217
+ f"{year:04d}-{month:02d}-{day_of_month:02d} "
218
+ f"{hours:02d}:{minutes:02d}:{seconds:02d}.{self._nanoseconds:09d}"
219
+ )
220
+
221
+ def CopyToSerializableDict(self):
222
+ """Copies the date time value to a serializable dictionary.
223
+
224
+ Returns:
225
+ dict[str, object]: serializable dictionary.
226
+ """
227
+ return {
228
+ "__class_name__": type(self).__name__,
229
+ "__type__": "DateTimeValues",
230
+ "golang_timestamp": self._golang_timestamp,
231
+ }
232
+
233
+
234
+ factory.Factory.RegisterDateTimeValues(GolangTime)
dfdatetime/hfs_time.py ADDED
@@ -0,0 +1,150 @@
1
+ """HFS time implementation."""
2
+
3
+ import decimal
4
+
5
+ from dfdatetime import definitions
6
+ from dfdatetime import factory
7
+ from dfdatetime import interface
8
+
9
+
10
+ class HFSTimeEpoch(interface.DateTimeEpoch):
11
+ """HFS time epoch."""
12
+
13
+ def __init__(self):
14
+ """Initializes a HFS time epoch."""
15
+ super().__init__(1904, 1, 1)
16
+
17
+
18
+ class HFSTime(interface.DateTimeValues):
19
+ """HFS timestamp.
20
+
21
+ The HFS timestamp is an unsigned 32-bit integer that contains the number of
22
+ seconds since 1904-01-01 00:00:00. Where in HFS the timestamp is typically
23
+ in local time and in HFS+/HFSX in UTC.
24
+
25
+ Attributes:
26
+ is_local_time (bool): True if the date and time value is in local time.
27
+ """
28
+
29
+ _EPOCH = HFSTimeEpoch()
30
+
31
+ # The difference between Jan 1, 1904 and Jan 1, 1970 in seconds.
32
+ _HFS_TO_POSIX_BASE = 2082844800
33
+
34
+ def __init__(self, precision=None, time_zone_offset=None, timestamp=None):
35
+ """Initializes a HFS timestamp.
36
+
37
+ Args:
38
+ precision (Optional[str]): precision of the date and time value, which
39
+ should be one of the PRECISION_VALUES in definitions.
40
+ time_zone_offset (Optional[int]): time zone offset in number of minutes
41
+ from UTC or None if not set.
42
+ timestamp (Optional[int]): HFS timestamp.
43
+ """
44
+ super().__init__(
45
+ precision=precision or definitions.PRECISION_1_SECOND,
46
+ time_zone_offset=time_zone_offset,
47
+ )
48
+ self._timestamp = timestamp
49
+
50
+ @property
51
+ def timestamp(self):
52
+ """int: HFS timestamp or None if not set."""
53
+ return self._timestamp
54
+
55
+ def _GetNormalizedTimestamp(self):
56
+ """Retrieves the normalized timestamp.
57
+
58
+ Returns:
59
+ decimal.Decimal: normalized timestamp, which contains the number of
60
+ seconds since January 1, 1970 00:00:00 and a fraction of second used
61
+ for increased precision, or None if the normalized timestamp cannot be
62
+ determined.
63
+ """
64
+ if self._normalized_timestamp is None:
65
+ if (
66
+ self._timestamp is not None
67
+ and self._timestamp >= 0
68
+ and self._timestamp <= self._UINT32_MAX
69
+ ):
70
+ self._normalized_timestamp = (
71
+ decimal.Decimal(self._timestamp) - self._HFS_TO_POSIX_BASE
72
+ )
73
+ if self._time_zone_offset:
74
+ self._normalized_timestamp -= self._time_zone_offset * 60
75
+
76
+ return self._normalized_timestamp
77
+
78
+ def CopyFromDateTimeString(self, time_string):
79
+ """Copies a HFS timestamp from a date and time string.
80
+
81
+ Args:
82
+ time_string (str): date and time value formatted as:
83
+ YYYY-MM-DD hh:mm:ss.######[+-]##:##
84
+
85
+ Where # are numeric digits ranging from 0 to 9 and the seconds
86
+ fraction can be either 3, 6 or 9 digits. The time of day, seconds
87
+ fraction and time zone offset are optional. The default time zone
88
+ is UTC.
89
+
90
+ Raises:
91
+ ValueError: if the time string is invalid or not supported.
92
+ """
93
+ date_time_values = self._CopyDateTimeFromString(time_string)
94
+
95
+ year = date_time_values.get("year", 0)
96
+ month = date_time_values.get("month", 0)
97
+ day_of_month = date_time_values.get("day_of_month", 0)
98
+ hours = date_time_values.get("hours", 0)
99
+ minutes = date_time_values.get("minutes", 0)
100
+ seconds = date_time_values.get("seconds", 0)
101
+ time_zone_offset = date_time_values.get("time_zone_offset")
102
+
103
+ if year < 1904 or year > 2040:
104
+ raise ValueError("Year value not supported.")
105
+
106
+ self._normalized_timestamp = None
107
+ self._timestamp = self._GetNumberOfSecondsFromElements(
108
+ year, month, day_of_month, hours, minutes, seconds
109
+ )
110
+ self._timestamp += self._HFS_TO_POSIX_BASE
111
+ self._time_zone_offset = time_zone_offset
112
+
113
+ def CopyToDateTimeString(self):
114
+ """Copies the HFS timestamp to a date and time string.
115
+
116
+ Returns:
117
+ str: date and time value formatted as: "YYYY-MM-DD hh:mm:ss" or None
118
+ if the timestamp is missing or invalid.
119
+ """
120
+ if (
121
+ self._timestamp is None
122
+ or self._timestamp < 0
123
+ or self._timestamp > self._UINT32_MAX
124
+ ):
125
+ return None
126
+
127
+ number_of_days, hours, minutes, seconds = self._GetTimeValues(self._timestamp)
128
+
129
+ year, month, day_of_month = self._GetDateValuesWithEpoch(
130
+ number_of_days, self._EPOCH
131
+ )
132
+ return (
133
+ f"{year:04d}-{month:02d}-{day_of_month:02d} "
134
+ f"{hours:02d}:{minutes:02d}:{seconds:02d}"
135
+ )
136
+
137
+ def CopyToSerializableDict(self):
138
+ """Copies the date time value to a serializable dictionary.
139
+
140
+ Returns:
141
+ dict[str, object]: serializable dictionary.
142
+ """
143
+ serializable_dict = self._CreateSerializableDict()
144
+
145
+ serializable_dict["timestamp"] = self._timestamp
146
+
147
+ return serializable_dict
148
+
149
+
150
+ factory.Factory.RegisterDateTimeValues(HFSTime)