|
|
@ -1,5 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
import datetime
|
|
|
|
import datetime
|
|
|
|
|
|
|
|
import urllib
|
|
|
|
|
|
|
|
import zoneinfo
|
|
|
|
|
|
|
|
|
|
|
|
from common import dateutil
|
|
|
|
from common import dateutil
|
|
|
|
from common.requests import InstrumentedSession
|
|
|
|
from common.requests import InstrumentedSession
|
|
|
@ -9,75 +11,93 @@ requests = InstrumentedSession()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_shift_time(time_str, timeout=5):
|
|
|
|
def parse_shift_time(time_str, timeout=5):
|
|
|
|
"""Parse times in the shift definition.
|
|
|
|
"""
|
|
|
|
|
|
|
|
Parse times in the shift definition.
|
|
|
|
The parser first tries to parse a string as a datetime before trying the string as a URL to fetch a timestamp from."""
|
|
|
|
|
|
|
|
if not time_str:
|
|
|
|
The parser first tries to parse a string as a URL to fetch a timestamp from before trying to parse it as a timestamp.
|
|
|
|
return None
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
if not time_str:
|
|
|
|
return dateutil.parse(time_str)
|
|
|
|
return None
|
|
|
|
except ValueError:
|
|
|
|
if urllib.parse.urlparse(time_str).scheme in ('http', 'https'):
|
|
|
|
try:
|
|
|
|
resp = requests.get(time_str, timeout=timeout)
|
|
|
|
resp = requests.get(time_str, timeout=timeout, metric_name='get_shift_time')
|
|
|
|
resp.raise_for_status()
|
|
|
|
resp.raise_for_status()
|
|
|
|
return dateutil.parse(resp.text.strip())
|
|
|
|
return dateutil.parse(resp.text.strip())
|
|
|
|
else:
|
|
|
|
except Exception:
|
|
|
|
return dateutil.parse(time_str)
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_shifts(shifts):
|
|
|
|
def parse_shifts(shifts):
|
|
|
|
"""Parse a shifts definition
|
|
|
|
"""
|
|
|
|
|
|
|
|
Parse a shifts definition.
|
|
|
|
The shifts definition is two entry mappable with two keys, repeating and one-off.
|
|
|
|
|
|
|
|
|
|
|
|
The shifts definition is three entry mappable with two keys, repeating, one_off and timezone.
|
|
|
|
The repeating shifts entry is a list of shift definition. Each of these is a sequence consisting of the name of the shift, the starting hour of the shift in local time, and the ending hour in local time. Repeating shifts extending across midnight can be handled by using two shifts with the same name. For example:
|
|
|
|
|
|
|
|
[['Night', 0, 6],
|
|
|
|
The repeating shifts entry is a list of shift definition.
|
|
|
|
['Day', 6, 18],
|
|
|
|
Each of these is a sequence consisting of the name of the shift,
|
|
|
|
['Night', 18, 24]]
|
|
|
|
the starting hour of the shift in local time, and the ending hour in local time.
|
|
|
|
|
|
|
|
Repeating shifts extending across midnight can be handled by using two shifts with the same name.
|
|
|
|
The one-off shifts entry is a list of shift definitions. Each of these is a sequence consisting of the name of the shift, the start the shift, and the end of the shift. A start or end time can be a timestamp, a URL or None. If it is a URL, the URL will be queried for a timestamp. If no timezone info is provided the timestamp will be assumed to be UTC. If the start time is None, then the start will be assumed to be the earliest possible datetime; if the end is None, it will be assumed to be the oldest possible datetime. If both the start and end are None, the shift will be ignored. For example:
|
|
|
|
For example:
|
|
|
|
[['Full', '2024-01-01T00:00:00', '2024-01-02T00:00:00'],
|
|
|
|
[['Night', 0, 6],
|
|
|
|
['End Only', '2024-01-02T00:00:00', None],
|
|
|
|
['Day', 6, 18],
|
|
|
|
['URL', 'http://example.com/start.html', '2024-01-01T00:00:00'],
|
|
|
|
['Night', 18, 24]]
|
|
|
|
['Both None', None, None]]
|
|
|
|
|
|
|
|
would be parsed as:
|
|
|
|
The one-off shifts entry is a list of shift definitions.
|
|
|
|
[['Full', '2024-01-01T00:00:00', '2024-01-02T00:00:00'],
|
|
|
|
Each of these is a sequence consisting of the name of the shift, the start the shift,
|
|
|
|
['Start Only', '2024-01-02T00:00:00', '9999-12-31T23:59:59.999999'],
|
|
|
|
and the end of the shift.
|
|
|
|
['URL', '2023-12-31T12:00:00', '2024-01-01T00:00:00']]
|
|
|
|
A start or end time can be a timestamp, a URL or None.
|
|
|
|
"""
|
|
|
|
If it is a URL, the URL will be queried for a timestamp.
|
|
|
|
new_shifts = {'repeating':shifts['repeating'], 'one_off':[]}
|
|
|
|
If no timezone info is provided the timestamp will be assumed to be UTC.
|
|
|
|
for shift in shifts['one_off']:
|
|
|
|
If the start time is None, then the start will be assumed to be the earliest possible datetime;
|
|
|
|
name, start, end = shift
|
|
|
|
if the end is None, it will be assumed to be the oldest possible datetime.
|
|
|
|
start = parse_shift_time(start)
|
|
|
|
For example:
|
|
|
|
end = parse_shift_time(end)
|
|
|
|
[['Full', '2024-01-01T00:00:00', '2024-01-02T00:00:00'],
|
|
|
|
if (start is None) and (end is None):
|
|
|
|
['End Only', '2024-01-02T00:00:00', None],
|
|
|
|
continue
|
|
|
|
['URL', 'http://example.com/start.html', '2024-01-01T00:00:00'],
|
|
|
|
if start is None:
|
|
|
|
['Both None', None, None]]
|
|
|
|
start = datetime.datetime.min
|
|
|
|
would be parsed as:
|
|
|
|
if end is None:
|
|
|
|
[['Full', '2024-01-01T00:00:00', '2024-01-02T00:00:00'],
|
|
|
|
end = datetime.datetime.max
|
|
|
|
['End Only', '2024-01-02T00:00:00', '9999-12-31T23:59:59.999999'],
|
|
|
|
new_shifts['one_off'].append([name, start, end])
|
|
|
|
['URL', '2023-12-31T12:00:00', '2024-01-01T00:00:00'],
|
|
|
|
return new_shifts
|
|
|
|
['Both None', '0001-01-01T00:00:00', '9999-12-31T23:59:59.999999']]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The timezone entry is a string that the zoneinfo package can interpret as a timezone
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
One-off shifts override repeating shifts.
|
|
|
|
|
|
|
|
In the case of overlapping shifts, the first shift in the list takes precedence.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
new_shifts = {'repeating':shifts['repeating'], 'one_off':[]}
|
|
|
|
|
|
|
|
for shift in shifts['one_off']:
|
|
|
|
|
|
|
|
name, start, end = shift
|
|
|
|
|
|
|
|
start = parse_shift_time(start)
|
|
|
|
|
|
|
|
end = parse_shift_time(end)
|
|
|
|
|
|
|
|
if start is None:
|
|
|
|
|
|
|
|
start = datetime.datetime.min
|
|
|
|
|
|
|
|
if end is None:
|
|
|
|
|
|
|
|
end = datetime.datetime.max
|
|
|
|
|
|
|
|
new_shifts['one_off'].append([name, start, end])
|
|
|
|
|
|
|
|
new_shifts['timezone'] = zoneinfo.ZoneInfo(shifts['timezone'])
|
|
|
|
|
|
|
|
return new_shifts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def calculate_shift(time, shifts, timezone):
|
|
|
|
def calculate_shift(time, shifts, timezone):
|
|
|
|
"""Calculate what shift a time falls in.
|
|
|
|
"""
|
|
|
|
|
|
|
|
Calculate what shift a time falls in.
|
|
|
|
time is a datetime, shifts the output from parse_shifts and timezone a
|
|
|
|
|
|
|
|
"""
|
|
|
|
Arguments:
|
|
|
|
if not time:
|
|
|
|
time -- a datetime.datetime instance
|
|
|
|
return ''
|
|
|
|
shifts -- the output from parse_shifts
|
|
|
|
|
|
|
|
"""
|
|
|
|
for shift in shifts['one_off']:
|
|
|
|
if time is not None:
|
|
|
|
print(time, shift[1], shift[2])
|
|
|
|
return ''
|
|
|
|
if shift[1] <= time < shift[2]:
|
|
|
|
|
|
|
|
return shift[0]
|
|
|
|
for shift in shifts['one_off']:
|
|
|
|
|
|
|
|
if shift[1] <= time < shift[2]:
|
|
|
|
#since shifts are based on local times we have to worry about timezones for once
|
|
|
|
return shift[0]
|
|
|
|
local_time = time.replace(tzinfo=UTC).astimezone(timezone)
|
|
|
|
|
|
|
|
# do a more involved calculation to allow for non-integer start and end hours
|
|
|
|
#since shifts are based on local times we have to worry about timezones for once
|
|
|
|
time_diff = local_time - datetime.datetime(local_time.year, local_time.month, local_time.day, tzinfo=timezone)
|
|
|
|
local_time = time.replace(tzinfo=UTC).astimezone(timezone)
|
|
|
|
hour = time_diff / datetime.timedelta(hours=1)
|
|
|
|
# do a more involved calculation to allow for non-integer start and end hours
|
|
|
|
for shift in shifts['repeating']:
|
|
|
|
hour = local_time.hour + local_time.minute / 60 + local_time.second / 3600
|
|
|
|
if shift[1] <= hour < shift[2]:
|
|
|
|
for shift in shifts['repeating']:
|
|
|
|
return shift[0]
|
|
|
|
if shift[1] <= hour < shift[2]:
|
|
|
|
|
|
|
|
return shift[0]
|
|
|
|