mirror of
https://github.com/weewx/weewx.git
synced 2026-04-18 16:46:56 -04:00
1921 lines
64 KiB
Python
1921 lines
64 KiB
Python
# This Python file uses the following encoding: utf-8
|
|
#
|
|
# Copyright (c) 2009-2023 Tom Keffer <tkeffer@gmail.com>
|
|
#
|
|
# See the file LICENSE.txt for your full rights.
|
|
#
|
|
"""Various handy utilities that don't belong anywhere else.
|
|
|
|
NB: To run the doctests, this code must be run as a module. For example:
|
|
cd ~/git/weewx/bin
|
|
python -m weeutil.weeutil
|
|
"""
|
|
|
|
import calendar
|
|
import cmath
|
|
import datetime
|
|
import math
|
|
import os
|
|
import re
|
|
import shutil
|
|
import time
|
|
|
|
# For backwards compatibility:
|
|
from weeutil.config import accumulateLeaves, search_up
|
|
|
|
|
|
def convertToFloat(seq):
|
|
"""Convert a sequence with strings to floats, honoring 'Nones'"""
|
|
|
|
if seq is None:
|
|
return None
|
|
res = [None if s in ('None', 'none') else float(s) for s in seq]
|
|
return res
|
|
|
|
|
|
def option_as_list(option):
|
|
if option is None:
|
|
return None
|
|
return [option] if not isinstance(option, list) else option
|
|
|
|
to_list = option_as_list
|
|
|
|
def list_as_string(option):
|
|
"""Returns the argument as a string.
|
|
|
|
Useful for insuring that ConfigObj options are always returned
|
|
as a string, despite the presence of a comma in the middle.
|
|
|
|
Example:
|
|
>>> print(list_as_string('a string'))
|
|
a string
|
|
>>> print(list_as_string(['a', 'string']))
|
|
a, string
|
|
>>> print(list_as_string('Reno, NV'))
|
|
Reno, NV
|
|
"""
|
|
# Check if it's already a string.
|
|
if option is not None and not isinstance(option, str):
|
|
return ', '.join(option)
|
|
return option
|
|
|
|
|
|
def startOfInterval(time_ts, interval):
|
|
"""Find the start time of an interval.
|
|
|
|
This algorithm assumes unit epoch time is divided up into
|
|
intervals of 'interval' length. Given a timestamp, it
|
|
figures out which interval it lies in, returning the start
|
|
time.
|
|
|
|
Args:
|
|
|
|
time_ts (float): A timestamp. The start of the interval containing this
|
|
timestamp will be returned.
|
|
interval (int): An interval length in seconds.
|
|
|
|
Returns:
|
|
int: A timestamp with the start of the interval.
|
|
|
|
Examples:
|
|
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> start_ts = time.mktime(time.strptime("2013-07-04 01:57:35", "%Y-%m-%d %H:%M:%S"))
|
|
>>> time.ctime(startOfInterval(start_ts, 300))
|
|
'Thu Jul 4 01:55:00 2013'
|
|
>>> time.ctime(startOfInterval(start_ts, 300.0))
|
|
'Thu Jul 4 01:55:00 2013'
|
|
>>> time.ctime(startOfInterval(start_ts, 600))
|
|
'Thu Jul 4 01:50:00 2013'
|
|
>>> time.ctime(startOfInterval(start_ts, 900))
|
|
'Thu Jul 4 01:45:00 2013'
|
|
>>> time.ctime(startOfInterval(start_ts, 3600))
|
|
'Thu Jul 4 01:00:00 2013'
|
|
>>> time.ctime(startOfInterval(start_ts, 7200))
|
|
'Thu Jul 4 01:00:00 2013'
|
|
>>> start_ts = time.mktime(time.strptime("2013-07-04 01:00:00", "%Y-%m-%d %H:%M:%S"))
|
|
>>> time.ctime(startOfInterval(start_ts, 300))
|
|
'Thu Jul 4 00:55:00 2013'
|
|
>>> start_ts = time.mktime(time.strptime("2013-07-04 01:00:01", "%Y-%m-%d %H:%M:%S"))
|
|
>>> time.ctime(startOfInterval(start_ts, 300))
|
|
'Thu Jul 4 01:00:00 2013'
|
|
>>> start_ts = time.mktime(time.strptime("2013-07-04 01:04:59", "%Y-%m-%d %H:%M:%S"))
|
|
>>> time.ctime(startOfInterval(start_ts, 300))
|
|
'Thu Jul 4 01:00:00 2013'
|
|
>>> start_ts = time.mktime(time.strptime("2013-07-04 00:00:00", "%Y-%m-%d %H:%M:%S"))
|
|
>>> time.ctime(startOfInterval(start_ts, 300))
|
|
'Wed Jul 3 23:55:00 2013'
|
|
>>> start_ts = time.mktime(time.strptime("2013-07-04 07:51:00", "%Y-%m-%d %H:%M:%S"))
|
|
>>> time.ctime(startOfInterval(start_ts, 60))
|
|
'Thu Jul 4 07:50:00 2013'
|
|
>>> start_ts += 0.1
|
|
>>> time.ctime(startOfInterval(start_ts, 60))
|
|
'Thu Jul 4 07:51:00 2013'
|
|
"""
|
|
|
|
start_interval_ts = int(time_ts / interval) * interval
|
|
|
|
if time_ts == start_interval_ts:
|
|
start_interval_ts -= interval
|
|
return start_interval_ts
|
|
|
|
|
|
def _ord_to_ts(ord):
|
|
"""Convert from ordinal date to unix epoch time.
|
|
|
|
Args:
|
|
ord (int): A proleptic Gregorian ordinal.
|
|
|
|
Returns:
|
|
int: Unix epoch time of the start of the corresponding day.
|
|
"""
|
|
d = datetime.date.fromordinal(ord)
|
|
t = int(time.mktime(d.timetuple()))
|
|
return t
|
|
|
|
|
|
# ===============================================================================
|
|
# What follows is a bunch of "time span" routines. Generally, time spans
|
|
# are used when start and stop times fall on calendar boundaries
|
|
# such as days, months, years. So, it makes sense to talk of "daySpans",
|
|
# "weekSpans", etc. They are generally not used between two random times.
|
|
# ===============================================================================
|
|
|
|
class TimeSpan(tuple):
|
|
"""Represents a time span, exclusive on the left, inclusive on the right."""
|
|
|
|
def __new__(cls, *args):
|
|
if args[0] > args[1]:
|
|
raise ValueError("start time (%d) is greater than stop time (%d)" % (args[0], args[1]))
|
|
return tuple.__new__(cls, args)
|
|
|
|
@property
|
|
def start(self):
|
|
return self[0]
|
|
|
|
@property
|
|
def stop(self):
|
|
return self[1]
|
|
|
|
@property
|
|
def length(self):
|
|
return self[1] - self[0]
|
|
|
|
def includesArchiveTime(self, timestamp):
|
|
"""
|
|
Returns True if the span includes the time timestamp, otherwise False.
|
|
|
|
timestamp: The timestamp to be tested.
|
|
"""
|
|
return self.start < timestamp <= self.stop
|
|
|
|
def includes(self, span):
|
|
|
|
return self.start <= span.start <= self.stop and self.start <= span.stop <= self.stop
|
|
|
|
def __eq__(self, other):
|
|
return self.start == other.start and self.stop == other.stop
|
|
|
|
def __str__(self):
|
|
return "[%s -> %s]" % (timestamp_to_string(self.start),
|
|
timestamp_to_string(self.stop))
|
|
|
|
def __hash__(self):
|
|
return hash(self.start) ^ hash(self.stop)
|
|
|
|
|
|
nominal_intervals = {
|
|
'hour': 3600,
|
|
'day': 86400,
|
|
'week': 7 * 86400,
|
|
'month': int(365.25 / 12 * 86400),
|
|
'year': int(365.25 * 86400),
|
|
}
|
|
duration_synonyms = {
|
|
'hour': '1h',
|
|
'day': '1d',
|
|
'week': '1w',
|
|
'month': '1m',
|
|
'year': '1y',
|
|
}
|
|
|
|
|
|
def nominal_spans(label):
|
|
"""Convert a (possible) string into an integer time."""
|
|
if label is None:
|
|
return None
|
|
|
|
if isinstance(label, str):
|
|
label = duration_synonyms.get(label.lower(), label)
|
|
if label.endswith('M'):
|
|
# Minute
|
|
return int(label[:-1]) * 60
|
|
elif label.endswith('h'):
|
|
# Hour
|
|
return int(label[:-1]) * nominal_intervals['hour']
|
|
elif label.endswith('d'):
|
|
# Day
|
|
return int(label[:-1]) * nominal_intervals['day']
|
|
elif label.endswith('w'):
|
|
# Week
|
|
return int(label[:-1]) * nominal_intervals['week']
|
|
elif label.endswith('m'):
|
|
# Month
|
|
return int(label[:-1]) * nominal_intervals['month']
|
|
elif label.endswith('y'):
|
|
# Year
|
|
return int(label[:-1]) * nominal_intervals['year']
|
|
else:
|
|
return int(label)
|
|
return label
|
|
|
|
|
|
def isStartOfDay(time_ts):
|
|
"""Is the indicated time at the start of the day, local time?
|
|
|
|
This algorithm will work even in countries that switch to DST at midnight, such as Brazil.
|
|
|
|
Args:
|
|
time_ts (float): A unix epoch timestamp.
|
|
|
|
Returns:
|
|
bool: True if the timestamp is at midnight, False otherwise.
|
|
|
|
Example:
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> time_ts = time.mktime(time.strptime("2013-07-04 01:57:35", "%Y-%m-%d %H:%M:%S"))
|
|
>>> print(isStartOfDay(time_ts))
|
|
False
|
|
>>> time_ts = time.mktime(time.strptime("2013-07-04 00:00:00", "%Y-%m-%d %H:%M:%S"))
|
|
>>> print(isStartOfDay(time_ts))
|
|
True
|
|
>>> os.environ['TZ'] = 'America/Sao_Paulo'
|
|
>>> time.tzset()
|
|
>>> time_ts = 1541300400
|
|
>>> print(isStartOfDay(time_ts))
|
|
True
|
|
>>> print(isStartOfDay(time_ts - 1))
|
|
False
|
|
"""
|
|
|
|
# Test the date of the time against the date a tenth of a second before.
|
|
# If they do not match, the time must have been the start of the day
|
|
dt1 = datetime.date.fromtimestamp(time_ts)
|
|
dt2 = datetime.date.fromtimestamp(time_ts - .1)
|
|
return not dt1 == dt2
|
|
|
|
|
|
def isMidnight(time_ts):
|
|
"""Is the indicated time on a midnight boundary, local time?
|
|
NB: This algorithm does not work in countries that switch to DST
|
|
at midnight, such as Brazil.
|
|
|
|
Args:
|
|
time_ts (float): A unix epoch timestamp.
|
|
|
|
Returns:
|
|
bool: True if the timestamp is at midnight, False otherwise.
|
|
|
|
Example:
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> time_ts = time.mktime(time.strptime("2013-07-04 01:57:35", "%Y-%m-%d %H:%M:%S"))
|
|
>>> print(isMidnight(time_ts))
|
|
False
|
|
>>> time_ts = time.mktime(time.strptime("2013-07-04 00:00:00", "%Y-%m-%d %H:%M:%S"))
|
|
>>> print(isMidnight(time_ts))
|
|
True
|
|
"""
|
|
|
|
time_tt = time.localtime(time_ts)
|
|
return time_tt.tm_hour == 0 and time_tt.tm_min == 0 and time_tt.tm_sec == 0
|
|
|
|
|
|
def archiveSpanSpan(time_ts, time_delta=0, hour_delta=0, day_delta=0, week_delta=0, month_delta=0,
|
|
year_delta=0, boundary=None):
|
|
""" Returns a TimeSpan for the last xxx seconds where xxx equals
|
|
time_delta sec + hour_delta hours + day_delta days + week_delta weeks + month_delta months + year_delta years
|
|
|
|
NOTE: Use of month_delta and year_delta is deprecated.
|
|
See issue #436 (https://github.com/weewx/weewx/issues/436)
|
|
|
|
Example:
|
|
>>> os.environ['TZ'] = 'Australia/Brisbane'
|
|
>>> time.tzset()
|
|
>>> time_ts = time.mktime(time.strptime("2015-07-21 09:05:35", "%Y-%m-%d %H:%M:%S"))
|
|
>>> print(archiveSpanSpan(time_ts, time_delta=3600))
|
|
[2015-07-21 08:05:35 AEST (1437429935) -> 2015-07-21 09:05:35 AEST (1437433535)]
|
|
>>> print(archiveSpanSpan(time_ts, hour_delta=6))
|
|
[2015-07-21 03:05:35 AEST (1437411935) -> 2015-07-21 09:05:35 AEST (1437433535)]
|
|
>>> print(archiveSpanSpan(time_ts, day_delta=1))
|
|
[2015-07-20 09:05:35 AEST (1437347135) -> 2015-07-21 09:05:35 AEST (1437433535)]
|
|
>>> print(archiveSpanSpan(time_ts, time_delta=3600, day_delta=1))
|
|
[2015-07-20 08:05:35 AEST (1437343535) -> 2015-07-21 09:05:35 AEST (1437433535)]
|
|
>>> print(archiveSpanSpan(time_ts, week_delta=4))
|
|
[2015-06-23 09:05:35 AEST (1435014335) -> 2015-07-21 09:05:35 AEST (1437433535)]
|
|
>>> print(archiveSpanSpan(time_ts, month_delta=1))
|
|
[2015-06-21 09:05:35 AEST (1434841535) -> 2015-07-21 09:05:35 AEST (1437433535)]
|
|
>>> print(archiveSpanSpan(time_ts, year_delta=1))
|
|
[2014-07-21 09:05:35 AEST (1405897535) -> 2015-07-21 09:05:35 AEST (1437433535)]
|
|
>>> print(archiveSpanSpan(time_ts))
|
|
[2015-07-21 09:05:34 AEST (1437433534) -> 2015-07-21 09:05:35 AEST (1437433535)]
|
|
|
|
Example over a DST boundary. Because Brisbane does not observe DST, we need to
|
|
switch timezones.
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> time_ts = 1457888400
|
|
>>> print(timestamp_to_string(time_ts))
|
|
2016-03-13 10:00:00 PDT (1457888400)
|
|
>>> span = archiveSpanSpan(time_ts, day_delta=1)
|
|
>>> print(span)
|
|
[2016-03-12 10:00:00 PST (1457805600) -> 2016-03-13 10:00:00 PDT (1457888400)]
|
|
|
|
Note that there is not 24 hours of time over this span:
|
|
>>> print((span.stop - span.start) / 3600.0)
|
|
23.0
|
|
"""
|
|
|
|
if time_ts is None:
|
|
return None
|
|
|
|
# Use a datetime.timedelta so that it can take DST into account:
|
|
time_dt = datetime.datetime.fromtimestamp(time_ts)
|
|
time_dt -= datetime.timedelta(weeks=week_delta, days=day_delta, hours=hour_delta,
|
|
seconds=time_delta)
|
|
|
|
# Now add the deltas for months and years. Because these can be variable in length,
|
|
# some special arithmetic is needed. Start by calculating the number of
|
|
# months since 0 AD:
|
|
total_months = 12 * time_dt.year + time_dt.month - 1 - 12 * year_delta - month_delta
|
|
# Convert back from total months since 0 AD to year and month:
|
|
year = total_months // 12
|
|
month = total_months % 12 + 1
|
|
# Apply the delta to our datetime object
|
|
start_dt = time_dt.replace(year=year, month=month)
|
|
|
|
# Finally, convert to unix epoch time
|
|
if boundary is None:
|
|
start_ts = int(time.mktime(start_dt.timetuple()))
|
|
if start_ts == time_ts:
|
|
start_ts -= 1
|
|
elif boundary.lower() == 'midnight':
|
|
start_ts = _ord_to_ts(start_dt.toordinal())
|
|
else:
|
|
raise ValueError("Unknown boundary %s" % boundary)
|
|
|
|
return TimeSpan(start_ts, time_ts)
|
|
|
|
|
|
def archiveHoursAgoSpan(time_ts, hours_ago=0):
|
|
"""Returns a one-hour long TimeSpan for x hours ago that includes the given time.
|
|
|
|
Args:
|
|
time_ts (float): A timestamp. An hour long time span will be returned that encompasses this
|
|
timestamp.
|
|
hours_ago (int, optional): Which hour we want. 0=this hour, 1=last hour, etc. Default is
|
|
zero (this hour).
|
|
|
|
Returns:
|
|
TimeSpan: A TimeSpan object one hour long, that includes time_ts.
|
|
|
|
NB: A timestamp that falls exactly on the hour boundary is considered to belong
|
|
to the *previous* hour.
|
|
|
|
Example:
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> time_ts = time.mktime(time.strptime("2013-07-04 01:57:35", "%Y-%m-%d %H:%M:%S"))
|
|
>>> print(archiveHoursAgoSpan(time_ts, hours_ago=0))
|
|
[2013-07-04 01:00:00 PDT (1372924800) -> 2013-07-04 02:00:00 PDT (1372928400)]
|
|
>>> print(archiveHoursAgoSpan(time_ts, hours_ago=2))
|
|
[2013-07-03 23:00:00 PDT (1372917600) -> 2013-07-04 00:00:00 PDT (1372921200)]
|
|
>>> time_ts = time.mktime(datetime.date(2013, 7, 4).timetuple())
|
|
>>> print(archiveHoursAgoSpan(time_ts, hours_ago=0))
|
|
[2013-07-03 23:00:00 PDT (1372917600) -> 2013-07-04 00:00:00 PDT (1372921200)]
|
|
>>> print(archiveHoursAgoSpan(time_ts, hours_ago=24))
|
|
[2013-07-02 23:00:00 PDT (1372831200) -> 2013-07-03 00:00:00 PDT (1372834800)]
|
|
"""
|
|
if time_ts is None:
|
|
return None
|
|
|
|
time_dt = datetime.datetime.fromtimestamp(time_ts)
|
|
|
|
# If we are exactly at an hour boundary, the start of the archive hour is actually
|
|
# the *previous* hour.
|
|
if time_dt.minute == 0 \
|
|
and time_dt.second == 0 \
|
|
and time_dt.microsecond == 0:
|
|
hours_ago += 1
|
|
|
|
# Find the start of the hour
|
|
start_of_hour_dt = time_dt.replace(minute=0, second=0, microsecond=0)
|
|
|
|
start_span_dt = start_of_hour_dt - datetime.timedelta(hours=hours_ago)
|
|
stop_span_dt = start_span_dt + datetime.timedelta(hours=1)
|
|
|
|
return TimeSpan(int(time.mktime(start_span_dt.timetuple())),
|
|
int(time.mktime(stop_span_dt.timetuple())))
|
|
|
|
|
|
def daySpan(time_ts, days_ago=0, archive=False):
|
|
"""Returns a one-day long TimeSpan for x days ago that includes a given time.
|
|
|
|
Args:
|
|
time_ts (float): The day will include this timestamp.
|
|
days_ago (int): Which day we want. 0=today, 1=yesterday, etc.
|
|
archive (bool, optional): True to calculate archive day; false otherwise.
|
|
|
|
Returns:
|
|
TimeSpan: A TimeSpan object one day long.
|
|
|
|
Example:
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> time_ts = time.mktime(time.strptime("2014-01-01 01:57:35", "%Y-%m-%d %H:%M:%S"))
|
|
|
|
As for today:
|
|
>>> print(daySpan(time_ts))
|
|
[2014-01-01 00:00:00 PST (1388563200) -> 2014-01-02 00:00:00 PST (1388649600)]
|
|
|
|
Do it again, but on the midnight boundary
|
|
>>> time_ts = time.mktime(time.strptime("2014-01-01 00:00:00", "%Y-%m-%d %H:%M:%S"))
|
|
|
|
We should still get today (this differs from the function archiveDaySpan())
|
|
>>> print(daySpan(time_ts))
|
|
[2014-01-01 00:00:00 PST (1388563200) -> 2014-01-02 00:00:00 PST (1388649600)]
|
|
"""
|
|
if time_ts is None:
|
|
return None
|
|
|
|
time_dt = datetime.datetime.fromtimestamp(time_ts)
|
|
|
|
if archive:
|
|
# If we are exactly at midnight, the start of the archive day is actually
|
|
# the *previous* day
|
|
if time_dt.hour == 0 \
|
|
and time_dt.minute == 0 \
|
|
and time_dt.second == 0 \
|
|
and time_dt.microsecond == 0:
|
|
days_ago += 1
|
|
|
|
# Find the start of the day
|
|
start_of_day_dt = time_dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
start_span_dt = start_of_day_dt - datetime.timedelta(days=days_ago)
|
|
stop_span_dt = start_span_dt + datetime.timedelta(days=1)
|
|
|
|
return TimeSpan(int(time.mktime(start_span_dt.timetuple())),
|
|
int(time.mktime(stop_span_dt.timetuple())))
|
|
|
|
|
|
def archiveDaySpan(time_ts, days_ago=0):
|
|
"""Returns a one-day long TimeSpan for x days ago that includes a given time.
|
|
|
|
Args:
|
|
time_ts (float): The day will include this timestamp.
|
|
days_ago (int): Which day we want. 0=today, 1=yesterday, etc.
|
|
|
|
Returns:
|
|
TimeSpan: A TimeSpan object one day long.
|
|
|
|
NB: A timestamp that falls exactly on midnight is considered to belong
|
|
to the *previous* day.
|
|
|
|
Example, which spans the end-of-year boundary
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> time_ts = time.mktime(time.strptime("2014-01-01 01:57:35", "%Y-%m-%d %H:%M:%S"))
|
|
|
|
As for today:
|
|
>>> print(archiveDaySpan(time_ts))
|
|
[2014-01-01 00:00:00 PST (1388563200) -> 2014-01-02 00:00:00 PST (1388649600)]
|
|
|
|
Ask for yesterday:
|
|
>>> print(archiveDaySpan(time_ts, days_ago=1))
|
|
[2013-12-31 00:00:00 PST (1388476800) -> 2014-01-01 00:00:00 PST (1388563200)]
|
|
|
|
Day before yesterday
|
|
>>> print(archiveDaySpan(time_ts, days_ago=2))
|
|
[2013-12-30 00:00:00 PST (1388390400) -> 2013-12-31 00:00:00 PST (1388476800)]
|
|
|
|
Do it again, but on the midnight boundary
|
|
>>> time_ts = time.mktime(time.strptime("2014-01-01 00:00:00", "%Y-%m-%d %H:%M:%S"))
|
|
|
|
This time, we should get the previous day
|
|
>>> print(archiveDaySpan(time_ts))
|
|
[2013-12-31 00:00:00 PST (1388476800) -> 2014-01-01 00:00:00 PST (1388563200)]
|
|
"""
|
|
return daySpan(time_ts, days_ago, True)
|
|
|
|
|
|
# For backwards compatibility. Not sure if anyone is actually using this
|
|
archiveDaysAgoSpan = archiveDaySpan
|
|
|
|
|
|
def archiveWeekSpan(time_ts, startOfWeek=6, weeks_ago=0):
|
|
"""Returns a one-week long TimeSpan for x weeks ago that includes a given time.
|
|
|
|
Args:
|
|
time_ts (float): The week will include this timestamp.
|
|
startOfWeek (int, optional): The start of the week (0=Monday, 1=Tues, ..., 6 = Sun).
|
|
weeks_ago (int, optional): Which week we want. 0=this week, 1=last week, etc.
|
|
|
|
Returns:
|
|
TimeSpan: A TimeSpan object one week long that contains time_ts. It will
|
|
start at midnight of the day considered the start of the week.
|
|
|
|
NB: The time at midnight at the end of the week is considered to
|
|
actually belong in the previous week.
|
|
|
|
Example:
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> time_ts = 1483429962
|
|
>>> print(timestamp_to_string(time_ts))
|
|
2017-01-02 23:52:42 PST (1483429962)
|
|
>>> print(archiveWeekSpan(time_ts))
|
|
[2017-01-01 00:00:00 PST (1483257600) -> 2017-01-08 00:00:00 PST (1483862400)]
|
|
>>> print(archiveWeekSpan(time_ts, weeks_ago=1))
|
|
[2016-12-25 00:00:00 PST (1482652800) -> 2017-01-01 00:00:00 PST (1483257600)]
|
|
"""
|
|
if time_ts is None:
|
|
return None
|
|
|
|
time_dt = datetime.datetime.fromtimestamp(time_ts)
|
|
|
|
# Find the start of the day:
|
|
start_of_day_dt = time_dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
# Find the relative start of the week
|
|
day_of_week = start_of_day_dt.weekday()
|
|
delta = day_of_week - startOfWeek
|
|
if delta < 0:
|
|
delta += 7
|
|
|
|
# If we are exactly at midnight, the start of the archive week is actually
|
|
# the *previous* week
|
|
if day_of_week == startOfWeek \
|
|
and time_dt.hour == 0 \
|
|
and time_dt.minute == 0 \
|
|
and time_dt.second == 0 \
|
|
and time_dt.microsecond == 0:
|
|
delta += 7
|
|
|
|
# Finally, find the start of the requested week.
|
|
delta += weeks_ago * 7
|
|
|
|
start_of_week = start_of_day_dt - datetime.timedelta(days=delta)
|
|
end_of_week = start_of_week + datetime.timedelta(days=7)
|
|
|
|
return TimeSpan(int(time.mktime(start_of_week.timetuple())),
|
|
int(time.mktime(end_of_week.timetuple())))
|
|
|
|
|
|
def archiveMonthSpan(time_ts, months_ago=0):
|
|
"""Returns a one-month long TimeSpan for x months ago that includes a given time.
|
|
|
|
Args:
|
|
time_ts (float): The month will include this timestamp.
|
|
months_ago (int, optional): Which month we want. 0=this month, 1=last month, etc.
|
|
|
|
Returns:
|
|
TimeSpan: A TimeSpan object one month long that contains time_ts.
|
|
|
|
NB: The time at midnight at the end of the month is considered to
|
|
actually belong in the previous week.
|
|
|
|
Example:
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> time_ts = 1483429962
|
|
>>> print(timestamp_to_string(time_ts))
|
|
2017-01-02 23:52:42 PST (1483429962)
|
|
>>> print(archiveMonthSpan(time_ts))
|
|
[2017-01-01 00:00:00 PST (1483257600) -> 2017-02-01 00:00:00 PST (1485936000)]
|
|
>>> print(archiveMonthSpan(time_ts, months_ago=1))
|
|
[2016-12-01 00:00:00 PST (1480579200) -> 2017-01-01 00:00:00 PST (1483257600)]
|
|
"""
|
|
if time_ts is None:
|
|
return None
|
|
|
|
time_dt = datetime.datetime.fromtimestamp(time_ts)
|
|
|
|
# If we are exactly at midnight of the first day of the month,
|
|
# the start of the archive month is actually the *previous* month
|
|
if time_dt.day == 1 \
|
|
and time_dt.hour == 0 \
|
|
and time_dt.minute == 0 \
|
|
and time_dt.second == 0 \
|
|
and time_dt.microsecond == 0:
|
|
months_ago += 1
|
|
|
|
# Find the start of the month
|
|
start_of_month_dt = time_dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
# Total number of months since 0AD
|
|
total_months = 12 * start_of_month_dt.year + start_of_month_dt.month - 1
|
|
|
|
# Adjust for the requested delta:
|
|
total_months -= months_ago
|
|
|
|
# Now rebuild the date
|
|
start_year = total_months // 12
|
|
start_month = total_months % 12 + 1
|
|
start_date = datetime.date(year=start_year, month=start_month, day=1)
|
|
|
|
# Advance to the start of the next month. This will be the end of the time span.
|
|
total_months += 1
|
|
stop_year = total_months // 12
|
|
stop_month = total_months % 12 + 1
|
|
stop_date = datetime.date(year=stop_year, month=stop_month, day=1)
|
|
|
|
return TimeSpan(int(time.mktime(start_date.timetuple())),
|
|
int(time.mktime(stop_date.timetuple())))
|
|
|
|
|
|
def archiveYearSpan(time_ts, years_ago=0):
|
|
"""Returns a TimeSpan representing a year that includes a given time.
|
|
|
|
Args:
|
|
time_ts (float): The year will include this timestamp.
|
|
years_ago (int, optional): Which year we want. 0=this year, 1=last year, etc.
|
|
|
|
Returns:
|
|
TimeSpan: A TimeSpan object one year long that contains time_ts. It will
|
|
start at midnight of 1-Jan
|
|
|
|
NB: Midnight of the 1st of the January is considered to actually belong
|
|
in the previous year.
|
|
"""
|
|
|
|
if time_ts is None:
|
|
return None
|
|
|
|
time_dt = datetime.datetime.fromtimestamp(time_ts)
|
|
|
|
# If we are exactly at midnight 1-Jan, then the start of the archive year is actually
|
|
# the *previous* year
|
|
if time_dt.month == 1 \
|
|
and time_dt.day == 1 \
|
|
and time_dt.hour == 0 \
|
|
and time_dt.minute == 0 \
|
|
and time_dt.second == 0 \
|
|
and time_dt.microsecond == 0:
|
|
years_ago += 1
|
|
|
|
return TimeSpan(int(time.mktime((time_dt.year - years_ago, 1, 1, 0, 0, 0, 0, 0, -1))),
|
|
int(time.mktime((time_dt.year - years_ago + 1, 1, 1, 0, 0, 0, 0, 0, -1))))
|
|
|
|
|
|
def archiveRainYearSpan(time_ts, sory_mon, years_ago=0):
|
|
"""Returns a TimeSpan representing a rain year that includes a given time.
|
|
|
|
Args:
|
|
time_ts (float): The rain year will include this timestamp.
|
|
sory_mon (int): The start of the rain year (1=Jan, 2=Feb, etc.)
|
|
years_ago (int, optional): Which rain year we want. 0=this year, 1=last year, etc.
|
|
|
|
Returns:
|
|
TimeSpan: A one-year long TimeSpan object containing the timestamp.
|
|
|
|
NB: Midnight of the 1st of the month starting the rain year is considered to
|
|
actually belong in the previous rain year.
|
|
"""
|
|
if time_ts is None:
|
|
return None
|
|
|
|
time_dt = datetime.datetime.fromtimestamp(time_ts)
|
|
|
|
# If we are exactly at midnight of the start of the rain year, then the start is actually
|
|
# the *previous* year
|
|
if time_dt.month == sory_mon \
|
|
and time_dt.day == 1 \
|
|
and time_dt.hour == 0 \
|
|
and time_dt.minute == 0 \
|
|
and time_dt.second == 0 \
|
|
and time_dt.microsecond == 0:
|
|
years_ago += 1
|
|
|
|
if time_dt.month < sory_mon:
|
|
years_ago += 1
|
|
|
|
year = time_dt.year - years_ago
|
|
|
|
return TimeSpan(int(time.mktime((year, sory_mon, 1, 0, 0, 0, 0, 0, -1))),
|
|
int(time.mktime((year + 1, sory_mon, 1, 0, 0, 0, 0, 0, -1))))
|
|
|
|
|
|
def timespan_by_name(label, time_ts, **kwargs):
|
|
"""Calculate an an appropriate TimeSpan"""
|
|
return {
|
|
'hour': archiveHoursAgoSpan,
|
|
'day': archiveDaySpan,
|
|
'week': archiveWeekSpan,
|
|
'month': archiveMonthSpan,
|
|
'year': archiveYearSpan,
|
|
'rainyear': archiveRainYearSpan
|
|
}[label](time_ts, **kwargs)
|
|
|
|
|
|
def stampgen(startstamp, stopstamp, interval):
|
|
"""Generator function yielding a sequence of timestamps, spaced interval apart.
|
|
|
|
The sequence will fall on the same local time boundary as startstamp.
|
|
|
|
Args:
|
|
startstamp (float): The start of the sequence in unix epoch time.
|
|
stopstamp (float): The end of the sequence in unix epoch time.
|
|
interval (int): The time length of an interval in seconds.
|
|
|
|
Yields:
|
|
float: yields a sequence of timestamps between startstamp and endstamp, inclusive.
|
|
|
|
Example:
|
|
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> startstamp = 1236560400
|
|
>>> print(timestamp_to_string(startstamp))
|
|
2009-03-08 18:00:00 PDT (1236560400)
|
|
>>> stopstamp = 1236607200
|
|
>>> print(timestamp_to_string(stopstamp))
|
|
2009-03-09 07:00:00 PDT (1236607200)
|
|
|
|
>>> for stamp in stampgen(startstamp, stopstamp, 10800):
|
|
... print(timestamp_to_string(stamp))
|
|
2009-03-08 18:00:00 PDT (1236560400)
|
|
2009-03-08 21:00:00 PDT (1236571200)
|
|
2009-03-09 00:00:00 PDT (1236582000)
|
|
2009-03-09 03:00:00 PDT (1236592800)
|
|
2009-03-09 06:00:00 PDT (1236603600)
|
|
|
|
Note that DST started in the middle of the sequence and that therefore the
|
|
actual time deltas between stamps is not necessarily 3 hours.
|
|
"""
|
|
dt = datetime.datetime.fromtimestamp(startstamp)
|
|
stop_dt = datetime.datetime.fromtimestamp(stopstamp)
|
|
if interval == 365.25 / 12 * 24 * 3600:
|
|
# Interval is a nominal month. This algorithm is
|
|
# necessary because not all months have the same length.
|
|
while dt <= stop_dt:
|
|
t_tuple = dt.timetuple()
|
|
yield time.mktime(t_tuple)
|
|
year = t_tuple[0]
|
|
month = t_tuple[1]
|
|
month += 1
|
|
if month > 12:
|
|
month -= 12
|
|
year += 1
|
|
dt = dt.replace(year=year, month=month)
|
|
else:
|
|
# This rather complicated algorithm is necessary (rather than just
|
|
# doing some time stamp arithmetic) because of the possibility that DST
|
|
# changes in the middle of an interval.
|
|
delta = datetime.timedelta(seconds=interval)
|
|
ts_last = 0
|
|
while dt <= stop_dt:
|
|
ts = int(time.mktime(dt.timetuple()))
|
|
# This check is necessary because time.mktime() cannot
|
|
# disambiguate between 2am ST and 3am DST. For example,
|
|
# time.mktime((2013, 3, 10, 2, 0, 0, 0, 0, -1)) and
|
|
# time.mktime((2013, 3, 10, 3, 0, 0, 0, 0, -1))
|
|
# both give the same value (1362909600)
|
|
if ts > ts_last:
|
|
yield ts
|
|
ts_last = ts
|
|
dt += delta
|
|
|
|
|
|
def intervalgen(start_ts, stop_ts, interval):
|
|
"""Generator function yielding a sequence of time spans whose boundaries
|
|
are on constant local time.
|
|
|
|
Args:
|
|
start_ts (float): The start of the first interval in unix epoch time. In unix epoch time.
|
|
stop_ts (float): The end of the last interval will be equal to or less than this.
|
|
In unix epoch time.
|
|
interval (int|str): The time length of an interval in seconds, or a shorthand description
|
|
(such as 'day', or 'hour', or '3d').
|
|
|
|
Yields:
|
|
TimeSpan: A sequence of TimeSpans. Both the start and end of the timespan
|
|
will be on the same time boundary as start_ts. See the example below.
|
|
|
|
Example:
|
|
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> startstamp = 1236477600
|
|
>>> print(timestamp_to_string(startstamp))
|
|
2009-03-07 18:00:00 PST (1236477600)
|
|
>>> stopstamp = 1236538800
|
|
>>> print(timestamp_to_string(stopstamp))
|
|
2009-03-08 12:00:00 PDT (1236538800)
|
|
|
|
>>> for span in intervalgen(startstamp, stopstamp, 10800):
|
|
... print(span)
|
|
[2009-03-07 18:00:00 PST (1236477600) -> 2009-03-07 21:00:00 PST (1236488400)]
|
|
[2009-03-07 21:00:00 PST (1236488400) -> 2009-03-08 00:00:00 PST (1236499200)]
|
|
[2009-03-08 00:00:00 PST (1236499200) -> 2009-03-08 03:00:00 PDT (1236506400)]
|
|
[2009-03-08 03:00:00 PDT (1236506400) -> 2009-03-08 06:00:00 PDT (1236517200)]
|
|
[2009-03-08 06:00:00 PDT (1236517200) -> 2009-03-08 09:00:00 PDT (1236528000)]
|
|
[2009-03-08 09:00:00 PDT (1236528000) -> 2009-03-08 12:00:00 PDT (1236538800)]
|
|
|
|
(Note how in this example the local time boundaries are constant, despite
|
|
DST kicking in. The interval length is not constant.)
|
|
|
|
Another example, this one over the Fall DST boundary, and using 1 hour intervals:
|
|
|
|
>>> startstamp = 1257051600
|
|
>>> print(timestamp_to_string(startstamp))
|
|
2009-10-31 22:00:00 PDT (1257051600)
|
|
>>> stopstamp = 1257080400
|
|
>>> print(timestamp_to_string(stopstamp))
|
|
2009-11-01 05:00:00 PST (1257080400)
|
|
>>> for span in intervalgen(startstamp, stopstamp, 3600):
|
|
... print(span)
|
|
[2009-10-31 22:00:00 PDT (1257051600) -> 2009-10-31 23:00:00 PDT (1257055200)]
|
|
[2009-10-31 23:00:00 PDT (1257055200) -> 2009-11-01 00:00:00 PDT (1257058800)]
|
|
[2009-11-01 00:00:00 PDT (1257058800) -> 2009-11-01 01:00:00 PDT (1257062400)]
|
|
[2009-11-01 01:00:00 PDT (1257062400) -> 2009-11-01 02:00:00 PST (1257069600)]
|
|
[2009-11-01 02:00:00 PST (1257069600) -> 2009-11-01 03:00:00 PST (1257073200)]
|
|
[2009-11-01 03:00:00 PST (1257073200) -> 2009-11-01 04:00:00 PST (1257076800)]
|
|
[2009-11-01 04:00:00 PST (1257076800) -> 2009-11-01 05:00:00 PST (1257080400)]
|
|
"""
|
|
|
|
dt1 = datetime.datetime.fromtimestamp(start_ts)
|
|
stop_dt = datetime.datetime.fromtimestamp(stop_ts)
|
|
|
|
# If a string was passed in, convert to seconds using nominal time intervals.
|
|
interval = nominal_spans(interval)
|
|
|
|
if interval == 365.25 / 12 * 24 * 3600:
|
|
# Interval is a nominal month. This algorithm is
|
|
# necessary because not all months have the same length.
|
|
while dt1 < stop_dt:
|
|
t_tuple = dt1.timetuple()
|
|
year = t_tuple[0]
|
|
month = t_tuple[1]
|
|
month += 1
|
|
if month > 12:
|
|
month -= 12
|
|
year += 1
|
|
dt2 = min(dt1.replace(year=year, month=month), stop_dt)
|
|
stamp1 = time.mktime(t_tuple)
|
|
stamp2 = time.mktime(dt2.timetuple())
|
|
yield TimeSpan(stamp1, stamp2)
|
|
dt1 = dt2
|
|
else:
|
|
# This rather complicated algorithm is necessary (rather than just
|
|
# doing some time stamp arithmetic) because of the possibility that DST
|
|
# changes in the middle of an interval
|
|
delta = datetime.timedelta(seconds=interval)
|
|
last_stamp1 = 0
|
|
while dt1 < stop_dt:
|
|
dt2 = min(dt1 + delta, stop_dt)
|
|
stamp1 = int(time.mktime(dt1.timetuple()))
|
|
stamp2 = int(time.mktime(dt2.timetuple()))
|
|
if stamp2 > stamp1 > last_stamp1:
|
|
yield TimeSpan(stamp1, stamp2)
|
|
last_stamp1 = stamp1
|
|
dt1 = dt2
|
|
|
|
|
|
def genHourSpans(start_ts, stop_ts):
|
|
"""Generator function that generates start/stop of hours in an inclusive range.
|
|
|
|
Args:
|
|
start_ts (float): A time stamp somewhere in the first day.
|
|
stop_ts (float): A time stamp somewhere in the last day.
|
|
|
|
Yields:
|
|
TimeSpan: Instance of TimeSpan, where the start is the time stamp
|
|
of the start of the day, the stop is the time stamp of the start
|
|
of the next day.
|
|
|
|
Example:
|
|
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> start_ts = 1204796460
|
|
>>> stop_ts = 1204818360
|
|
|
|
>>> print(timestamp_to_string(start_ts))
|
|
2008-03-06 01:41:00 PST (1204796460)
|
|
>>> print(timestamp_to_string(stop_ts))
|
|
2008-03-06 07:46:00 PST (1204818360)
|
|
|
|
>>> for span in genHourSpans(start_ts, stop_ts):
|
|
... print(span)
|
|
[2008-03-06 01:00:00 PST (1204794000) -> 2008-03-06 02:00:00 PST (1204797600)]
|
|
[2008-03-06 02:00:00 PST (1204797600) -> 2008-03-06 03:00:00 PST (1204801200)]
|
|
[2008-03-06 03:00:00 PST (1204801200) -> 2008-03-06 04:00:00 PST (1204804800)]
|
|
[2008-03-06 04:00:00 PST (1204804800) -> 2008-03-06 05:00:00 PST (1204808400)]
|
|
[2008-03-06 05:00:00 PST (1204808400) -> 2008-03-06 06:00:00 PST (1204812000)]
|
|
[2008-03-06 06:00:00 PST (1204812000) -> 2008-03-06 07:00:00 PST (1204815600)]
|
|
[2008-03-06 07:00:00 PST (1204815600) -> 2008-03-06 08:00:00 PST (1204819200)]
|
|
"""
|
|
_stop_dt = datetime.datetime.fromtimestamp(stop_ts)
|
|
_start_hour = int(start_ts / 3600)
|
|
_stop_hour = int(stop_ts / 3600)
|
|
if (_stop_dt.minute, _stop_dt.second) == (0, 0):
|
|
_stop_hour -= 1
|
|
|
|
for _hour in range(_start_hour, _stop_hour + 1):
|
|
yield TimeSpan(_hour * 3600, (_hour + 1) * 3600)
|
|
|
|
|
|
def genDaySpans(start_ts, stop_ts):
|
|
"""Generator function that generates start/stop of days in an inclusive range.
|
|
|
|
Args:
|
|
|
|
start_ts (float): A time stamp somewhere in the first day.
|
|
stop_ts (float): A time stamp somewhere in the last day.
|
|
|
|
Yields:
|
|
TimeSpan: A sequence of TimeSpans, where the start is the time stamp
|
|
of the start of the day, the stop is the time stamp of the start
|
|
of the next day.
|
|
|
|
Example:
|
|
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> start_ts = 1204796460
|
|
>>> stop_ts = 1205265720
|
|
|
|
>>> print(timestamp_to_string(start_ts))
|
|
2008-03-06 01:41:00 PST (1204796460)
|
|
>>> print(timestamp_to_string(stop_ts))
|
|
2008-03-11 13:02:00 PDT (1205265720)
|
|
|
|
>>> for span in genDaySpans(start_ts, stop_ts):
|
|
... print(span)
|
|
[2008-03-06 00:00:00 PST (1204790400) -> 2008-03-07 00:00:00 PST (1204876800)]
|
|
[2008-03-07 00:00:00 PST (1204876800) -> 2008-03-08 00:00:00 PST (1204963200)]
|
|
[2008-03-08 00:00:00 PST (1204963200) -> 2008-03-09 00:00:00 PST (1205049600)]
|
|
[2008-03-09 00:00:00 PST (1205049600) -> 2008-03-10 00:00:00 PDT (1205132400)]
|
|
[2008-03-10 00:00:00 PDT (1205132400) -> 2008-03-11 00:00:00 PDT (1205218800)]
|
|
[2008-03-11 00:00:00 PDT (1205218800) -> 2008-03-12 00:00:00 PDT (1205305200)]
|
|
|
|
Note that a daylight savings time change happened 8 March 2009.
|
|
"""
|
|
_start_dt = datetime.datetime.fromtimestamp(start_ts)
|
|
_stop_dt = datetime.datetime.fromtimestamp(stop_ts)
|
|
|
|
_start_ord = _start_dt.toordinal()
|
|
_stop_ord = _stop_dt.toordinal()
|
|
if (_stop_dt.hour, _stop_dt.minute, _stop_dt.second) == (0, 0, 0):
|
|
_stop_ord -= 1
|
|
|
|
for _ord in range(_start_ord, _stop_ord + 1):
|
|
yield TimeSpan(_ord_to_ts(_ord), _ord_to_ts(_ord + 1))
|
|
|
|
|
|
def genMonthSpans(start_ts, stop_ts):
|
|
"""Generator function that generates start/stop of months in an
|
|
inclusive range.
|
|
|
|
Args:
|
|
start_ts: A time stamp somewhere in the first month.
|
|
stop_ts: A time stamp somewhere in the last month.
|
|
|
|
Yields:
|
|
TimeSpan: A sequence of TimeSpans, where the start is the time stamp
|
|
of the start of the month, the stop is the time stamp of the start
|
|
of the next month.
|
|
|
|
Example:
|
|
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> start_ts = 1196705700
|
|
>>> stop_ts = 1206101100
|
|
>>> print("start time is %s" % timestamp_to_string(start_ts))
|
|
start time is 2007-12-03 10:15:00 PST (1196705700)
|
|
>>> print("stop time is %s" % timestamp_to_string(stop_ts))
|
|
stop time is 2008-03-21 05:05:00 PDT (1206101100)
|
|
|
|
>>> for span in genMonthSpans(start_ts, stop_ts):
|
|
... print(span)
|
|
[2007-12-01 00:00:00 PST (1196496000) -> 2008-01-01 00:00:00 PST (1199174400)]
|
|
[2008-01-01 00:00:00 PST (1199174400) -> 2008-02-01 00:00:00 PST (1201852800)]
|
|
[2008-02-01 00:00:00 PST (1201852800) -> 2008-03-01 00:00:00 PST (1204358400)]
|
|
[2008-03-01 00:00:00 PST (1204358400) -> 2008-04-01 00:00:00 PDT (1207033200)]
|
|
|
|
Note that a daylight savings time change happened 8 March 2009.
|
|
"""
|
|
if None in (start_ts, stop_ts):
|
|
return
|
|
_start_dt = datetime.date.fromtimestamp(start_ts)
|
|
_stop_date = datetime.datetime.fromtimestamp(stop_ts)
|
|
|
|
_start_month = 12 * _start_dt.year + _start_dt.month
|
|
_stop_month = 12 * _stop_date.year + _stop_date.month
|
|
|
|
if (_stop_date.day, _stop_date.hour, _stop_date.minute, _stop_date.second) == (1, 0, 0, 0):
|
|
_stop_month -= 1
|
|
|
|
for month in range(_start_month, _stop_month + 1):
|
|
_this_yr, _this_mo = divmod(month, 12)
|
|
_next_yr, _next_mo = divmod(month + 1, 12)
|
|
yield TimeSpan(int(time.mktime((_this_yr, _this_mo, 1, 0, 0, 0, 0, 0, -1))),
|
|
int(time.mktime((_next_yr, _next_mo, 1, 0, 0, 0, 0, 0, -1))))
|
|
|
|
|
|
def genYearSpans(start_ts, stop_ts):
|
|
if None in (start_ts, stop_ts):
|
|
return
|
|
_start_date = datetime.date.fromtimestamp(start_ts)
|
|
_stop_dt = datetime.datetime.fromtimestamp(stop_ts)
|
|
|
|
_start_year = _start_date.year
|
|
_stop_year = _stop_dt.year
|
|
|
|
if (_stop_dt.month, _stop_dt.day, _stop_dt.hour,
|
|
_stop_dt.minute, _stop_dt.second) == (1, 1, 0, 0, 0):
|
|
_stop_year -= 1
|
|
|
|
for year in range(_start_year, _stop_year + 1):
|
|
yield TimeSpan(int(time.mktime((year, 1, 1, 0, 0, 0, 0, 0, -1))),
|
|
int(time.mktime((year + 1, 1, 1, 0, 0, 0, 0, 0, -1))))
|
|
|
|
|
|
def startOfDay(time_ts):
|
|
"""Calculate the unix epoch time for the start of a (local time) day.
|
|
|
|
Args:
|
|
|
|
time_ts (float): A timestamp somewhere in the day for which the start-of-day
|
|
is desired.
|
|
|
|
Returns:
|
|
float: The timestamp for the start-of-day (00:00) in unix epoch time.
|
|
|
|
"""
|
|
_time_tt = time.localtime(time_ts)
|
|
_bod_ts = time.mktime((_time_tt.tm_year,
|
|
_time_tt.tm_mon,
|
|
_time_tt.tm_mday,
|
|
0, 0, 0, 0, 0, -1))
|
|
return int(_bod_ts)
|
|
|
|
|
|
def startOfGregorianDay(date_greg):
|
|
"""Given a Gregorian day, returns the start of the day in unix epoch time.
|
|
|
|
Args:
|
|
date_greg (int): A date as an ordinal Gregorian day.
|
|
|
|
Returns:
|
|
int: The local start of the day as a unix epoch time.
|
|
|
|
Example:
|
|
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> date_greg = 735973 # 10-Jan-2016
|
|
>>> print(startOfGregorianDay(date_greg))
|
|
1452412800
|
|
"""
|
|
date_dt = datetime.datetime.fromordinal(date_greg)
|
|
date_tt = date_dt.timetuple()
|
|
sod_ts = int(time.mktime(date_tt))
|
|
return sod_ts
|
|
|
|
|
|
def toGregorianDay(time_ts):
|
|
"""Return the Gregorian day a timestamp belongs to.
|
|
|
|
Args:
|
|
time_ts (float): A time in unix epoch time.
|
|
|
|
Returns:
|
|
int: The ordinal Gregorian day that contains that time
|
|
|
|
Example:
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> time_ts = 1452412800 # Midnight, 10-Jan-2016
|
|
>>> print(toGregorianDay(time_ts))
|
|
735972
|
|
>>> time_ts = 1452412801 # Just after midnight, 10-Jan-2016
|
|
>>> print(toGregorianDay(time_ts))
|
|
735973
|
|
"""
|
|
|
|
date_dt = datetime.datetime.fromtimestamp(time_ts)
|
|
date_greg = date_dt.toordinal()
|
|
if date_dt.hour == date_dt.minute == date_dt.second == date_dt.microsecond == 0:
|
|
# Midnight actually belongs to the previous day
|
|
date_greg -= 1
|
|
return date_greg
|
|
|
|
|
|
def startOfDayUTC(time_ts):
|
|
"""Calculate the unix epoch time for the start of a UTC day.
|
|
|
|
Args:
|
|
time_ts (float): A timestamp somewhere in the day for which the start-of-day
|
|
is desired.
|
|
|
|
Returns:
|
|
int: The timestamp for the start-of-day (00:00) in unix epoch time.
|
|
|
|
"""
|
|
_time_tt = time.gmtime(time_ts)
|
|
_bod_ts = calendar.timegm((_time_tt.tm_year,
|
|
_time_tt.tm_mon,
|
|
_time_tt.tm_mday,
|
|
0, 0, 0, 0, 0, -1))
|
|
return int(_bod_ts)
|
|
|
|
|
|
def startOfArchiveDay(time_ts):
|
|
"""Given an archive time stamp, calculate its start of day.
|
|
|
|
Similar to startOfDay(), except that an archive stamped at midnight
|
|
actually belongs to the *previous* day.
|
|
|
|
Args:
|
|
time_ts (float): A timestamp somewhere in the day for which the start-of-day
|
|
is desired.
|
|
|
|
Returns:
|
|
float: The timestamp for the start-of-day (00:00) in unix epoch time."""
|
|
|
|
time_dt = datetime.datetime.fromtimestamp(time_ts)
|
|
start_of_day_dt = time_dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
# If we are exactly on the midnight boundary, the start of the archive day is actually
|
|
# the *previous* day.
|
|
if time_dt.hour == 0 \
|
|
and time_dt.minute == 0 \
|
|
and time_dt.second == 0 \
|
|
and time_dt.microsecond == 0:
|
|
start_of_day_dt -= datetime.timedelta(days=1)
|
|
start_of_day_tt = start_of_day_dt.timetuple()
|
|
start_of_day_ts = int(time.mktime(start_of_day_tt))
|
|
return start_of_day_ts
|
|
|
|
|
|
def getDayNightTransitions(start_ts, end_ts, lat, lon):
|
|
"""Return the day-night transitions between the start and end times.
|
|
|
|
Args:
|
|
|
|
start_ts (float or int): A timestamp (UTC) indicating the beginning of the period
|
|
end_ts (float or int): A timestamp (UTC) indicating the end of the period
|
|
lat (float): The latitude in degrees
|
|
lon (float): The longitude in degrees
|
|
|
|
Returns:
|
|
tuple: A two-way tuple, The first element is either the string 'day' or 'night'.
|
|
If 'day', the first transition is from day to night.
|
|
If 'night', the first transition is from night to day.
|
|
The second element is a sequence of transition times in unix epoch times.
|
|
|
|
Example:
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> startstamp = 1658428400
|
|
>>> # Stop stamp is three days later:
|
|
>>> stopstamp = startstamp + 3 * 24 * 3600
|
|
>>> print(timestamp_to_string(startstamp))
|
|
2022-07-21 11:33:20 PDT (1658428400)
|
|
>>> print(timestamp_to_string(stopstamp))
|
|
2022-07-24 11:33:20 PDT (1658687600)
|
|
>>> whichway, transitions = getDayNightTransitions(startstamp, stopstamp, 45, -122)
|
|
>>> print(whichway)
|
|
day
|
|
>>> for x in transitions:
|
|
... print(timestamp_to_string(x))
|
|
2022-07-21 20:47:00 PDT (1658461620)
|
|
2022-07-22 05:42:58 PDT (1658493778)
|
|
2022-07-22 20:46:02 PDT (1658547962)
|
|
2022-07-23 05:44:00 PDT (1658580240)
|
|
2022-07-23 20:45:03 PDT (1658634303)
|
|
2022-07-24 05:45:04 PDT (1658666704)
|
|
"""
|
|
from weeutil import Sun
|
|
|
|
start_ts = int(start_ts)
|
|
end_ts = int(end_ts)
|
|
|
|
first = None
|
|
values = []
|
|
for t in range(start_ts - 3600 * 24, end_ts + 3600 * 24 + 1, 3600 * 24):
|
|
x = startOfDayUTC(t)
|
|
x_tt = time.gmtime(x)
|
|
y, m, d = x_tt[:3]
|
|
(sunrise_utc, sunset_utc) = Sun.sunRiseSet(y, m, d, lon, lat)
|
|
daystart_ts = calendar.timegm((y, m, d, 0, 0, 0, 0, 0, -1))
|
|
sunrise_ts = int(daystart_ts + sunrise_utc * 3600.0 + 0.5)
|
|
sunset_ts = int(daystart_ts + sunset_utc * 3600.0 + 0.5)
|
|
|
|
if start_ts < sunrise_ts < end_ts:
|
|
values.append(sunrise_ts)
|
|
if first is None:
|
|
first = 'night'
|
|
if start_ts < sunset_ts < end_ts:
|
|
values.append(sunset_ts)
|
|
if first is None:
|
|
first = 'day'
|
|
return first, values
|
|
|
|
|
|
# The following has been replaced by the I18N-aware function delta_secs_to_string in units.py:
|
|
|
|
# def secs_to_string(secs):
|
|
# """Convert seconds to a string with days, hours, and minutes"""
|
|
# str_list = []
|
|
# for (label, interval) in (('day', 86400), ('hour', 3600), ('minute', 60)):
|
|
# amt = int(secs / interval)
|
|
# plural = u'' if amt == 1 else u's'
|
|
# str_list.append(u"%d %s%s" % (amt, label, plural))
|
|
# secs %= interval
|
|
# ans = ', '.join(str_list)
|
|
# return ans
|
|
|
|
|
|
def timestamp_to_string(ts, format_str="%Y-%m-%d %H:%M:%S %Z"):
|
|
"""Return a string formatted from the timestamp
|
|
|
|
Example:
|
|
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> print(timestamp_to_string(1196705700))
|
|
2007-12-03 10:15:00 PST (1196705700)
|
|
>>> print(timestamp_to_string(None))
|
|
******* N/A ******* ( N/A )
|
|
"""
|
|
if ts is not None:
|
|
return "%s (%d)" % (time.strftime(format_str, time.localtime(ts)), ts)
|
|
else:
|
|
return "******* N/A ******* ( N/A )"
|
|
|
|
|
|
def timestamp_to_gmtime(ts):
|
|
"""Return a string formatted for GMT
|
|
|
|
>>> print(timestamp_to_gmtime(1196705700))
|
|
2007-12-03 18:15:00 UTC (1196705700)
|
|
>>> print(timestamp_to_gmtime(None))
|
|
******* N/A ******* ( N/A )
|
|
"""
|
|
if ts:
|
|
return "%s (%d)" % (time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime(ts)), ts)
|
|
else:
|
|
return "******* N/A ******* ( N/A )"
|
|
|
|
|
|
def utc_to_ts(y, m, d, hrs_utc):
|
|
"""Converts from a tuple-time in UTC to unix epoch time.
|
|
|
|
Args:
|
|
|
|
y (int): The year for which the conversion is desired.
|
|
m (int): The month.
|
|
d (int): The day.
|
|
hrs_utc (float): Floating point number with the number of hours since midnight in UTC.
|
|
|
|
Returns:
|
|
int: The corresponding unix epoch time.
|
|
|
|
>>> print(utc_to_ts(2009, 3, 27, 14.5))
|
|
1238164200
|
|
"""
|
|
# Construct a time tuple with the time at midnight, UTC:
|
|
daystart_utc_tt = (y, m, d, 0, 0, 0, 0, 0, -1)
|
|
# Convert the time tuple to a time stamp and add on the number of seconds since midnight:
|
|
time_ts = int(calendar.timegm(daystart_utc_tt) + hrs_utc * 3600.0 + 0.5)
|
|
return time_ts
|
|
|
|
|
|
def utc_to_local_tt(y, m, d, hrs_utc):
|
|
"""Converts from a UTC time to a local time.
|
|
|
|
y,m,d: The year, month, day for which the conversion is desired.
|
|
|
|
hrs_tc: Floating point number with the number of hours since midnight in UTC.
|
|
|
|
Returns: A timetuple with the local time.
|
|
|
|
>>> os.environ['TZ'] = 'America/Los_Angeles'
|
|
>>> time.tzset()
|
|
>>> tt=utc_to_local_tt(2009, 3, 27, 14.5)
|
|
>>> print(tt.tm_year, tt.tm_mon, tt.tm_mday, tt.tm_hour, tt.tm_min)
|
|
2009 3 27 7 30
|
|
"""
|
|
# Get the UTC time:
|
|
time_ts = utc_to_ts(y, m, d, hrs_utc)
|
|
# Convert to local time:
|
|
time_local_tt = time.localtime(time_ts)
|
|
return time_local_tt
|
|
|
|
|
|
def latlon_string(ll, hemi, which, format_list=None):
|
|
"""Decimal degrees into a string for degrees, and one for minutes.
|
|
|
|
Args:
|
|
ll (float): The decimal latitude or longitude
|
|
hemi (list|tuple): A tuple holding strings representing positive or negative values.
|
|
E.g.: ('N', 'S', 'E', 'W')
|
|
which (str): 'lat' for latitude, 'lon' for longitude
|
|
format_list (list|tuple): A list or tuple holding the format strings to be used.
|
|
These are [whole degrees latitude, whole degrees longitude, minutes]
|
|
|
|
Returns:
|
|
tuple: A 3-way tuple holding (latlon whole degrees, latlon minutes,
|
|
hemisphere designator). Example: ('022', '08.3', 'N')
|
|
"""
|
|
labs = abs(ll)
|
|
frac, deg = math.modf(labs)
|
|
minutes = frac * 60.0
|
|
format_list = format_list or ["%02d", "%03d", "%05.2f"]
|
|
return ((format_list[0] if which == 'lat' else format_list[1]) % deg,
|
|
format_list[2] % minutes,
|
|
hemi[0] if ll >= 0 else hemi[1])
|
|
|
|
|
|
def get_object(module_class):
|
|
"""Given a string with a module class name, it imports and returns the class."""
|
|
# Split the path into its parts
|
|
parts = module_class.split('.')
|
|
# Strip off the classname:
|
|
module = '.'.join(parts[:-1])
|
|
# Import the top level module
|
|
mod = __import__(module)
|
|
# Recursively work down from the top level module to the class name.
|
|
# Be prepared to catch an exception if something cannot be found.
|
|
try:
|
|
for part in parts[1:]:
|
|
mod = getattr(mod, part)
|
|
except AttributeError:
|
|
# Can't find something. Give a more informative error message:
|
|
raise AttributeError(
|
|
"Module '%s' has no attribute '%s' when searching for '%s'"
|
|
% (mod.__name__, part, module_class))
|
|
return mod
|
|
|
|
|
|
# For backwards compatibility:
|
|
_get_object = get_object
|
|
|
|
|
|
class GenWithPeek(object):
|
|
"""Generator object which allows a peek at the next object to be returned.
|
|
|
|
Sometimes Python solves a complicated problem with such elegance! This is
|
|
one of them.
|
|
|
|
Example of usage:
|
|
>>> # Define a generator function:
|
|
>>> def genfunc(N):
|
|
... for j in range(N):
|
|
... yield j
|
|
>>>
|
|
>>> # Now wrap it with the GenWithPeek object:
|
|
>>> g_with_peek = GenWithPeek(genfunc(5))
|
|
>>> # We can iterate through the object as normal:
|
|
>>> for i in g_with_peek:
|
|
... print(i)
|
|
... # Every second object, let's take a peek ahead
|
|
... if i%2:
|
|
... # We can get a peek at the next object without disturbing the wrapped generator:
|
|
... print("peeking ahead, the next object will be: %s" % g_with_peek.peek())
|
|
0
|
|
1
|
|
peeking ahead, the next object will be: 2
|
|
2
|
|
3
|
|
peeking ahead, the next object will be: 4
|
|
4
|
|
"""
|
|
|
|
def __init__(self, generator):
|
|
"""Initialize the generator object.
|
|
|
|
generator: A generator object to be wrapped
|
|
"""
|
|
self.generator = generator
|
|
self.have_peek = False
|
|
self.peek_obj = None
|
|
|
|
def __iter__(self):
|
|
return self
|
|
|
|
def __next__(self):
|
|
"""Advance to the next object"""
|
|
if self.have_peek:
|
|
self.have_peek = False
|
|
return self.peek_obj
|
|
else:
|
|
return next(self.generator)
|
|
|
|
# For Python 2:
|
|
next = __next__
|
|
|
|
def peek(self):
|
|
"""Take a peek at the next object"""
|
|
if not self.have_peek:
|
|
self.peek_obj = next(self.generator)
|
|
self.have_peek = True
|
|
return self.peek_obj
|
|
|
|
# For Python 3 compatiblity
|
|
__next__ = next
|
|
|
|
|
|
class GenByBatch(object):
|
|
"""Generator wrapper. Calls the wrapped generator in batches of a specified size."""
|
|
|
|
def __init__(self, generator, batch_size=0):
|
|
"""Initialize an instance of GenWithConvert
|
|
|
|
generator: An iterator which will be wrapped.
|
|
|
|
batch_size: The number of items to fetch in a batch.
|
|
"""
|
|
self.generator = generator
|
|
self.batch_size = batch_size
|
|
self.batch_buffer = []
|
|
|
|
def __iter__(self):
|
|
return self
|
|
|
|
def __next__(self):
|
|
# If there isn't anything in the buffer, fetch new items
|
|
if not self.batch_buffer:
|
|
# Fetch in batches of 'batch_size'.
|
|
count = 0
|
|
for item in self.generator:
|
|
self.batch_buffer.append(item)
|
|
count += 1
|
|
# If batch_size is zero, that means fetch everything in one big batch, so keep
|
|
# going. Otherwise, break when we have fetched 'batch_size" items.
|
|
if self.batch_size and count >= self.batch_size:
|
|
break
|
|
# If there's still nothing in the buffer, we're done. Stop the iteration. Otherwise,
|
|
# return the first item in the buffer.
|
|
if self.batch_buffer:
|
|
return self.batch_buffer.pop(0)
|
|
else:
|
|
raise StopIteration
|
|
|
|
# For Python 2:
|
|
next = __next__
|
|
|
|
|
|
def tobool(x):
|
|
"""Convert an object to boolean.
|
|
|
|
Examples:
|
|
>>> print(tobool('TRUE'))
|
|
True
|
|
>>> print(tobool(True))
|
|
True
|
|
>>> print(tobool(1))
|
|
True
|
|
>>> print(tobool('FALSE'))
|
|
False
|
|
>>> print(tobool(False))
|
|
False
|
|
>>> print(tobool(0))
|
|
False
|
|
>>> print(tobool('Foo'))
|
|
Traceback (most recent call last):
|
|
ValueError: Unknown boolean specifier: 'Foo'.
|
|
>>> print(tobool(None))
|
|
Traceback (most recent call last):
|
|
ValueError: Unknown boolean specifier: 'None'.
|
|
"""
|
|
|
|
try:
|
|
if x.lower() in ('true', 'yes', 'y'):
|
|
return True
|
|
elif x.lower() in ('false', 'no', 'n'):
|
|
return False
|
|
except AttributeError:
|
|
pass
|
|
try:
|
|
return bool(int(x))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
raise ValueError("Unknown boolean specifier: '%s'." % x)
|
|
|
|
|
|
to_bool = tobool
|
|
|
|
|
|
def to_int(x):
|
|
"""Convert an object to an integer, unless it is None
|
|
|
|
Examples:
|
|
>>> print(to_int(123))
|
|
123
|
|
>>> print(to_int('123'))
|
|
123
|
|
>>> print(to_int(-5.2))
|
|
-5
|
|
>>> print(to_int(None))
|
|
None
|
|
"""
|
|
if isinstance(x, str) and (x.lower() == 'none' or x == ''):
|
|
x = None
|
|
try:
|
|
return int(x) if x is not None else None
|
|
except ValueError:
|
|
# Perhaps it's a string, holding a floating point number?
|
|
return int(float(x))
|
|
|
|
|
|
def to_float(x):
|
|
"""Convert an object to a float, unless it is None
|
|
|
|
Examples:
|
|
>>> print(to_float(12.3))
|
|
12.3
|
|
>>> print(to_float('12.3'))
|
|
12.3
|
|
>>> print(to_float(None))
|
|
None
|
|
"""
|
|
if isinstance(x, str) and x.lower() == 'none':
|
|
x = None
|
|
return float(x) if x is not None else None
|
|
|
|
|
|
def to_complex(magnitude, direction):
|
|
"""Convert from magnitude and direction to a complex number."""
|
|
if magnitude is None:
|
|
value = None
|
|
elif magnitude == 0:
|
|
# If magnitude is zero, it doesn't matter what direction is. Can even be None.
|
|
value = complex(0.0, 0.0)
|
|
elif direction is None:
|
|
# Magnitude must be non-zero, but we don't know the direction.
|
|
value = None
|
|
else:
|
|
# Magnitude is non-zero, and we have a good direction.
|
|
x = magnitude * math.cos(math.radians(90.0 - direction))
|
|
y = magnitude * math.sin(math.radians(90.0 - direction))
|
|
value = complex(x, y)
|
|
return value
|
|
|
|
|
|
def dirN(c):
|
|
"""Given a complex number, return its phase as a compass heading"""
|
|
if c is None:
|
|
value = None
|
|
else:
|
|
value = (450 - math.degrees(cmath.phase(c))) % 360.0
|
|
return value
|
|
|
|
|
|
class Polar(object):
|
|
"""Polar notation, except the direction is a compass heading."""
|
|
|
|
def __init__(self, mag, dir):
|
|
self.mag = mag
|
|
self.dir = dir
|
|
|
|
@classmethod
|
|
def from_complex(cls, c):
|
|
return cls(abs(c), dirN(c))
|
|
|
|
def __str__(self):
|
|
return "(%s, %s)" % (self.mag, self.dir)
|
|
|
|
def __eq__(self, other):
|
|
return self.mag == other.mag and self.dir == other.dir
|
|
|
|
|
|
def rounder(x, ndigits):
|
|
"""Round a number, or sequence of numbers, to a specified number of decimal digits
|
|
|
|
Args:
|
|
x (None, float, complex, list): The number or sequence of numbers to be rounded. If the
|
|
argument is None, then None will be returned.
|
|
ndigits (int): The number of decimal digits to retain.
|
|
|
|
Returns:
|
|
None, float, complex, list: Returns the number, or sequence of numbers, with the requested
|
|
number of decimal digits. If 'None', no rounding is done, and the function returns
|
|
the original value.
|
|
"""
|
|
if ndigits is None:
|
|
return x
|
|
elif x is None:
|
|
return None
|
|
elif isinstance(x, complex):
|
|
return complex(round(x.real, ndigits), round(x.imag, ndigits))
|
|
elif isinstance(x, Polar):
|
|
return Polar(round(x.mag, ndigits), round(x.dir, ndigits))
|
|
elif isinstance(x, float):
|
|
return round(x, ndigits) if ndigits else int(x)
|
|
elif is_iterable(x):
|
|
return [rounder(v, ndigits) for v in x]
|
|
return x
|
|
|
|
|
|
def min_with_none(x_seq):
|
|
"""Find the minimum in a (possibly empty) sequence, ignoring Nones"""
|
|
xmin = None
|
|
for x in x_seq:
|
|
if xmin is None:
|
|
xmin = x
|
|
elif x is not None:
|
|
xmin = min(x, xmin)
|
|
return xmin
|
|
|
|
|
|
def max_with_none(x_seq):
|
|
"""Find the maximum in a (possibly empty) sequence, ignoring Nones.
|
|
|
|
While this function is not necessary under Python 2, under Python 3 it is.
|
|
"""
|
|
xmax = None
|
|
for x in x_seq:
|
|
if xmax is None:
|
|
xmax = x
|
|
elif x is not None:
|
|
xmax = max(x, xmax)
|
|
return xmax
|
|
|
|
|
|
def move_with_timestamp(path):
|
|
"""Save a file or directory to a path with a timestamp."""
|
|
import shutil
|
|
# Sometimes the target has a trailing '/'. This will take care of it:
|
|
path = os.path.normpath(path)
|
|
newpath = path + time.strftime(".%Y%m%d%H%M%S")
|
|
# Check to see if this name already exists
|
|
if os.path.exists(newpath):
|
|
# It already exists. Stick a version number on it:
|
|
version = 1
|
|
while os.path.exists(newpath + '-' + str(version)):
|
|
version += 1
|
|
newpath = newpath + '-' + str(version)
|
|
shutil.move(path, newpath)
|
|
return newpath
|
|
|
|
|
|
try:
|
|
# Python 3
|
|
from collections import ChainMap
|
|
|
|
|
|
class ListOfDicts(ChainMap):
|
|
def extend(self, m):
|
|
self.maps.append(m)
|
|
|
|
def prepend(self, m):
|
|
self.maps.insert(0, m)
|
|
|
|
except ImportError:
|
|
|
|
# Python 2. We'll have to supply our own
|
|
class ListOfDicts(object):
|
|
"""A near clone of ChainMap"""
|
|
|
|
def __init__(self, *maps):
|
|
self.maps = list(maps) or [{}]
|
|
|
|
def __missing__(self, key):
|
|
raise KeyError(key)
|
|
|
|
def __getitem__(self, key):
|
|
for mapping in self.maps:
|
|
try:
|
|
return mapping[key]
|
|
except KeyError:
|
|
pass
|
|
return self.__missing__(key)
|
|
|
|
def get(self, key, default=None):
|
|
return self[key] if key in self else default
|
|
|
|
def __len__(self):
|
|
return len(set().union(*self.maps))
|
|
|
|
def __iter__(self):
|
|
return iter(set().union(*self.maps))
|
|
|
|
def __contains__(self, key):
|
|
return any(key in m for m in self.maps)
|
|
|
|
def __bool__(self):
|
|
return any(self.maps)
|
|
|
|
def __setitem__(self, key, value):
|
|
"""Set a key, value on the first map. """
|
|
self.maps[0][key] = value
|
|
|
|
def __delitem__(self, key):
|
|
try:
|
|
del self.maps[0][key]
|
|
except KeyError:
|
|
raise KeyError('Key not found in the first mapping: {!r}'.format(key))
|
|
|
|
def popitem(self):
|
|
'Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty.'
|
|
try:
|
|
return self.maps[0].popitem()
|
|
except KeyError:
|
|
raise KeyError('No keys found in the first mapping.')
|
|
|
|
def pop(self, key, *args):
|
|
'Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0].'
|
|
try:
|
|
return self.maps[0].pop(key, *args)
|
|
except KeyError:
|
|
raise KeyError('Key not found in the first mapping: {!r}'.format(key))
|
|
|
|
def extend(self, m):
|
|
self.maps.append(m)
|
|
|
|
def prepend(self, m):
|
|
self.maps.insert(0, m)
|
|
|
|
def copy(self):
|
|
return self.__class__(self.maps[0].copy(), *self.maps[1:])
|
|
|
|
__copy__ = copy
|
|
|
|
def keys(self):
|
|
"""Return an iterator of all keys in the maps."""
|
|
return iter(i for s in self.maps for i in s)
|
|
|
|
def values(self):
|
|
"""Return an iterator of all values in the maps."""
|
|
return iter(i for s in self.maps for i in s.values())
|
|
|
|
|
|
class KeyDict(dict):
|
|
"""A dictionary that returns the key for an unsuccessful lookup."""
|
|
|
|
def __missing__(self, key):
|
|
return key
|
|
|
|
|
|
def atoi(text):
|
|
return int(text) if text.isdigit() else text
|
|
|
|
|
|
def natural_keys(text):
|
|
"""Natural key sort.
|
|
|
|
Allows use of key=natural_keys to sort a list in human order, eg:
|
|
alist.sort(key=natural_keys)
|
|
|
|
http://nedbatchelder.com/blog/200712/human_sorting.html
|
|
"""
|
|
|
|
return [atoi(c) for c in re.split(natural_keys.compiled_re, text.lower())]
|
|
|
|
|
|
natural_keys.compiled_re = re.compile(r'(\d+)')
|
|
|
|
|
|
def natural_sort_keys(source_dict):
|
|
"""Return a naturally sorted list of keys for a dict."""
|
|
|
|
# create a list of keys in the dict
|
|
keys_list = list(source_dict.keys())
|
|
# naturally sort the list of keys such that, for example, xxxxx16 appears
|
|
# after xxxxx1
|
|
keys_list.sort(key=natural_keys)
|
|
# return the sorted list
|
|
return keys_list
|
|
|
|
|
|
def to_sorted_string(rec, simple_sort=False):
|
|
"""Return a string representation of a dict sorted by key.
|
|
|
|
Default action is to perform a 'natural' sort by key, ie 'xxx1' appears
|
|
before 'xxx16'. If called with simple_sort=True a simple alphanumeric sort
|
|
is performed instead which will result in 'xxx16' appearing before 'xxx1'.
|
|
"""
|
|
|
|
if simple_sort:
|
|
import locale
|
|
return ", ".join(["%s: %s" % (k, rec.get(k)) for k in sorted(rec, key=locale.strxfrm)])
|
|
else:
|
|
# first obtain a list of key:value pairs sorted naturally by key
|
|
sorted_dict = ["'%s': '%s'" % (k, rec[k]) for k in natural_sort_keys(rec)]
|
|
# return as a string of comma separated key:value pairs in braces
|
|
return ", ".join(sorted_dict)
|
|
|
|
|
|
def y_or_n(msg, noprompt=False, default=None):
|
|
"""Prompt and look for a 'y' or 'n' response"""
|
|
|
|
# If noprompt is truthy, return the default
|
|
if noprompt:
|
|
return 'y' if default is None else default
|
|
|
|
while True:
|
|
ans = input(msg).strip().lower()
|
|
if not ans and default is not None:
|
|
return default
|
|
elif ans in ('y', 'n'):
|
|
return ans
|
|
|
|
|
|
def deep_copy_path(path, dest_dir):
|
|
"""Copy a path to a destination, making any subdirectories along the way.
|
|
The source path is relative to the current directory.
|
|
|
|
Returns the number of files copied
|
|
"""
|
|
|
|
ncopy = 0
|
|
# Are we copying a directory?
|
|
if os.path.isdir(path):
|
|
# Yes. Walk it
|
|
for dirpath, _, filenames in os.walk(path):
|
|
for f in filenames:
|
|
# For each source file found, call myself recursively:
|
|
ncopy += deep_copy_path(os.path.join(dirpath, f), dest_dir)
|
|
else:
|
|
# path is a file. Get the directory it's in.
|
|
d = os.path.dirname(os.path.join(dest_dir, path))
|
|
# Make the destination directory, wrapping it in a try block in
|
|
# case it already exists:
|
|
try:
|
|
os.makedirs(d)
|
|
except OSError:
|
|
pass
|
|
# This version of copy does not copy over modification time,
|
|
# so it will look like a new file, causing it to be (for
|
|
# example) ftp'd to the server:
|
|
shutil.copy(path, d)
|
|
ncopy += 1
|
|
return ncopy
|
|
|
|
|
|
def is_iterable(x):
|
|
"""Test if something is iterable, but not a string"""
|
|
return hasattr(x, '__iter__') and not isinstance(x, (bytes, str))
|
|
|
|
|
|
class bcolors:
|
|
"""Colors used for terminals"""
|
|
HEADER = '\033[95m'
|
|
OKBLUE = '\033[94m'
|
|
OKCYAN = '\033[96m'
|
|
OKGREEN = '\033[92m'
|
|
WARNING = '\033[93m'
|
|
FAIL = '\033[91m'
|
|
ENDC = '\033[0m'
|
|
BOLD = '\033[1m'
|
|
UNDERLINE = '\033[4m'
|
|
|
|
|
|
def version_compare(v1, v2):
|
|
"""Compare two version numbers
|
|
|
|
Args:
|
|
v1(str): The first version number as a string. Can be something like '4.5.1a1'
|
|
v2(str): The second version number as a string.
|
|
|
|
Returns:
|
|
int: Returns +1 if v1 is greater than v2, -1 if less than, 0 if they are the same.
|
|
"""
|
|
|
|
import itertools
|
|
|
|
mash = itertools.zip_longest(v1.split('.'), v2.split('.'), fillvalue='0')
|
|
|
|
for x1, x2 in mash:
|
|
if x1 > x2:
|
|
return 1
|
|
if x1 < x2:
|
|
return -1
|
|
return 0
|
|
|
|
|
|
def path_to_resource(package, resource):
|
|
"""Return a path to a resource within a package. The resource can be a directory or a file."""
|
|
import sys
|
|
import importlib.resources
|
|
|
|
if sys.version_info.major == 3 and sys.version_info.minor < 9:
|
|
# For earlier Python versions, use the deprecated function path()
|
|
return importlib.resources.path(package, resource)
|
|
else:
|
|
# For later versions...
|
|
return importlib.resources.as_file(importlib.resources.files(package).joinpath(resource))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import doctest
|
|
|
|
if not doctest.testmod().failed:
|
|
print("PASSED")
|