-
Notifications
You must be signed in to change notification settings - Fork 0
/
multizone.py
executable file
·259 lines (208 loc) · 7.68 KB
/
multizone.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2023 Kevin Meagher
#
# SPDX-License-Identifier: GPL-3.0-or-later
"""Command line tool for calculating times in multiple different timezones."""
from __future__ import annotations
__version__ = "0.1.0"
import argparse
import sys
import zoneinfo
from datetime import datetime, timedelta, tzinfo
from pathlib import Path
from typing import Any
import tabulate
import tzlocal
from babel.core import default_locale
from babel.dates import format_datetime
from termcolor import colored
if sys.version_info < (3, 10):
from xdg import xdg_config_home
else:
from xdg_base_dirs import xdg_config_home
if sys.version_info < (3, 11):
import tomli as tomllib
else:
import tomllib
ConfigType = dict[str, Any]
def get_arg(time_str: str, sep: str) -> list[int]:
"""Parse date or time with 1 or two delimiters."""
split = time_str.split(sep)
if len(split) in (2, 3):
return [int(x) for x in split]
raise ValueError
def parse_time_arg( # noqa: C901,PLR0912
time_arg: list[str],
default_time: datetime,
default_zone: tzinfo,
) -> datetime:
"""Parse the date time an time zone arguments."""
refdate = None
reftime = None
refzone: tzinfo | None = None
for arg in time_arg:
if refdate is None:
try:
refdate = get_arg(arg, "-")
continue
except ValueError:
pass
try:
refdate = get_arg(arg, "/")
continue
except ValueError:
pass
if reftime is None:
try:
reftime = get_arg(arg, ":")
continue
except ValueError:
pass
if refzone is None:
try:
refzone = zoneinfo.ZoneInfo(arg)
continue
except zoneinfo.ZoneInfoNotFoundError:
pass
mesg = f'I don\'t understand argument "{arg}"'
raise ValueError(mesg)
if not refzone:
refzone = default_zone
ref_dt = default_time.astimezone(refzone)
if refdate:
if len(refdate) == 2: # noqa: PLR2004
refdate.insert(0, ref_dt.year)
else:
refdate = [ref_dt.year, ref_dt.month, ref_dt.day]
if reftime:
if len(reftime) == 2: # noqa: PLR2004
reftime.append(0)
else:
reftime = [ref_dt.hour, ref_dt.minute, ref_dt.second]
return datetime(refdate[0], refdate[1], refdate[2], reftime[0], reftime[1], reftime[2], tzinfo=refzone)
def parse_args() -> argparse.Namespace:
"""Parse command line arguments with argparse."""
parser = argparse.ArgumentParser(description="Convert localtime to other timezones")
parser.add_argument("time", nargs="*", help="Time to convert (Default: Now)")
parser.add_argument("--config", help="Configuration file")
parser.add_argument(
"--format",
"-f",
help="Output format to use for times, same as used by strftime",
)
parser.add_argument("--zones", "-z", nargs="+", help="List of timezones to print out that time")
parser.add_argument("--list", action="store_true", help="Print a list of all timezones and exit")
parser.add_argument("-l", "--locale")
args = parser.parse_args()
if args.list:
for avail_zones in sorted(zoneinfo.available_timezones()):
print(avail_zones) # noqa: T201
sys.exit(0)
return args
def load_config(config_path: Path | None) -> ConfigType:
"""Read toml configuration file from given path or default path."""
if config_path is None:
config_filename = xdg_config_home() / "multizone" / "multizone.toml"
if not config_filename.is_file():
config_filename = None
else:
config_filename = Path(config_path)
if config_filename is not None:
with config_filename.open("rb") as file_buff:
return tomllib.load(file_buff)
else:
return {}
def configure(args: argparse.Namespace, config_file: ConfigType) -> ConfigType:
"""Get the configuration from the command line arguments and contents of config file."""
config = {}
if args.format:
config["format"] = args.format
else:
config["format"] = config_file.get("format", "E, dd MMM, yyyy 'at' HH:mm a Z")
if args.zones:
config["zones"] = args.zones
else:
config["zones"] = config_file.get("zones", [])
config["aliases"] = config_file.get("aliases", {})
config["time"] = args.time
config["reftime"] = {"color": None, "on_color": None, "attrs": []}
config["reftime"].update(config_file.get("reftime", {}))
config["localtime"] = {"color": None, "on_color": None, "attrs": []}
config["localtime"].update(config_file.get("localtime", {}))
if args.locale:
config["locale"] = args.locale
else:
config["locale"] = config_file.get("locale", default_locale())
return config
def time_offset(item: tuple[str, datetime]) -> timedelta:
"""Key function to sort times."""
offset = item[1].utcoffset()
assert offset is not None
return -offset
def zone_table(
timezones: list[str],
aliases: dict[str, str],
refdt: datetime,
localzone: tzinfo,
) -> list[tuple[str, datetime]]:
"""Create a table of times for each time zone."""
zones: list[tuple[str, datetime]] = []
found_local = False
found_ref = False
localdt = refdt.astimezone(localzone)
localoffset = localdt.utcoffset()
refoffset = refdt.utcoffset()
for zonename in timezones:
name = aliases.get(zonename, zonename)
zone = zoneinfo.ZoneInfo(zonename)
time = refdt.astimezone(zone)
if time.utcoffset() == localoffset:
found_local = True
if time.utcoffset() == refoffset:
found_ref = True
zones.append((name, time))
if not found_local:
localname = str(localzone)
zones.append((aliases.get(localname, localname), localdt))
if not found_ref and localoffset != refoffset:
tzname = refdt.tzname()
assert tzname is not None
zones.append((aliases.get(tzname, tzname), refdt))
zones.sort(key=time_offset)
return zones
def table_to_string(
zones: list[tuple[str, datetime]],
refdt: datetime,
localzone: tzinfo,
config: ConfigType,
) -> str:
"""Convert a table of times into a tabulated string to print."""
zones2 = []
for name, time in zones:
timestr = format_datetime(time, config["format"], locale=config["locale"])
new_name = name
if time.utcoffset() == refdt.astimezone(localzone).utcoffset():
new_name = colored(new_name, **config["localtime"])
timestr = colored(timestr, **config["localtime"])
if time.utcoffset() == refdt.utcoffset():
new_name = colored(new_name, **config["reftime"])
timestr = colored(timestr, **config["reftime"])
zones2.append((new_name, timestr))
return tabulate.tabulate(zones2, tablefmt="plain")
def main() -> None: # pragma: no cover
"""Script entry point."""
# get the current time and the machine's timezone
utcnow = datetime.now(tz=zoneinfo.ZoneInfo("UTC"))
localzone = tzlocal.get_localzone()
args = parse_args()
config_file = load_config(args.config)
# run configuration
config = configure(args, config_file)
# parse the time given at command line and localzone
refdt = parse_time_arg(config["time"], utcnow, localzone)
# create the actual table
zones = zone_table(config["zones"], config["aliases"], refdt, localzone)
# print the table
print(table_to_string(zones, refdt, localzone, config)) # noqa: T201
if __name__ == "__main__":
main() # pragma: no cover