mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-22 10:43:38 +00:00
639 lines
17 KiB
Python
639 lines
17 KiB
Python
"""
|
|
Various utilities.
|
|
"""
|
|
|
|
import re
|
|
import stat
|
|
from datetime import datetime, timedelta, MAXYEAR
|
|
from warnings import warn
|
|
|
|
|
|
def deprecated(comment=None):
|
|
"""
|
|
This is a decorator which can be used to mark functions
|
|
as deprecated. It will result in a warning being emmitted
|
|
when the function is used.
|
|
|
|
Examples: ::
|
|
|
|
@deprecated
|
|
def oldfunc(): ...
|
|
|
|
@deprecated("use newfunc()!")
|
|
def oldfunc2(): ...
|
|
|
|
Code from: http://code.activestate.com/recipes/391367/
|
|
"""
|
|
def _deprecated(func):
|
|
def newFunc(*args, **kwargs):
|
|
message = "Call to deprecated function %s" % func.__name__
|
|
if comment:
|
|
message += ": " + comment
|
|
warn(message, category=DeprecationWarning, stacklevel=2)
|
|
return func(*args, **kwargs)
|
|
newFunc.__name__ = func.__name__
|
|
newFunc.__doc__ = func.__doc__
|
|
newFunc.__dict__.update(func.__dict__)
|
|
return newFunc
|
|
return _deprecated
|
|
|
|
|
|
def paddingSize(value, align):
|
|
"""
|
|
Compute size of a padding field.
|
|
|
|
>>> paddingSize(31, 4)
|
|
1
|
|
>>> paddingSize(32, 4)
|
|
0
|
|
>>> paddingSize(33, 4)
|
|
3
|
|
|
|
Note: (value + paddingSize(value, align)) == alignValue(value, align)
|
|
"""
|
|
if value % align != 0:
|
|
return align - (value % align)
|
|
else:
|
|
return 0
|
|
|
|
|
|
def alignValue(value, align):
|
|
"""
|
|
Align a value to next 'align' multiple.
|
|
|
|
>>> alignValue(31, 4)
|
|
32
|
|
>>> alignValue(32, 4)
|
|
32
|
|
>>> alignValue(33, 4)
|
|
36
|
|
|
|
Note: alignValue(value, align) == (value + paddingSize(value, align))
|
|
"""
|
|
|
|
if value % align != 0:
|
|
return value + align - (value % align)
|
|
else:
|
|
return value
|
|
|
|
|
|
def timedelta2seconds(delta):
|
|
"""
|
|
Convert a datetime.timedelta() objet to a number of second
|
|
(floatting point number).
|
|
|
|
>>> timedelta2seconds(timedelta(seconds=2, microseconds=40000))
|
|
2.04
|
|
>>> timedelta2seconds(timedelta(minutes=1, milliseconds=250))
|
|
60.25
|
|
"""
|
|
return delta.microseconds / 1000000.0 \
|
|
+ delta.seconds + delta.days * 60 * 60 * 24
|
|
|
|
|
|
def humanDurationNanosec(nsec):
|
|
"""
|
|
Convert a duration in nanosecond to human natural representation.
|
|
Returns an unicode string.
|
|
|
|
>>> humanDurationNanosec(60417893)
|
|
'60.42 ms'
|
|
"""
|
|
|
|
# Nano second
|
|
if nsec < 1000:
|
|
return "%u nsec" % nsec
|
|
|
|
# Micro seconds
|
|
usec, nsec = divmod(nsec, 1000)
|
|
if usec < 1000:
|
|
return "%.2f usec" % (usec + float(nsec) / 1000)
|
|
|
|
# Milli seconds
|
|
msec, usec = divmod(usec, 1000)
|
|
if msec < 1000:
|
|
return "%.2f ms" % (msec + float(usec) / 1000)
|
|
return humanDuration(msec)
|
|
|
|
|
|
def humanDuration(delta):
|
|
"""
|
|
Convert a duration in millisecond to human natural representation.
|
|
Returns an unicode string.
|
|
|
|
>>> humanDuration(0)
|
|
'0 ms'
|
|
>>> humanDuration(213)
|
|
'213 ms'
|
|
>>> humanDuration(4213)
|
|
'4 sec 213 ms'
|
|
>>> humanDuration(6402309)
|
|
'1 hours 46 min 42 sec'
|
|
"""
|
|
if not isinstance(delta, timedelta):
|
|
delta = timedelta(microseconds=delta * 1000)
|
|
|
|
# Milliseconds
|
|
text = []
|
|
if 1000 <= delta.microseconds:
|
|
text.append("%u ms" % (delta.microseconds // 1000))
|
|
|
|
# Seconds
|
|
minutes, seconds = divmod(delta.seconds, 60)
|
|
hours, minutes = divmod(minutes, 60)
|
|
if seconds:
|
|
text.append("%u sec" % seconds)
|
|
if minutes:
|
|
text.append("%u min" % minutes)
|
|
if hours:
|
|
text.append("%u hours" % hours)
|
|
|
|
# Days
|
|
years, days = divmod(delta.days, 365)
|
|
if days:
|
|
text.append("%u days" % days)
|
|
if years:
|
|
text.append("%u years" % years)
|
|
if 3 < len(text):
|
|
text = text[-3:]
|
|
elif not text:
|
|
return "0 ms"
|
|
return " ".join(reversed(text))
|
|
|
|
|
|
def humanFilesize(size):
|
|
"""
|
|
Convert a file size in byte to human natural representation.
|
|
It uses the values: 1 KB is 1024 bytes, 1 MB is 1024 KB, etc.
|
|
The result is an unicode string.
|
|
|
|
>>> humanFilesize(1)
|
|
'1 bytes'
|
|
>>> humanFilesize(790)
|
|
'790 bytes'
|
|
>>> humanFilesize(256960)
|
|
'250.9 KB'
|
|
"""
|
|
if size < 10000:
|
|
return "%u bytes" % size
|
|
units = ["KB", "MB", "GB", "TB"]
|
|
size = float(size)
|
|
divisor = 1024
|
|
for unit in units:
|
|
size = size / divisor
|
|
if size < divisor:
|
|
return "%.1f %s" % (size, unit)
|
|
return "%u %s" % (size, unit)
|
|
|
|
|
|
def humanBitSize(size):
|
|
"""
|
|
Convert a size in bit to human classic representation.
|
|
It uses the values: 1 Kbit is 1000 bits, 1 Mbit is 1000 Kbit, etc.
|
|
The result is an unicode string.
|
|
|
|
>>> humanBitSize(1)
|
|
'1 bits'
|
|
>>> humanBitSize(790)
|
|
'790 bits'
|
|
>>> humanBitSize(256960)
|
|
'257.0 Kbit'
|
|
"""
|
|
divisor = 1000
|
|
if size < divisor:
|
|
return "%u bits" % size
|
|
units = ["Kbit", "Mbit", "Gbit", "Tbit"]
|
|
size = float(size)
|
|
for unit in units:
|
|
size = size / divisor
|
|
if size < divisor:
|
|
return "%.1f %s" % (size, unit)
|
|
return "%u %s" % (size, unit)
|
|
|
|
|
|
def humanBitRate(size):
|
|
"""
|
|
Convert a bit rate to human classic representation. It uses humanBitSize()
|
|
to convert size into human reprensation. The result is an unicode string.
|
|
|
|
>>> humanBitRate(790)
|
|
'790 bits/sec'
|
|
>>> humanBitRate(256960)
|
|
'257.0 Kbit/sec'
|
|
"""
|
|
return "".join((humanBitSize(size), "/sec"))
|
|
|
|
|
|
def humanFrequency(hertz):
|
|
"""
|
|
Convert a frequency in hertz to human classic representation.
|
|
It uses the values: 1 KHz is 1000 Hz, 1 MHz is 1000 KMhz, etc.
|
|
The result is an unicode string.
|
|
|
|
>>> humanFrequency(790)
|
|
'790 Hz'
|
|
>>> humanFrequency(629469)
|
|
'629.5 kHz'
|
|
"""
|
|
divisor = 1000
|
|
if hertz < divisor:
|
|
return "%u Hz" % hertz
|
|
units = ["kHz", "MHz", "GHz", "THz"]
|
|
hertz = float(hertz)
|
|
for unit in units:
|
|
hertz = hertz / divisor
|
|
if hertz < divisor:
|
|
return "%.1f %s" % (hertz, unit)
|
|
return "%s %s" % (hertz, unit)
|
|
|
|
|
|
regex_control_code = re.compile(r"([\x00-\x1f\x7f])")
|
|
controlchars = tuple({
|
|
# Don't use "\0", because "\0"+"0"+"1" = "\001" = "\1" (1 character)
|
|
# Same rease to not use octal syntax ("\1")
|
|
ord("\n"): r"\n",
|
|
ord("\r"): r"\r",
|
|
ord("\t"): r"\t",
|
|
ord("\a"): r"\a",
|
|
ord("\b"): r"\b",
|
|
}.get(code, '\\x%02x' % code)
|
|
for code in range(128)
|
|
)
|
|
|
|
|
|
def makePrintable(data, charset, quote=None, smart=True):
|
|
r"""
|
|
Prepare a string to make it printable in the specified charset.
|
|
It escapes control characters. Characters with code bigger than 127
|
|
are escaped if data type is 'str' or if charset is "ASCII".
|
|
|
|
Examples with Unicode:
|
|
>>> aged = "âgé"
|
|
>>> repr(aged) # text type is 'unicode'
|
|
"'âgé'"
|
|
>>> makePrintable(b"abc\0", "UTF-8")
|
|
'abc\\0'
|
|
>>> makePrintable(aged, "latin1")
|
|
'\xe2g\xe9'
|
|
>>> makePrintable(aged, "latin1", quote='"')
|
|
'"\xe2g\xe9"'
|
|
|
|
Examples with string encoded in latin1:
|
|
>>> aged_latin = "âgé".encode("latin1")
|
|
>>> repr(aged_latin) # text type is 'bytes'
|
|
"b'\\xe2g\\xe9'"
|
|
>>> makePrintable(aged_latin, "latin1")
|
|
'\\xe2g\\xe9'
|
|
>>> makePrintable("", "latin1")
|
|
''
|
|
>>> makePrintable("a", "latin1", quote='"')
|
|
'"a"'
|
|
>>> makePrintable("", "latin1", quote='"')
|
|
'(empty)'
|
|
>>> makePrintable("abc", "latin1", quote="'")
|
|
"'abc'"
|
|
|
|
Control codes:
|
|
>>> makePrintable("\0\x03\x0a\x10 \x7f", "latin1")
|
|
'\\0\\3\\n\\x10 \\x7f'
|
|
|
|
Quote character may also be escaped (only ' and "):
|
|
>>> print(makePrintable("a\"b", "latin-1", quote='"'))
|
|
"a\"b"
|
|
>>> print(makePrintable("a\"b", "latin-1", quote="'"))
|
|
'a"b'
|
|
>>> print(makePrintable("a'b", "latin-1", quote="'"))
|
|
'a\'b'
|
|
"""
|
|
|
|
if data:
|
|
if not isinstance(data, str):
|
|
data = str(data, "ISO-8859-1")
|
|
charset = "ASCII"
|
|
data = regex_control_code.sub(
|
|
lambda regs: controlchars[ord(regs.group(1))], data)
|
|
if quote:
|
|
if quote in "\"'":
|
|
data = data.replace(quote, '\\' + quote)
|
|
data = ''.join((quote, data, quote))
|
|
elif quote:
|
|
data = "(empty)"
|
|
else:
|
|
data = ""
|
|
data = data.encode(charset, "backslashreplace")
|
|
if smart:
|
|
# Replace \x00\x01 by \0\1
|
|
data = re.sub(br"\\x0([0-7])(?=[^0-7]|$)", br"\\\1", data)
|
|
return str(data, charset)
|
|
|
|
|
|
def makeUnicode(text):
|
|
r"""
|
|
Convert text to printable Unicode string. For byte string (type 'str'),
|
|
use charset ISO-8859-1 for the conversion to Unicode
|
|
|
|
>>> makeUnicode('abc\0d')
|
|
'abc\\0d'
|
|
>>> makeUnicode('a\xe9')
|
|
'a\xe9'
|
|
"""
|
|
if isinstance(text, bytes):
|
|
text = str(text, "ISO-8859-1")
|
|
elif not isinstance(text, str):
|
|
try:
|
|
text = str(text)
|
|
except UnicodeError:
|
|
try:
|
|
text = str(text)
|
|
except Exception:
|
|
text = repr(text)
|
|
return makeUnicode(text)
|
|
text = regex_control_code.sub(
|
|
lambda regs: controlchars[ord(regs.group(1))], text)
|
|
text = re.sub(r"\\x0([0-7])(?=[^0-7]|$)", r"\\\1", text)
|
|
return text
|
|
|
|
|
|
def binarySearch(seq, cmp_func):
|
|
"""
|
|
Search a value in a sequence using binary search. Returns index of the
|
|
value, or None if the value doesn't exist.
|
|
|
|
'seq' have to be sorted in ascending order according to the
|
|
comparaison function ;
|
|
|
|
'cmp_func', prototype func(x), is the compare function:
|
|
- Return strictly positive value if we have to search forward ;
|
|
- Return strictly negative value if we have to search backward ;
|
|
- Otherwise (zero) we got the value.
|
|
|
|
>>> # Search number 5 (search forward)
|
|
... binarySearch([0, 4, 5, 10], lambda x: 5-x)
|
|
2
|
|
>>> # Backward search
|
|
... binarySearch([10, 5, 4, 0], lambda x: x-5)
|
|
1
|
|
"""
|
|
lower = 0
|
|
upper = len(seq)
|
|
while lower < upper:
|
|
index = (lower + upper) >> 1
|
|
diff = cmp_func(seq[index])
|
|
if diff < 0:
|
|
upper = index
|
|
elif diff > 0:
|
|
lower = index + 1
|
|
else:
|
|
return index
|
|
return None
|
|
|
|
|
|
def lowerBound(seq, cmp_func):
|
|
f = 0
|
|
seqlen = len(seq)
|
|
while seqlen > 0:
|
|
h = seqlen >> 1
|
|
m = f + h
|
|
if cmp_func(seq[m]):
|
|
f = m
|
|
f += 1
|
|
seqlen -= h + 1
|
|
else:
|
|
seqlen = h
|
|
return f
|
|
|
|
|
|
def _ftypelet(mode):
|
|
if stat.S_ISREG(mode) or not stat.S_IFMT(mode):
|
|
return '-'
|
|
if stat.S_ISBLK(mode):
|
|
return 'b'
|
|
if stat.S_ISCHR(mode):
|
|
return 'c'
|
|
if stat.S_ISDIR(mode):
|
|
return 'd'
|
|
if stat.S_ISFIFO(mode):
|
|
return 'p'
|
|
if stat.S_ISLNK(mode):
|
|
return 'l'
|
|
if stat.S_ISSOCK(mode):
|
|
return 's'
|
|
return '?'
|
|
|
|
|
|
def humanUnixAttributes(mode):
|
|
"""
|
|
Convert a Unix file attributes (or "file mode") to an unicode string.
|
|
|
|
Original source code:
|
|
http://cvs.savannah.gnu.org/viewcvs/coreutils/lib/filemode.c?root=coreutils
|
|
|
|
>>> humanUnixAttributes(0o644)
|
|
'-rw-r--r-- (644)'
|
|
>>> humanUnixAttributes(0o2755)
|
|
'-rwxr-sr-x (2755)'
|
|
"""
|
|
|
|
chars = [_ftypelet(mode), 'r', 'w', 'x', 'r', 'w', 'x', 'r', 'w', 'x']
|
|
for i in range(1, 10):
|
|
if not mode & 1 << 9 - i:
|
|
chars[i] = '-'
|
|
if mode & stat.S_ISUID:
|
|
if chars[3] != 'x':
|
|
chars[3] = 'S'
|
|
else:
|
|
chars[3] = 's'
|
|
if mode & stat.S_ISGID:
|
|
if chars[6] != 'x':
|
|
chars[6] = 'S'
|
|
else:
|
|
chars[6] = 's'
|
|
if mode & stat.S_ISVTX:
|
|
if chars[9] != 'x':
|
|
chars[9] = 'T'
|
|
else:
|
|
chars[9] = 't'
|
|
return "%s (%o)" % (''.join(chars), mode)
|
|
|
|
|
|
def createDict(data, index):
|
|
"""
|
|
Create a new dictionnay from dictionnary key=>values:
|
|
just keep value number 'index' from all values.
|
|
|
|
>>> data={10: ("dix", 100, "a"), 20: ("vingt", 200, "b")}
|
|
>>> createDict(data, 0)
|
|
{10: 'dix', 20: 'vingt'}
|
|
>>> createDict(data, 2)
|
|
{10: 'a', 20: 'b'}
|
|
"""
|
|
return dict((key, values[index]) for key, values in data.items())
|
|
|
|
|
|
# Start of UNIX timestamp (Epoch): 1st January 1970 at 00:00
|
|
UNIX_TIMESTAMP_T0 = datetime(1970, 1, 1)
|
|
|
|
|
|
def timestampUNIX(value):
|
|
"""
|
|
Convert an UNIX (32-bit) timestamp to datetime object. Timestamp value
|
|
is the number of seconds since the 1st January 1970 at 00:00. Maximum
|
|
value is 2147483647: 19 january 2038 at 03:14:07.
|
|
|
|
May raise ValueError for invalid value: value have to be in 0..2147483647.
|
|
|
|
>>> timestampUNIX(0)
|
|
datetime.datetime(1970, 1, 1, 0, 0)
|
|
>>> timestampUNIX(1154175644)
|
|
datetime.datetime(2006, 7, 29, 12, 20, 44)
|
|
>>> timestampUNIX(1154175644.37)
|
|
datetime.datetime(2006, 7, 29, 12, 20, 44, 370000)
|
|
>>> timestampUNIX(2147483647)
|
|
datetime.datetime(2038, 1, 19, 3, 14, 7)
|
|
"""
|
|
if not isinstance(value, (float, int)):
|
|
raise TypeError("timestampUNIX(): an integer or float is required")
|
|
if not (0 <= value <= 2147483647):
|
|
raise ValueError("timestampUNIX(): value have to be in 0..2147483647")
|
|
return UNIX_TIMESTAMP_T0 + timedelta(seconds=value)
|
|
|
|
|
|
# Start of Macintosh timestamp: 1st January 1904 at 00:00
|
|
MAC_TIMESTAMP_T0 = datetime(1904, 1, 1)
|
|
|
|
|
|
def timestampMac32(value):
|
|
"""
|
|
Convert an Mac (32-bit) timestamp to string. The format is the number
|
|
of seconds since the 1st January 1904 (to 2040). Returns unicode string.
|
|
|
|
>>> timestampMac32(0)
|
|
datetime.datetime(1904, 1, 1, 0, 0)
|
|
>>> timestampMac32(2843043290)
|
|
datetime.datetime(1994, 2, 2, 14, 14, 50)
|
|
"""
|
|
if not isinstance(value, (float, int)):
|
|
raise TypeError("an integer or float is required")
|
|
if not (0 <= value <= 4294967295):
|
|
return "invalid Mac timestamp (%s)" % value
|
|
return MAC_TIMESTAMP_T0 + timedelta(seconds=value)
|
|
|
|
|
|
def durationWin64(value):
|
|
"""
|
|
Convert Windows 64-bit duration to string. The timestamp format is
|
|
a 64-bit number: number of 100ns. See also timestampWin64().
|
|
|
|
>>> str(durationWin64(1072580000))
|
|
'0:01:47.258000'
|
|
>>> str(durationWin64(2146280000))
|
|
'0:03:34.628000'
|
|
"""
|
|
if not isinstance(value, (float, int)):
|
|
raise TypeError("an integer or float is required")
|
|
if value < 0:
|
|
raise ValueError("value have to be a positive or nul integer")
|
|
return timedelta(microseconds=value / 10)
|
|
|
|
|
|
def durationMillisWin64(value):
|
|
"""
|
|
Convert Windows 64-bit duration to string. The timestamp format is
|
|
a 64-bit number: number of milliseconds. See also timestampMilliWin64().
|
|
|
|
>>> str(durationMillisWin64(107258))
|
|
'0:01:47.258000'
|
|
>>> str(durationMillisWin64(214628))
|
|
'0:03:34.628000'
|
|
"""
|
|
if not isinstance(value, (float, int)):
|
|
raise TypeError("an integer or float is required")
|
|
if value < 0:
|
|
raise ValueError("value have to be a positive or nul integer")
|
|
return timedelta(microseconds=value * 1000)
|
|
|
|
|
|
# Start of 64-bit Windows timestamp: 1st January 1600 at 00:00
|
|
WIN64_TIMESTAMP_T0 = datetime(1601, 1, 1, 0, 0, 0)
|
|
|
|
|
|
def timestampWin64(value):
|
|
"""
|
|
Convert Windows 64-bit timestamp to string. The timestamp format is
|
|
a 64-bit number which represents number of 100ns since the
|
|
1st January 1601 at 00:00. Result is an unicode string.
|
|
See also durationWin64(). Maximum date is 28 may 60056.
|
|
|
|
>>> timestampWin64(0)
|
|
datetime.datetime(1601, 1, 1, 0, 0)
|
|
>>> timestampWin64(127840491566710000)
|
|
datetime.datetime(2006, 2, 10, 12, 45, 56, 671000)
|
|
"""
|
|
try:
|
|
return WIN64_TIMESTAMP_T0 + durationWin64(value)
|
|
except OverflowError:
|
|
raise ValueError("date newer than year %s (value=%s)"
|
|
% (MAXYEAR, value))
|
|
|
|
|
|
# Start of 60-bit UUID timestamp: 15 October 1582 at 00:00
|
|
UUID60_TIMESTAMP_T0 = datetime(1582, 10, 15, 0, 0, 0)
|
|
|
|
|
|
def timestampUUID60(value):
|
|
"""
|
|
Convert UUID 60-bit timestamp to string. The timestamp format is
|
|
a 60-bit number which represents number of 100ns since the
|
|
the 15 October 1582 at 00:00. Result is an unicode string.
|
|
|
|
>>> timestampUUID60(0)
|
|
datetime.datetime(1582, 10, 15, 0, 0)
|
|
>>> timestampUUID60(130435676263032368)
|
|
datetime.datetime(1996, 2, 14, 5, 13, 46, 303236)
|
|
"""
|
|
if not isinstance(value, (float, int)):
|
|
raise TypeError("an integer or float is required")
|
|
if value < 0:
|
|
raise ValueError("value have to be a positive or nul integer")
|
|
try:
|
|
return UUID60_TIMESTAMP_T0 + timedelta(microseconds=value / 10)
|
|
except OverflowError:
|
|
raise ValueError("timestampUUID60() overflow (value=%s)" % value)
|
|
|
|
|
|
def humanDatetime(value, strip_microsecond=True):
|
|
"""
|
|
Convert a timestamp to Unicode string: use ISO format with space separator.
|
|
|
|
>>> humanDatetime( datetime(2006, 7, 29, 12, 20, 44) )
|
|
'2006-07-29 12:20:44'
|
|
>>> humanDatetime( datetime(2003, 6, 30, 16, 0, 5, 370000) )
|
|
'2003-06-30 16:00:05'
|
|
>>> humanDatetime( datetime(2003, 6, 30, 16, 0, 5, 370000), False )
|
|
'2003-06-30 16:00:05.370000'
|
|
"""
|
|
text = str(value.isoformat())
|
|
text = text.replace('T', ' ')
|
|
if strip_microsecond and "." in text:
|
|
text = text.split(".")[0]
|
|
return text
|
|
|
|
|
|
NEWLINES_REGEX = re.compile("\n+")
|
|
|
|
|
|
def normalizeNewline(text):
|
|
r"""
|
|
Replace Windows and Mac newlines with Unix newlines.
|
|
Replace multiple consecutive newlines with one newline.
|
|
|
|
>>> normalizeNewline('a\r\nb')
|
|
'a\nb'
|
|
>>> normalizeNewline('a\r\rb')
|
|
'a\nb'
|
|
>>> normalizeNewline('a\n\nb')
|
|
'a\nb'
|
|
"""
|
|
text = text.replace("\r\n", "\n")
|
|
text = text.replace("\r", "\n")
|
|
return NEWLINES_REGEX.sub("\n", text)
|