2 # NT Time utility functions.
4 # Copyright (C) Catalyst.Net Ltd 2023
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <https://www.gnu.org/licenses/>.
21 from typing
import NewType
, Optional
25 NtTime
= NewType("NtTime", int)
26 NtTimeDelta
= NewType("NtTimeDelta", int)
28 NT_TIME_MAX
= NtTime((1 << 64) - 1)
30 NT_EPOCH
= datetime
.datetime(1601, 1, 1, 0, 0, 0, 0, tzinfo
=datetime
.timezone
.utc
)
31 NT_TICKS_PER_μSEC
= 10
32 NT_TICKS_PER_SEC
= NT_TICKS_PER_μSEC
* 1_000_000
35 def _validate_nt_time(nt_time
: NtTime
) -> None:
36 if not isinstance(nt_time
, int):
37 raise ValueError(f
"{nt_time} is not an integer")
38 if not 0 <= nt_time
<= NT_TIME_MAX
:
39 raise ValueError(f
"{nt_time} is out of range")
42 def nt_time_from_datetime(tm
: datetime
.datetime
) -> NtTime
:
43 time_since_epoch
= tm
- NT_EPOCH
44 nt_time
= NtTime(round(time_since_epoch
.total_seconds() * NT_TICKS_PER_SEC
))
45 _validate_nt_time(nt_time
)
49 def nt_now() -> NtTime
:
50 dt
= datetime
.datetime
.now(datetime
.timezone
.utc
)
51 return nt_time_from_datetime(dt
)
54 def datetime_from_nt_time(nt_time
: NtTime
) -> datetime
.datetime
:
55 _validate_nt_time(nt_time
)
56 time_since_epoch
= datetime
.timedelta(microseconds
=nt_time
/ NT_TICKS_PER_μSEC
)
57 return NT_EPOCH
+ time_since_epoch
60 def nt_time_delta_from_timedelta(dt
: datetime
.timedelta
) -> NtTimeDelta
:
61 return NtTimeDelta(round(dt
.total_seconds() * NT_TICKS_PER_SEC
))
64 def timedelta_from_nt_time_delta(nt_time_delta
: NtTimeDelta
) -> datetime
.timedelta
:
65 return datetime
.timedelta(microseconds
=nt_time_delta
/ NT_TICKS_PER_μSEC
)
68 def nt_time_from_string(s
: str) -> NtTime
:
69 """Convert a subset of ISO 8601 date/time strings, ldap timestamps,
70 and the string 'now' into NT time.
76 which is 14 digits followed by the fixed string '.0Z'. This is
77 used in LDIF and internally by ldb.
81 YYYY-mm-dd[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]]
83 where the '*' can be any character, and the optional last
84 '[+HH:MM[:SS[.ffffff]]]' is a timezone offset (use '+00:00' for
89 dt
= datetime
.datetime
.now(datetime
.timezone
.utc
)
90 elif re
.match(r
"^\d{14}\.0Z$", s
):
92 dt
= datetime
.datetime
.strptime(s
, "%Y%m%d%H%M%S.0Z")
94 dt
= datetime
.datetime
.fromisoformat(s
)
97 "Expected a date in either "
98 "ISO8601 'YYYY-MM-DD HH:MM:SS' format, "
99 "LDAP timestamp 'YYYYmmddHHMMSS.0Z', "
100 "or the literal string 'now'. "
104 if dt
.tzinfo
is None:
105 # This is a cursed timestamp with no timezone info. We have to
106 # guess or nt_time_from_datetime() will fail. The best guess
107 # is the system timezone, which we can get this way:
110 return nt_time_from_datetime(dt
)
113 def string_from_nt_time(nttime
: NtTime
, format
: Optional
[str] = None) -> str:
114 """Format an NtTime date as a string.
116 If format is not provided, an ISO 8601 string is used.
118 dt
= datetime_from_nt_time(nttime
)
120 if format
is not None:
121 return dt
.strftime(format
)
123 return dt
.isoformat()