You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
378 lines
10 KiB
378 lines
10 KiB
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the (LGPL) GNU Lesser General Public License as
|
|
# published by the Free Software Foundation; either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Library Lesser General Public License for more details at
|
|
# ( http://www.gnu.org/licenses/lgpl.html ).
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
# written by: Nathan Van Gheem (vangheem@gmail.com)
|
|
|
|
"""
|
|
The I{xdate} module provides classes for converstion
|
|
between XML dates and python objects.
|
|
"""
|
|
|
|
from logging import getLogger
|
|
from suds import *
|
|
from suds.xsd import *
|
|
import time
|
|
import datetime as dt
|
|
import re
|
|
|
|
log = getLogger(__name__)
|
|
|
|
|
|
class Date:
|
|
"""
|
|
An XML date object.
|
|
Supported formats:
|
|
- YYYY-MM-DD
|
|
- YYYY-MM-DD(z|Z)
|
|
- YYYY-MM-DD+06:00
|
|
- YYYY-MM-DD-06:00
|
|
@ivar date: The object value.
|
|
@type date: B{datetime}.I{date}
|
|
"""
|
|
def __init__(self, date):
|
|
"""
|
|
@param date: The value of the object.
|
|
@type date: (date|str)
|
|
@raise ValueError: When I{date} is invalid.
|
|
"""
|
|
if isinstance(date, dt.date):
|
|
self.date = date
|
|
return
|
|
if isinstance(date, basestring):
|
|
self.date = self.__parse(date)
|
|
return
|
|
raise ValueError, type(date)
|
|
|
|
def year(self):
|
|
"""
|
|
Get the I{year} component.
|
|
@return: The year.
|
|
@rtype: int
|
|
"""
|
|
return self.date.year
|
|
|
|
def month(self):
|
|
"""
|
|
Get the I{month} component.
|
|
@return: The month.
|
|
@rtype: int
|
|
"""
|
|
return self.date.month
|
|
|
|
def day(self):
|
|
"""
|
|
Get the I{day} component.
|
|
@return: The day.
|
|
@rtype: int
|
|
"""
|
|
return self.date.day
|
|
|
|
def __parse(self, s):
|
|
"""
|
|
Parse the string date.
|
|
Supported formats:
|
|
- YYYY-MM-DD
|
|
- YYYY-MM-DD(z|Z)
|
|
- YYYY-MM-DD+06:00
|
|
- YYYY-MM-DD-06:00
|
|
Although, the TZ is ignored because it's meaningless
|
|
without the time, right?
|
|
@param s: A date string.
|
|
@type s: str
|
|
@return: A date object.
|
|
@rtype: I{date}
|
|
"""
|
|
try:
|
|
year, month, day = s[:10].split('-', 2)
|
|
year = int(year)
|
|
month = int(month)
|
|
day = int(day)
|
|
return dt.date(year, month, day)
|
|
except:
|
|
log.debug(s, exec_info=True)
|
|
raise ValueError, 'Invalid format "%s"' % s
|
|
|
|
def __str__(self):
|
|
return unicode(self)
|
|
|
|
def __unicode__(self):
|
|
return self.date.isoformat()
|
|
|
|
|
|
class Time:
|
|
"""
|
|
An XML time object.
|
|
Supported formats:
|
|
- HH:MI:SS
|
|
- HH:MI:SS(z|Z)
|
|
- HH:MI:SS.ms
|
|
- HH:MI:SS.ms(z|Z)
|
|
- HH:MI:SS(+|-)06:00
|
|
- HH:MI:SS.ms(+|-)06:00
|
|
@ivar tz: The timezone
|
|
@type tz: L{Timezone}
|
|
@ivar date: The object value.
|
|
@type date: B{datetime}.I{time}
|
|
"""
|
|
|
|
def __init__(self, time, adjusted=True):
|
|
"""
|
|
@param time: The value of the object.
|
|
@type time: (time|str)
|
|
@param adjusted: Adjust for I{local} Timezone.
|
|
@type adjusted: boolean
|
|
@raise ValueError: When I{time} is invalid.
|
|
"""
|
|
self.tz = Timezone()
|
|
if isinstance(time, dt.time):
|
|
self.time = time
|
|
return
|
|
if isinstance(time, basestring):
|
|
self.time = self.__parse(time)
|
|
if adjusted:
|
|
self.__adjust()
|
|
return
|
|
raise ValueError, type(time)
|
|
|
|
def hour(self):
|
|
"""
|
|
Get the I{hour} component.
|
|
@return: The hour.
|
|
@rtype: int
|
|
"""
|
|
return self.time.hour
|
|
|
|
def minute(self):
|
|
"""
|
|
Get the I{minute} component.
|
|
@return: The minute.
|
|
@rtype: int
|
|
"""
|
|
return self.time.minute
|
|
|
|
def second(self):
|
|
"""
|
|
Get the I{seconds} component.
|
|
@return: The seconds.
|
|
@rtype: int
|
|
"""
|
|
return self.time.second
|
|
|
|
def microsecond(self):
|
|
"""
|
|
Get the I{microsecond} component.
|
|
@return: The microsecond.
|
|
@rtype: int
|
|
"""
|
|
return self.time.microsecond
|
|
|
|
def __adjust(self):
|
|
"""
|
|
Adjust for TZ offset.
|
|
"""
|
|
if hasattr(self, 'offset'):
|
|
today = dt.date.today()
|
|
delta = self.tz.adjustment(self.offset)
|
|
d = dt.datetime.combine(today, self.time)
|
|
d = ( d + delta )
|
|
self.time = d.time()
|
|
|
|
def __parse(self, s):
|
|
"""
|
|
Parse the string date.
|
|
Patterns:
|
|
- HH:MI:SS
|
|
- HH:MI:SS(z|Z)
|
|
- HH:MI:SS.ms
|
|
- HH:MI:SS.ms(z|Z)
|
|
- HH:MI:SS(+|-)06:00
|
|
- HH:MI:SS.ms(+|-)06:00
|
|
@param s: A time string.
|
|
@type s: str
|
|
@return: A time object.
|
|
@rtype: B{datetime}.I{time}
|
|
"""
|
|
try:
|
|
offset = None
|
|
part = Timezone.split(s)
|
|
hour, minute, second = part[0].split(':', 2)
|
|
hour = int(hour)
|
|
minute = int(minute)
|
|
second, ms = self.__second(second)
|
|
if len(part) == 2:
|
|
self.offset = self.__offset(part[1])
|
|
if ms is None:
|
|
return dt.time(hour, minute, second)
|
|
else:
|
|
return dt.time(hour, minute, second, ms)
|
|
except:
|
|
log.debug(s, exec_info=True)
|
|
raise ValueError, 'Invalid format "%s"' % s
|
|
|
|
def __second(self, s):
|
|
"""
|
|
Parse the seconds and microseconds.
|
|
The microseconds are truncated to 999999 due to a restriction in
|
|
the python datetime.datetime object.
|
|
@param s: A string representation of the seconds.
|
|
@type s: str
|
|
@return: Tuple of (sec,ms)
|
|
@rtype: tuple.
|
|
"""
|
|
part = s.split('.')
|
|
if len(part) > 1:
|
|
return (int(part[0]), int(part[1][:6]))
|
|
else:
|
|
return (int(part[0]), None)
|
|
|
|
def __offset(self, s):
|
|
"""
|
|
Parse the TZ offset.
|
|
@param s: A string representation of the TZ offset.
|
|
@type s: str
|
|
@return: The signed offset in hours.
|
|
@rtype: str
|
|
"""
|
|
if len(s) == len('-00:00'):
|
|
return int(s[:3])
|
|
if len(s) == 0:
|
|
return self.tz.local
|
|
if len(s) == 1:
|
|
return 0
|
|
raise Exception()
|
|
|
|
def __str__(self):
|
|
return unicode(self)
|
|
|
|
def __unicode__(self):
|
|
time = self.time.isoformat()
|
|
if self.tz.local:
|
|
return '%s%+.2d:00' % (time, self.tz.local)
|
|
else:
|
|
return '%sZ' % time
|
|
|
|
|
|
class DateTime(Date,Time):
|
|
"""
|
|
An XML time object.
|
|
Supported formats:
|
|
- YYYY-MM-DDB{T}HH:MI:SS
|
|
- YYYY-MM-DDB{T}HH:MI:SS(z|Z)
|
|
- YYYY-MM-DDB{T}HH:MI:SS.ms
|
|
- YYYY-MM-DDB{T}HH:MI:SS.ms(z|Z)
|
|
- YYYY-MM-DDB{T}HH:MI:SS(+|-)06:00
|
|
- YYYY-MM-DDB{T}HH:MI:SS.ms(+|-)06:00
|
|
@ivar datetime: The object value.
|
|
@type datetime: B{datetime}.I{datedate}
|
|
"""
|
|
def __init__(self, date):
|
|
"""
|
|
@param date: The value of the object.
|
|
@type date: (datetime|str)
|
|
@raise ValueError: When I{tm} is invalid.
|
|
"""
|
|
if isinstance(date, dt.datetime):
|
|
Date.__init__(self, date.date())
|
|
Time.__init__(self, date.time())
|
|
self.datetime = \
|
|
dt.datetime.combine(self.date, self.time)
|
|
return
|
|
if isinstance(date, basestring):
|
|
part = date.split('T')
|
|
Date.__init__(self, part[0])
|
|
Time.__init__(self, part[1], 0)
|
|
self.datetime = \
|
|
dt.datetime.combine(self.date, self.time)
|
|
self.__adjust()
|
|
return
|
|
raise ValueError, type(date)
|
|
|
|
def __adjust(self):
|
|
"""
|
|
Adjust for TZ offset.
|
|
"""
|
|
if not hasattr(self, 'offset'):
|
|
return
|
|
delta = self.tz.adjustment(self.offset)
|
|
try:
|
|
d = ( self.datetime + delta )
|
|
self.datetime = d
|
|
self.date = d.date()
|
|
self.time = d.time()
|
|
except OverflowError:
|
|
log.warn('"%s" caused overflow, not-adjusted', self.datetime)
|
|
|
|
def __str__(self):
|
|
return unicode(self)
|
|
|
|
def __unicode__(self):
|
|
s = []
|
|
s.append(Date.__unicode__(self))
|
|
s.append(Time.__unicode__(self))
|
|
return 'T'.join(s)
|
|
|
|
|
|
class UTC(DateTime):
|
|
"""
|
|
Represents current UTC time.
|
|
"""
|
|
|
|
def __init__(self, date=None):
|
|
if date is None:
|
|
date = dt.datetime.utcnow()
|
|
DateTime.__init__(self, date)
|
|
self.tz.local = 0
|
|
|
|
|
|
class Timezone:
|
|
"""
|
|
Timezone object used to do TZ conversions
|
|
@cvar local: The (A) local TZ offset.
|
|
@type local: int
|
|
@cvar patten: The regex patten to match TZ.
|
|
@type patten: re.Pattern
|
|
"""
|
|
|
|
pattern = re.compile('([zZ])|([\-\+][0-9]{2}:[0-9]{2})')
|
|
|
|
LOCAL = ( 0-time.timezone/60/60 )
|
|
|
|
def __init__(self, offset=None):
|
|
if offset is None:
|
|
offset = self.LOCAL
|
|
self.local = offset
|
|
|
|
@classmethod
|
|
def split(cls, s):
|
|
"""
|
|
Split the TZ from string.
|
|
@param s: A string containing a timezone
|
|
@type s: basestring
|
|
@return: The split parts.
|
|
@rtype: tuple
|
|
"""
|
|
m = cls.pattern.search(s)
|
|
if m is None:
|
|
return (s,)
|
|
x = m.start(0)
|
|
return (s[:x], s[x:])
|
|
|
|
def adjustment(self, offset):
|
|
"""
|
|
Get the adjustment to the I{local} TZ.
|
|
@return: The delta between I{offset} and local TZ.
|
|
@rtype: B{datetime}.I{timedelta}
|
|
"""
|
|
delta = ( self.local - offset )
|
|
return dt.timedelta(hours=delta)
|
|
|