Files
deb-python-pyvmomi/pyVmomi/Iso8601.py
Joshua Kwan 26649465c6 Initial source drop
This is the initial version of pyVmomi which represents vSphere API 5.1.
Going forward, expect only the 'Objects' and 'Types' generated source
files to need updating via official OSS drops.
2013-12-14 13:36:09 -05:00

357 lines
12 KiB
Python

#!/usr/bin/env python
# VMware vSphere Python SDK
# Copyright (c) 2008-2013 VMware, Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This module is for ISO 8601 parsing
"""
__author__ = 'VMware, Inc.'
import time
from datetime import datetime, timedelta, tzinfo
import re
""" Regular expression to parse a subset of ISO 8601 format """
_dtExpr = re.compile(
# XMLSchema datetime. Mandatory to have - and :
# See: http://www.w3.org/TR/xmlschema-2/#isoformats
# Note: python datetime cannot handle the following:
# - leap second, ie. 0-60 seconds (not 0-59)
# - BC (negative years)
# year [-]0000..9999
r'(?P<year>-?\d{4})' \
# month 01..12
r'(-(?P<month>(0[1-9]|1[0-2]))' \
# day 01..31
r'(-(?P<day>(0[1-9]|[1-2]\d|3[01])))?)?' \
# time separator 'T'
r'(T' \
# hour 00..24
r'(?P<hour>([01]\d|2[0-4]))' \
# minute 00..59
r'((:(?P<minute>[0-5]\d))' \
# seconds 00..60 (leap second ok)
r'(:(?P<second>([0-5]\d|60))' \
# microsecond. max 16 digits
# - Should not allows trailing zeros. But python isoformat() put zeros
# after microseconds. Oh well, allows trailing zeros, quite harmless
r'(\.(?P<microsecond>\d{1,16}))?)?)?' \
# UTC 'Z', or...
r'((?P<tzutc>Z)' \
# tz [+-]00..13:0..59|14:00
r'|((?P<tzhr>[+-](([0]\d)|(1[0-3])|(?P<tzlimit>)14))' \
r'(:(?P<tzmin>(?(tzlimit)00|([0-5]\d))))?))?' \
r')?$')
""" Default date time val. Key should match the tags in _dtExpr """
_dtExprKeyDefValMap = {'year' : None, 'month' : 1, 'day' : 1,
'hour' : 0, 'minute' : 0, 'second' : 0,
'microsecond' : 0}
class TZInfo(tzinfo):
""" Timezone info class """
timedelta0 = timedelta(hours=0)
timedelta1 = timedelta(hours=1)
def __init__(self, tzname='UTC', utcOffset=None, dst=None):
self._tzname = tzname
if not utcOffset:
utcOffset = self.timedelta0
self._utcOffset = utcOffset
if not dst:
dst = None
self._dst = dst
def utcoffset(self, dt):
return self._utcOffset + self.dst(dt)
def tzname(self, dt):
return self._tzname
def dst(self, dt):
ret = self.timedelta0
if self._dst:
if self._dst[0] <= dt.replace(tzinfo=None) < self._dst[1]:
ret = self.timedelta1
return ret
class TZManager:
""" Time zone manager """
_tzInfos = {}
@staticmethod
def GetTZInfo(tzname='UTC', utcOffset=None, dst=None):
""" Get / Add timezone info """
key = (tzname, utcOffset, dst)
tzInfo = TZManager._tzInfos.get(key)
if not tzInfo:
tzInfo = TZInfo(tzname, utcOffset, dst)
TZManager._tzInfos[key] = tzInfo
return tzInfo
def ParseISO8601(datetimeStr):
"""
Parse ISO 8601 date time from string.
Returns datetime if ok, None otherwise
Note: Allows YYYY / YYYY-MM, but truncate YYYY -> YYYY-01-01,
YYYY-MM -> YYYY-MM-01
Truncate microsecond to most significant 6 digits
"""
datetimeVal = None
match = _dtExpr.match(datetimeStr)
if match:
try:
dt = {}
for key, defaultVal in _dtExprKeyDefValMap.iteritems():
val = match.group(key)
if val:
if key == 'microsecond':
val = val[:6] + '0' * (6 - len(val))
dt[key] = int(val)
elif defaultVal:
dt[key] = defaultVal
# Orig. XMLSchema don't allow all zeros year. But newer draft is ok
#if dt['year'] == 0:
# # Year cannot be all zeros
# raise Exception('Year cannot be all zeros')
# 24 is a special case. It is actually represented as next day 00:00
delta = None
if dt.get('hour', 0) == 24:
# Must be 24:00:00.0
if dt.get('minute', 0) == 0 and dt.get('second', 0) == 0 and \
dt.get('microsecond', 0) == 0:
dt['hour'] = 23
delta = timedelta(hours=1)
else:
return None
# Set tzinfo
# TODO: dst
tzInfo = None
val = match.group('tzutc')
if val:
tzInfo = TZManager.GetTZInfo()
else:
val = match.group('tzhr')
if val:
# tz hours offset
tzhr = int(val)
utcsign = val[0]
# tz minutes offset
tzmin = 0
val = match.group('tzmin')
if val:
tzmin = tzhr >= 0 and int(val) or -int(val)
# Better tzname (map UTC +-00:00 to UTC)
tzname = 'UTC'
if tzhr != 0 or tzmin != 0:
tzname += ' %s%02d:%02d' % (utcsign, abs(tzhr), abs(tzmin))
tzInfo = TZManager.GetTZInfo(tzname=tzname,
utcOffset=timedelta(hours=tzhr,
minutes=tzmin))
if tzInfo:
dt['tzinfo'] = tzInfo
datetimeVal = datetime(**dt)
if delta:
datetimeVal += delta
except Exception, e:
pass
return datetimeVal
def ISO8601Format(dt):
"""
Python datetime isoformat() has the following problems:
- leave trailing 0 at the end of microseconds (violates XMLSchema rule)
- tz print +00:00 instead of Z
- Missing timezone offset for datetime without tzinfo
"""
isoStr = dt.strftime('%Y-%m-%dT%H:%M:%S')
if dt.microsecond:
isoStr += ('.%06d' % dt.microsecond).rstrip('0')
if dt.tzinfo:
tz = dt.strftime('%z')
else:
utcOffset_minutes = -time.altzone / 60
tz = "%+.2d%.2d" % (utcOffset_minutes / 60, (abs(utcOffset_minutes) % 60))
if tz == '+0000':
return isoStr + 'Z'
elif tz:
return isoStr + tz[:3] + ':' + tz[3:]
else:
# Local offset is unknown
return isoStr + '-00:00'
# Testing
if __name__ == '__main__':
# Valid entries
for testStr in [
'1971', # 1971-01-01
'1971-11', # 1971-11-01
'1971-11-02',
'1971-11-02T23',
'1971-11-02T23Z',
'1971-11-02T23:04',
'1971-11-02T23:04Z',
'1971-11-02T23:04:15',
'1971-11-02T23:04:15Z',
'1971-11-02T23:04:15.1',
'1971-11-02T23:04:15.01',
'1971-11-02T23:04:15.023456',
'1971-11-02T23:04:15.103456Z',
'1971-11-02T23:04:15.123456+11',
'1971-11-02T23:04:15.123456-11',
'1971-11-02T23:04:15.123456+11:30',
'1971-11-02T23:04:15.123456-11:30',
'1971-11-02T23:04:15.123456+00:00', # Same as Z
'1971-11-02T23:04:15.123456-00:00', # Same as Z
'1971-01-02T23:04:15+14',
'1971-01-02T23:04:15+14:00',
'1971-01-02T23:04:15-14',
'1971-01-02T23:04:15-14:00',
# Valid: Truncate microsec to 6 digits
'1971-01-02T23:04:15.123456891+11',
'1971-01-02T24', # 24 is valid. It should represent the 00:00 the
# next day
'1971-01-02T24:00',
'1971-01-02T24:00:00',
'1971-01-02T24:00:00.0',
# Should NOT be valid but python isoformat adding trailing zeros
'1971-01-02T23:04:15.123430', # Microseconds ends in zero
'1971-01-02T23:04:15.0', # Microseconds ends in zero
# Should be valid but python datetime don't support it
#'2005-12-31T23:59:60Z', # Leap second
#'-0001', # BC 1
]:
dt = ParseISO8601(testStr)
if dt == None:
print 'Failed to parse (%s)' % testStr
assert(False)
# Make sure we can translate back
isoformat = ISO8601Format(dt)
dt1 = ParseISO8601(isoformat)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=dt1.tzinfo)
if dt1 != dt:
print 'ParseISO8601 -> ISO8601Format -> ParseISO8601 failed (%s)' % testStr
assert(False)
# Make sure we can parse python isoformat()
dt2 = ParseISO8601(dt.isoformat())
if dt2 == None:
print 'ParseISO8601("%s".isoformat()) failed' % testStr
assert(False)
print testStr, '->', dt, isoformat
# Basic form
for testStr in [
'197111', # 1971-11-01
'19711102',
'19711102T23',
'19711102T23Z',
'19711102T2304',
'19711102T2304Z',
'19711102T230415',
'19711102T230415Z',
'19711102T230415.123456',
'19711102T230415.123456Z',
'19711102T230415.123456+11',
'19711102T230415.123456-11',
'19711102T230415.123456+1130',
'19711102T230415.123456-1130',
]:
# Reject for now
dt = ParseISO8601(testStr)
if dt != None:
print 'ParseISO8601 (%s) should fail, but it did not' % testStr
assert(False)
#print testStr, '->', dt
#assert(dt != None)
# Invalid entries
for testStr in [
# Xml schema reject year 0
'0000', # 0 years are not allowed
'+0001', # Leading + is not allowed
'', # Empty datetime str
'09', # Years must be at least 4 digits
'1971-01-02T', # T not follow by time
'1971-01-02TZ', # T not follow by time
'1971-01-02T+10', # T not follow by time
'1971-01-02T-10', # T not follow by time
'1971-01-02T23:', # extra :
'1971-01-02T23:04:', # extra :
'1971-01-02T23:0d', # 0d
'1971-01-02T23:04:15.', # Dot not follows by microsec
'1971-01-02+12', # time without T
'1971Z', # Z without T
'1971-01-02T23:04:15.123456Z+11', # Z follows by +
'1971-01-02T23:04:15.123456Z-11', # Z follows by -
'1971-01-02T23:04:15.123456+:30', # extra :
'1971-01-02T23:04:15.123456+30:', # extra :
'1971-01-02T23:04:15.01234567890123456789', # Too many microseconds digits
# Python isoformat leave trailing zeros in microseconds
# Relax regular expression to accept it
#'1971-01-02T23:04:15.123430', # Microseconds ends in zero
#'1971-01-02T23:04:15.0', # Microseconds ends in zero
# Timezone must be between +14 / -14
'1971-01-02T23:04:15+15',
'1971-01-02T23:04:15-15',
'1971-01-02T23:04:15+14:01',
'1971-01-02T23:04:15-14:01',
# Mix basic form with extended format
'197101-02T23:04:15.123456',
'19710102T23:04:15.123456',
'19710102T230415.123456+11:30',
'1971-01-02T230415.123456',
'1971-01-02T23:04:15.123456+1130',
# Error captured by datetime class
'1971-00-02', # Less than 1 month
'1971-13-02', # Larger than 12 months
'1971-01-00', # Less than 1 day
'1971-11-32', # Larger than 30 days for Nov
'1971-12-32', # Larger than 31 days
'1971-01-02T24:01', # Larger than 23 hr
'1971-01-02T23:61', # Larger than 60 min
'1971-01-02T23:60:61', # Larger than 61 sec
]:
dt = ParseISO8601(testStr)
if dt != None:
print 'ParseISO8601 (%s) should fail, but it did not' % testStr
assert(False)