Vendor pywinrm (and dependencies).

https://trello.com/c/FQ9AkmRV/46-install-vendored-pywinrm-for-ansible-us
e
This commit is contained in:
Luke Sneeringer 2014-08-20 09:35:40 -04:00
parent 0766bc302b
commit 356532424e
37 changed files with 3942 additions and 0 deletions

View File

@ -0,0 +1,55 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
Import all essential functions and constants to re-export them here for easy
access.
This module contains also various pre-defined ISO 8601 format strings.
'''
from isodate.isodates import parse_date, date_isoformat
from isodate.isotime import parse_time, time_isoformat
from isodate.isodatetime import parse_datetime, datetime_isoformat
from isodate.isoduration import parse_duration, duration_isoformat, Duration
from isodate.isoerror import ISO8601Error
from isodate.isotzinfo import parse_tzinfo, tz_isoformat
from isodate.tzinfo import UTC, FixedOffset, LOCAL
from isodate.duration import Duration
from isodate.isostrf import strftime
from isodate.isostrf import DATE_BAS_COMPLETE, DATE_BAS_ORD_COMPLETE
from isodate.isostrf import DATE_BAS_WEEK, DATE_BAS_WEEK_COMPLETE
from isodate.isostrf import DATE_CENTURY, DATE_EXT_COMPLETE
from isodate.isostrf import DATE_EXT_ORD_COMPLETE, DATE_EXT_WEEK
from isodate.isostrf import DATE_EXT_WEEK_COMPLETE, DATE_MONTH, DATE_YEAR
from isodate.isostrf import TIME_BAS_COMPLETE, TIME_BAS_MINUTE
from isodate.isostrf import TIME_EXT_COMPLETE, TIME_EXT_MINUTE
from isodate.isostrf import TIME_HOUR
from isodate.isostrf import TZ_BAS, TZ_EXT, TZ_HOUR
from isodate.isostrf import DT_BAS_COMPLETE, DT_EXT_COMPLETE
from isodate.isostrf import DT_BAS_ORD_COMPLETE, DT_EXT_ORD_COMPLETE
from isodate.isostrf import DT_BAS_WEEK_COMPLETE, DT_EXT_WEEK_COMPLETE
from isodate.isostrf import D_DEFAULT, D_WEEK, D_ALT_EXT, D_ALT_BAS
from isodate.isostrf import D_ALT_BAS_ORD, D_ALT_EXT_ORD

View File

@ -0,0 +1,280 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
This module defines a Duration class.
The class Duration allows to define durations in years and months and can be
used as limited replacement for timedelta objects.
'''
from datetime import date, datetime, timedelta
from decimal import Decimal, ROUND_FLOOR
def fquotmod(val, low, high):
'''
A divmod function with boundaries.
'''
# assumes that all the maths is done with Decimals.
# divmod for Decimal uses truncate instead of floor as builtin divmod, so we have
# to do it manually here.
a, b = val - low, high - low
div = (a / b).to_integral(ROUND_FLOOR)
mod = a - div * b
# if we were not usig Decimal, it would look like this.
#div, mod = divmod(val - low, high - low)
mod += low
return int(div), mod
def max_days_in_month(year, month):
'''
Determines the number of days of a specific month in a specific year.
'''
if month in (1, 3, 5, 7, 8, 10, 12):
return 31
if month in (4, 6, 9, 11):
return 30
if ((year % 400) == 0) or ((year % 100) != 0) and ((year % 4) == 0):
return 29
return 28
class Duration(object):
'''
A class which represents a duration.
The difference to datetime.timedelta is, that this class handles also
differences given in years and months.
A Duration treats differences given in year, months separately from all
other components.
A Duration can be used almost like any timedelta object, however there
are some restrictions:
* It is not really possible to compare Durations, because it is unclear,
whether a duration of 1 year is bigger than 365 days or not.
* Equality is only tested between the two (year, month vs. timedelta)
basic components.
A Duration can also be converted into a datetime object, but this requires
a start date or an end date.
The algorithm to add a duration to a date is defined at
http://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes
'''
def __init__(self, days=0, seconds=0, microseconds=0, milliseconds=0,
minutes=0, hours=0, weeks=0, months=0, years=0):
'''
Initialise this Duration instance with the given parameters.
'''
if not isinstance(months, Decimal):
months = Decimal(str(months))
if not isinstance(years, Decimal):
years = Decimal(str(years))
self.months = months
self.years = years
self.tdelta = timedelta(days, seconds, microseconds, milliseconds,
minutes, hours, weeks)
def __getattr__(self, name):
'''
Provide direct access to attributes of included timedelta instance.
'''
return getattr(self.tdelta, name)
def __str__(self):
'''
Return a string representation of this duration similar to timedelta.
'''
params = []
if self.years:
params.append('%d years' % self.years)
if self.months:
params.append('%d months' % self.months)
params.append(str(self.tdelta))
return ', '.join(params)
def __repr__(self):
'''
Return a string suitable for repr(x) calls.
'''
return "%s.%s(%d, %d, %d, years=%d, months=%d)" % (
self.__class__.__module__, self.__class__.__name__,
self.tdelta.days, self.tdelta.seconds,
self.tdelta.microseconds, self.years, self.months)
def __neg__(self):
"""
A simple unary minus.
Returns a new Duration instance with all it's negated.
"""
negduration = Duration(years=-self.years, months=-self.months)
negduration.tdelta = -self.tdelta
return negduration
def __add__(self, other):
'''
Durations can be added with Duration, timedelta, date and datetime
objects.
'''
if isinstance(other, timedelta):
newduration = Duration(years=self.years, months=self.months)
newduration.tdelta = self.tdelta + other
return newduration
if isinstance(other, Duration):
newduration = Duration(years=self.years + other.years,
months=self.months + other.months)
newduration.tdelta = self.tdelta + other.tdelta
return newduration
if isinstance(other, (date, datetime)):
if (not( float(self.years).is_integer() and float(self.months).is_integer())):
raise ValueError('fractional years or months not supported for date calculations')
newmonth = other.month + self.months
carry, newmonth = fquotmod(newmonth, 1, 13)
newyear = other.year + self.years + carry
maxdays = max_days_in_month(newyear, newmonth)
if other.day > maxdays:
newday = maxdays
else:
newday = other.day
newdt = other.replace(year=newyear, month=newmonth, day=newday)
return self.tdelta + newdt
raise TypeError('unsupported operand type(s) for +: %s and %s' %
(self.__class__, other.__class__))
def __radd__(self, other):
'''
Add durations to timedelta, date and datetime objects.
'''
if isinstance(other, timedelta):
newduration = Duration(years=self.years, months=self.months)
newduration.tdelta = self.tdelta + other
return newduration
if isinstance(other, (date, datetime)):
if (not( float(self.years).is_integer() and float(self.months).is_integer())):
raise ValueError('fractional years or months not supported for date calculations')
newmonth = other.month + self.months
carry, newmonth = fquotmod(newmonth, 1, 13)
newyear = other.year + self.years + carry
maxdays = max_days_in_month(newyear, newmonth)
if other.day > maxdays:
newday = maxdays
else:
newday = other.day
newdt = other.replace(year=newyear, month=newmonth, day=newday)
return newdt + self.tdelta
raise TypeError('unsupported operand type(s) for +: %s and %s' %
(other.__class__, self.__class__))
def __sub__(self, other):
'''
It is possible to subtract Duration and timedelta objects from Duration
objects.
'''
if isinstance(other, Duration):
newduration = Duration(years=self.years - other.years,
months=self.months - other.months)
newduration.tdelta = self.tdelta - other.tdelta
return newduration
if isinstance(other, timedelta):
newduration = Duration(years=self.years, months=self.months)
newduration.tdelta = self.tdelta - other
return newduration
raise TypeError('unsupported operand type(s) for -: %s and %s' %
(self.__class__, other.__class__))
def __rsub__(self, other):
'''
It is possible to subtract Duration objecs from date, datetime and
timedelta objects.
'''
#print '__rsub__:', self, other
if isinstance(other, (date, datetime)):
if (not( float(self.years).is_integer() and float(self.months).is_integer())):
raise ValueError('fractional years or months not supported for date calculations')
newmonth = other.month - self.months
carry, newmonth = fquotmod(newmonth, 1, 13)
newyear = other.year - self.years + carry
maxdays = max_days_in_month(newyear, newmonth)
if other.day > maxdays:
newday = maxdays
else:
newday = other.day
newdt = other.replace(year=newyear, month=newmonth, day=newday)
return newdt - self.tdelta
if isinstance(other, timedelta):
tmpdur = Duration()
tmpdur.tdelta = other
return tmpdur - self
raise TypeError('unsupported operand type(s) for -: %s and %s' %
(other.__class__, self.__class__))
def __eq__(self, other):
'''
If the years, month part and the timedelta part are both equal, then
the two Durations are considered equal.
'''
if (isinstance(other, timedelta) and
self.years == 0 and self.months == 0):
return self.tdelta == other
if not isinstance(other, Duration):
return NotImplemented
if ((self.years * 12 + self.months) ==
(other.years * 12 + other.months) and self.tdelta == other.tdelta):
return True
return False
def __ne__(self, other):
'''
If the years, month part or the timedelta part is not equal, then
the two Durations are considered not equal.
'''
if isinstance(other, timedelta) and self.years == 0 and self.months == 0:
return self.tdelta != other
if not isinstance(other, Duration):
return NotImplemented
if ((self.years * 12 + self.months) !=
(other.years * 12 + other.months) or self.tdelta != other.tdelta):
return True
return False
def totimedelta(self, start=None, end=None):
'''
Convert this duration into a timedelta object.
This method requires a start datetime or end datetimem, but raises
an exception if both are given.
'''
if start is None and end is None:
raise ValueError("start or end required")
if start is not None and end is not None:
raise ValueError("only start or end allowed")
if start is not None:
return (start + self) - start
return end - (end - self)

View File

@ -0,0 +1,201 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
This modules provides a method to parse an ISO 8601:2004 date string to a
python datetime.date instance.
It supports all basic, extended and expanded formats as described in the ISO
standard. The only limitations it has, are given by the Python datetime.date
implementation, which does not support dates before 0001-01-01.
'''
import re
from datetime import date, timedelta
from isodate.isostrf import strftime, DATE_EXT_COMPLETE
from isodate.isoerror import ISO8601Error
DATE_REGEX_CACHE = {}
# A dictionary to cache pre-compiled regular expressions.
# A set of regular expressions is identified, by number of year digits allowed
# and whether a plus/minus sign is required or not. (This option is changeable
# only for 4 digit years).
def build_date_regexps(yeardigits=4, expanded=False):
'''
Compile set of regular expressions to parse ISO dates. The expressions will
be created only if they are not already in REGEX_CACHE.
It is necessary to fix the number of year digits, else it is not possible
to automatically distinguish between various ISO date formats.
ISO 8601 allows more than 4 digit years, on prior agreement, but then a +/-
sign is required (expanded format). To support +/- sign for 4 digit years,
the expanded parameter needs to be set to True.
'''
if yeardigits != 4:
expanded = True
if (yeardigits, expanded) not in DATE_REGEX_CACHE:
cache_entry = []
# ISO 8601 expanded DATE formats allow an arbitrary number of year
# digits with a leading +/- sign.
if expanded:
sign = 1
else:
sign = 0
# 1. complete dates:
# YYYY-MM-DD or +- YYYYYY-MM-DD... extended date format
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
r"-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})"
% (sign, yeardigits)))
# YYYYMMDD or +- YYYYYYMMDD... basic date format
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
r"(?P<month>[0-9]{2})(?P<day>[0-9]{2})"
% (sign, yeardigits)))
# 2. complete week dates:
# YYYY-Www-D or +-YYYYYY-Www-D ... extended week date
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
r"-W(?P<week>[0-9]{2})-(?P<day>[0-9]{1})"
% (sign, yeardigits)))
# YYYYWwwD or +-YYYYYYWwwD ... basic week date
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})W"
r"(?P<week>[0-9]{2})(?P<day>[0-9]{1})"
% (sign, yeardigits)))
# 3. ordinal dates:
# YYYY-DDD or +-YYYYYY-DDD ... extended format
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
r"-(?P<day>[0-9]{3})"
% (sign, yeardigits)))
# YYYYDDD or +-YYYYYYDDD ... basic format
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
r"(?P<day>[0-9]{3})"
% (sign, yeardigits)))
# 4. week dates:
# YYYY-Www or +-YYYYYY-Www ... extended reduced accuracy week date
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
r"-W(?P<week>[0-9]{2})"
% (sign, yeardigits)))
# YYYYWww or +-YYYYYYWww ... basic reduced accuracy week date
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})W"
r"(?P<week>[0-9]{2})"
% (sign, yeardigits)))
# 5. month dates:
# YYY-MM or +-YYYYYY-MM ... reduced accuracy specific month
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
r"-(?P<month>[0-9]{2})"
% (sign, yeardigits)))
# 6. year dates:
# YYYY or +-YYYYYY ... reduced accuracy specific year
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
% (sign, yeardigits)))
# 7. century dates:
# YY or +-YYYY ... reduced accuracy specific century
cache_entry.append(re.compile(r"(?P<sign>[+-]){%d}"
r"(?P<century>[0-9]{%d})"
% (sign, yeardigits - 2)))
DATE_REGEX_CACHE[(yeardigits, expanded)] = cache_entry
return DATE_REGEX_CACHE[(yeardigits, expanded)]
def parse_date(datestring, yeardigits=4, expanded=False):
'''
Parse an ISO 8601 date string into a datetime.date object.
As the datetime.date implementation is limited to dates starting from
0001-01-01, negative dates (BC) and year 0 can not be parsed by this
method.
For incomplete dates, this method chooses the first day for it. For
instance if only a century is given, this method returns the 1st of
January in year 1 of this century.
supported formats: (expanded formats are shown with 6 digits for year)
YYYYMMDD +-YYYYYYMMDD basic complete date
YYYY-MM-DD +-YYYYYY-MM-DD extended complete date
YYYYWwwD +-YYYYYYWwwD basic complete week date
YYYY-Www-D +-YYYYYY-Www-D extended complete week date
YYYYDDD +-YYYYYYDDD basic ordinal date
YYYY-DDD +-YYYYYY-DDD extended ordinal date
YYYYWww +-YYYYYYWww basic incomplete week date
YYYY-Www +-YYYYYY-Www extended incomplete week date
YYY-MM +-YYYYYY-MM incomplete month date
YYYY +-YYYYYY incomplete year date
YY +-YYYY incomplete century date
@param datestring: the ISO date string to parse
@param yeardigits: how many digits are used to represent a year
@param expanded: if True then +/- signs are allowed. This parameter
is forced to True, if yeardigits != 4
@return: a datetime.date instance represented by datestring
@raise ISO8601Error: if this function can not parse the datestring
@raise ValueError: if datestring can not be represented by datetime.date
'''
if yeardigits != 4:
expanded = True
isodates = build_date_regexps(yeardigits, expanded)
for pattern in isodates:
match = pattern.match(datestring)
if match:
groups = match.groupdict()
# sign, century, year, month, week, day,
# FIXME: negative dates not possible with python standard types
sign = (groups['sign'] == '-' and -1) or 1
if 'century' in groups:
return date(sign * (int(groups['century']) * 100 + 1), 1, 1)
if not 'month' in groups: # weekdate or ordinal date
ret = date(sign * int(groups['year']), 1, 1)
if 'week' in groups:
isotuple = ret.isocalendar()
if 'day' in groups:
days = int(groups['day'] or 1)
else:
days = 1
# if first week in year, do weeks-1
return ret + timedelta(weeks=int(groups['week']) -
(((isotuple[1] == 1) and 1) or 0),
days = -isotuple[2] + days)
elif 'day' in groups: # ordinal date
return ret + timedelta(days=int(groups['day'])-1)
else: # year date
return ret
# year-, month-, or complete date
if 'day' not in groups or groups['day'] is None:
day = 1
else:
day = int(groups['day'])
return date(sign * int(groups['year']),
int(groups['month']) or 1, day)
raise ISO8601Error('Unrecognised ISO 8601 date format: %r' % datestring)
def date_isoformat(tdate, format=DATE_EXT_COMPLETE, yeardigits=4):
'''
Format date strings.
This method is just a wrapper around isodate.isostrf.strftime and uses
Date-Extended-Complete as default format.
'''
return strftime(tdate, format, yeardigits)

View File

@ -0,0 +1,61 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
This module defines a method to parse an ISO 8601:2004 date time string.
For this job it uses the parse_date and parse_time methods defined in date
and time module.
'''
from datetime import datetime
from isodate.isostrf import strftime
from isodate.isostrf import DATE_EXT_COMPLETE, TIME_EXT_COMPLETE, TZ_EXT
from isodate.isodates import parse_date
from isodate.isotime import parse_time
def parse_datetime(datetimestring):
'''
Parses ISO 8601 date-times into datetime.datetime objects.
This function uses parse_date and parse_time to do the job, so it allows
more combinations of date and time representations, than the actual
ISO 8601:2004 standard allows.
'''
datestring, timestring = datetimestring.split('T')
tmpdate = parse_date(datestring)
tmptime = parse_time(timestring)
return datetime.combine(tmpdate, tmptime)
def datetime_isoformat(tdt, format=DATE_EXT_COMPLETE + 'T' +
TIME_EXT_COMPLETE + TZ_EXT):
'''
Format datetime strings.
This method is just a wrapper around isodate.isostrf.strftime and uses
Extended-Complete as default format.
'''
return strftime(tdt, format)

View File

@ -0,0 +1,145 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
This module provides an ISO 8601:2004 duration parser.
It also provides a wrapper to strftime. This wrapper makes it easier to
format timedelta or Duration instances as ISO conforming strings.
'''
from datetime import timedelta
from decimal import Decimal
import re
from isodate.duration import Duration
from isodate.isoerror import ISO8601Error
from isodate.isodatetime import parse_datetime
from isodate.isostrf import strftime, D_DEFAULT
ISO8601_PERIOD_REGEX = re.compile(r"^(?P<sign>[+-])?"
r"P(?P<years>[0-9]+([,.][0-9]+)?Y)?"
r"(?P<months>[0-9]+([,.][0-9]+)?M)?"
r"(?P<weeks>[0-9]+([,.][0-9]+)?W)?"
r"(?P<days>[0-9]+([,.][0-9]+)?D)?"
r"((?P<separator>T)(?P<hours>[0-9]+([,.][0-9]+)?H)?"
r"(?P<minutes>[0-9]+([,.][0-9]+)?M)?"
r"(?P<seconds>[0-9]+([,.][0-9]+)?S)?)?$")
# regular expression to parse ISO duartion strings.
def parse_duration(datestring):
"""
Parses an ISO 8601 durations into datetime.timedelta or Duration objects.
If the ISO date string does not contain years or months, a timedelta
instance is returned, else a Duration instance is returned.
The following duration formats are supported:
-PnnW duration in weeks
-PnnYnnMnnDTnnHnnMnnS complete duration specification
-PYYYYMMDDThhmmss basic alternative complete date format
-PYYYY-MM-DDThh:mm:ss extended alternative complete date format
-PYYYYDDDThhmmss basic alternative ordinal date format
-PYYYY-DDDThh:mm:ss extended alternative ordinal date format
The '-' is optional.
Limitations: ISO standard defines some restrictions about where to use
fractional numbers and which component and format combinations are
allowed. This parser implementation ignores all those restrictions and
returns something when it is able to find all necessary components.
In detail:
it does not check, whether only the last component has fractions.
it allows weeks specified with all other combinations
The alternative format does not support durations with years, months or
days set to 0.
"""
if not isinstance(datestring, basestring):
raise TypeError("Expecting a string %r" % datestring)
match = ISO8601_PERIOD_REGEX.match(datestring)
if not match:
# try alternative format:
if datestring.startswith("P"):
durdt = parse_datetime(datestring[1:])
if durdt.year != 0 or durdt.month != 0:
# create Duration
ret = Duration(days=durdt.day, seconds=durdt.second,
microseconds=durdt.microsecond,
minutes=durdt.minute, hours=durdt.hour,
months=durdt.month, years=durdt.year)
else: # FIXME: currently not possible in alternative format
# create timedelta
ret = timedelta(days=durdt.day, seconds=durdt.second,
microseconds=durdt.microsecond,
minutes=durdt.minute, hours=durdt.hour)
return ret
raise ISO8601Error("Unable to parse duration string %r" % datestring)
groups = match.groupdict()
for key, val in groups.items():
if key not in ('separator', 'sign'):
if val is None:
groups[key] = "0n"
#print groups[key]
if key in ('years', 'months'):
groups[key] = Decimal(groups[key][:-1].replace(',', '.'))
else:
# these values are passed into a timedelta object, which works with floats.
groups[key] = float(groups[key][:-1].replace(',', '.'))
if groups["years"] == 0 and groups["months"] == 0:
ret = timedelta(days=groups["days"], hours=groups["hours"],
minutes=groups["minutes"], seconds=groups["seconds"],
weeks=groups["weeks"])
if groups["sign"] == '-':
ret = timedelta(0) - ret
else:
ret = Duration(years=groups["years"], months=groups["months"],
days=groups["days"], hours=groups["hours"],
minutes=groups["minutes"], seconds=groups["seconds"],
weeks=groups["weeks"])
if groups["sign"] == '-':
ret = Duration(0) - ret
return ret
def duration_isoformat(tduration, format=D_DEFAULT):
'''
Format duration strings.
This method is just a wrapper around isodate.isostrf.strftime and uses
P%P (D_DEFAULT) as default format.
'''
# TODO: implement better decision for negative Durations.
# should be done in Duration class in consistent way with timedelta.
if ((isinstance(tduration, Duration) and (tduration.years < 0 or
tduration.months < 0 or
tduration.tdelta < timedelta(0)))
or (isinstance(tduration, timedelta) and (tduration < timedelta(0)))):
ret = '-'
else:
ret = ''
ret += strftime(tduration, format)
return ret

View File

@ -0,0 +1,32 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
This module defines all exception classes in the whole package.
'''
class ISO8601Error(ValueError):
'''Raised when the given ISO string can not be parsed.'''

View File

@ -0,0 +1,207 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
"""
This module provides an alternative strftime method.
The strftime method in this module allows only a subset of Python's strftime
format codes, plus a few additional. It supports the full range of date values
possible with standard Python date/time objects. Furthermore there are several
pr-defined format strings in this module to make ease producing of ISO 8601
conforming strings.
"""
import re
from datetime import date, timedelta
from isodate.duration import Duration
from isodate.isotzinfo import tz_isoformat
# Date specific format strings
DATE_BAS_COMPLETE = '%Y%m%d'
DATE_EXT_COMPLETE = '%Y-%m-%d'
DATE_BAS_WEEK_COMPLETE = '%YW%W%w'
DATE_EXT_WEEK_COMPLETE = '%Y-W%W-%w'
DATE_BAS_ORD_COMPLETE = '%Y%j'
DATE_EXT_ORD_COMPLETE = '%Y-%j'
DATE_BAS_WEEK = '%YW%W'
DATE_EXT_WEEK = '%Y-W%W'
DATE_MONTH = '%Y-%m'
DATE_YEAR = '%Y'
DATE_CENTURY = '%C'
# Time specific format strings
TIME_BAS_COMPLETE = '%H%M%S'
TIME_EXT_COMPLETE = '%H:%M:%S'
TIME_BAS_MINUTE = '%H%M'
TIME_EXT_MINUTE = '%H:%M'
TIME_HOUR = '%H'
# Time zone formats
TZ_BAS = '%z'
TZ_EXT = '%Z'
TZ_HOUR = '%h'
# DateTime formats
DT_EXT_COMPLETE = DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE + TZ_EXT
DT_BAS_COMPLETE = DATE_BAS_COMPLETE + 'T' + TIME_BAS_COMPLETE + TZ_BAS
DT_EXT_ORD_COMPLETE = DATE_EXT_ORD_COMPLETE + 'T' + TIME_EXT_COMPLETE + TZ_EXT
DT_BAS_ORD_COMPLETE = DATE_BAS_ORD_COMPLETE + 'T' + TIME_BAS_COMPLETE + TZ_BAS
DT_EXT_WEEK_COMPLETE = DATE_EXT_WEEK_COMPLETE + 'T' + TIME_EXT_COMPLETE +\
TZ_EXT
DT_BAS_WEEK_COMPLETE = DATE_BAS_WEEK_COMPLETE + 'T' + TIME_BAS_COMPLETE +\
TZ_BAS
# Duration formts
D_DEFAULT = 'P%P'
D_WEEK = 'P%p'
D_ALT_EXT = 'P' + DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE
D_ALT_BAS = 'P' + DATE_BAS_COMPLETE + 'T' + TIME_BAS_COMPLETE
D_ALT_EXT_ORD = 'P' + DATE_EXT_ORD_COMPLETE + 'T' + TIME_EXT_COMPLETE
D_ALT_BAS_ORD = 'P' + DATE_BAS_ORD_COMPLETE + 'T' + TIME_BAS_COMPLETE
STRF_DT_MAP = {'%d': lambda tdt, yds: '%02d' % tdt.day,
'%f': lambda tdt, yds: '%06d' % tdt.microsecond,
'%H': lambda tdt, yds: '%02d' % tdt.hour,
'%j': lambda tdt, yds: '%03d' % (tdt.toordinal() -
date(tdt.year, 1, 1).toordinal() +
1),
'%m': lambda tdt, yds: '%02d' % tdt.month,
'%M': lambda tdt, yds: '%02d' % tdt.minute,
'%S': lambda tdt, yds: '%02d' % tdt.second,
'%w': lambda tdt, yds: '%1d' % tdt.isoweekday(),
'%W': lambda tdt, yds: '%02d' % tdt.isocalendar()[1],
'%Y': lambda tdt, yds: (((yds != 4) and '+') or '') +\
(('%%0%dd' % yds) % tdt.year),
'%C': lambda tdt, yds: (((yds != 4) and '+') or '') +\
(('%%0%dd' % (yds - 2)) % (tdt.year / 100)),
'%h': lambda tdt, yds: tz_isoformat(tdt, '%h'),
'%Z': lambda tdt, yds: tz_isoformat(tdt, '%Z'),
'%z': lambda tdt, yds: tz_isoformat(tdt, '%z'),
'%%': lambda tdt, yds: '%'}
STRF_D_MAP = {'%d': lambda tdt, yds: '%02d' % tdt.days,
'%f': lambda tdt, yds: '%06d' % tdt.microseconds,
'%H': lambda tdt, yds: '%02d' % (tdt.seconds / 60 / 60),
'%m': lambda tdt, yds: '%02d' % tdt.months,
'%M': lambda tdt, yds: '%02d' % ((tdt.seconds / 60) % 60),
'%S': lambda tdt, yds: '%02d' % (tdt.seconds % 60),
'%W': lambda tdt, yds: '%02d' % (abs(tdt.days / 7)),
'%Y': lambda tdt, yds: (((yds != 4) and '+') or '') +\
(('%%0%dd' % yds) % tdt.years),
'%C': lambda tdt, yds: (((yds != 4) and '+') or '') +\
(('%%0%dd' % (yds - 2)) %
(tdt.years / 100)),
'%%': lambda tdt, yds: '%'}
def _strfduration(tdt, format, yeardigits=4):
'''
this is the work method for timedelta and Duration instances.
see strftime for more details.
'''
def repl(match):
'''
lookup format command and return corresponding replacement.
'''
if match.group(0) in STRF_D_MAP:
return STRF_D_MAP[match.group(0)](tdt, yeardigits)
elif match.group(0) == '%P':
ret = []
if isinstance(tdt, Duration):
if tdt.years:
ret.append('%sY' % abs(tdt.years))
if tdt.months:
ret.append('%sM' % abs(tdt.months))
usecs = abs((tdt.days * 24 * 60 * 60 + tdt.seconds) * 1000000 +
tdt.microseconds)
seconds, usecs = divmod(usecs, 1000000)
minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)
days, hours = divmod(hours, 24)
if days:
ret.append('%sD' % days)
if hours or minutes or seconds or usecs:
ret.append('T')
if hours:
ret.append('%sH' % hours)
if minutes:
ret.append('%sM' % minutes)
if seconds or usecs:
if usecs:
ret.append(("%d.%06d" % (seconds, usecs)).rstrip('0'))
else:
ret.append("%d" % seconds)
ret.append('S')
# at least one component has to be there.
return ret and ''.join(ret) or '0D'
elif match.group(0) == '%p':
return str(abs(tdt.days // 7)) + 'W'
return match.group(0)
return re.sub('%d|%f|%H|%m|%M|%S|%W|%Y|%C|%%|%P|%p', repl,
format)
def _strfdt(tdt, format, yeardigits=4):
'''
this is the work method for time and date instances.
see strftime for more details.
'''
def repl(match):
'''
lookup format command and return corresponding replacement.
'''
if match.group(0) in STRF_DT_MAP:
return STRF_DT_MAP[match.group(0)](tdt, yeardigits)
return match.group(0)
return re.sub('%d|%f|%H|%j|%m|%M|%S|%w|%W|%Y|%C|%z|%Z|%h|%%', repl,
format)
def strftime(tdt, format, yeardigits=4):
'''
Directive Meaning Notes
%d Day of the month as a decimal number [01,31].
%f Microsecond as a decimal number [0,999999], zero-padded on the left (1)
%H Hour (24-hour clock) as a decimal number [00,23].
%j Day of the year as a decimal number [001,366].
%m Month as a decimal number [01,12].
%M Minute as a decimal number [00,59].
%S Second as a decimal number [00,61]. (3)
%w Weekday as a decimal number [0(Monday),6].
%W Week number of the year (Monday as the first day of the week) as a decimal number [00,53]. All days in a new year preceding the first Monday are considered to be in week 0. (4)
%Y Year with century as a decimal number. [0000,9999]
%C Century as a decimal number. [00,99]
%z UTC offset in the form +HHMM or -HHMM (empty string if the the object is naive). (5)
%Z Time zone name (empty string if the object is naive).
%P ISO8601 duration format.
%p ISO8601 duration format in weeks.
%% A literal '%' character.
'''
if isinstance(tdt, (timedelta, Duration)):
return _strfduration(tdt, format, yeardigits)
return _strfdt(tdt, format, yeardigits)

View File

@ -0,0 +1,157 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
This modules provides a method to parse an ISO 8601:2004 time string to a
Python datetime.time instance.
It supports all basic and extended formats including time zone specifications
as described in the ISO standard.
'''
import re
from decimal import Decimal
from datetime import time
from isodate.isostrf import strftime, TIME_EXT_COMPLETE, TZ_EXT
from isodate.isoerror import ISO8601Error
from isodate.isotzinfo import TZ_REGEX, build_tzinfo
TIME_REGEX_CACHE = []
# used to cache regular expressions to parse ISO time strings.
def build_time_regexps():
'''
Build regular expressions to parse ISO time string.
The regular expressions are compiled and stored in TIME_REGEX_CACHE
for later reuse.
'''
if not TIME_REGEX_CACHE:
# ISO 8601 time representations allow decimal fractions on least
# significant time component. Command and Full Stop are both valid
# fraction separators.
# The letter 'T' is allowed as time designator in front of a time
# expression.
# Immediately after a time expression, a time zone definition is
# allowed.
# a TZ may be missing (local time), be a 'Z' for UTC or a string of
# +-hh:mm where the ':mm' part can be skipped.
# TZ information patterns:
# ''
# Z
# +-hh:mm
# +-hhmm
# +-hh =>
# isotzinfo.TZ_REGEX
# 1. complete time:
# hh:mm:ss.ss ... extended format
TIME_REGEX_CACHE.append(re.compile(r"T?(?P<hour>[0-9]{2}):"
r"(?P<minute>[0-9]{2}):"
r"(?P<second>[0-9]{2}([,.][0-9]+)?)"
+ TZ_REGEX))
# hhmmss.ss ... basic format
TIME_REGEX_CACHE.append(re.compile(r"T?(?P<hour>[0-9]{2})"
r"(?P<minute>[0-9]{2})"
r"(?P<second>[0-9]{2}([,.][0-9]+)?)"
+ TZ_REGEX))
# 2. reduced accuracy:
# hh:mm.mm ... extended format
TIME_REGEX_CACHE.append(re.compile(r"T?(?P<hour>[0-9]{2}):"
r"(?P<minute>[0-9]{2}([,.][0-9]+)?)"
+ TZ_REGEX))
# hhmm.mm ... basic format
TIME_REGEX_CACHE.append(re.compile(r"T?(?P<hour>[0-9]{2})"
r"(?P<minute>[0-9]{2}([,.][0-9]+)?)"
+ TZ_REGEX))
# hh.hh ... basic format
TIME_REGEX_CACHE.append(re.compile(r"T?(?P<hour>[0-9]{2}([,.][0-9]+)?)"
+ TZ_REGEX))
return TIME_REGEX_CACHE
def parse_time(timestring):
'''
Parses ISO 8601 times into datetime.time objects.
Following ISO 8601 formats are supported:
(as decimal separator a ',' or a '.' is allowed)
hhmmss.ssTZD basic complete time
hh:mm:ss.ssTZD extended compelte time
hhmm.mmTZD basic reduced accuracy time
hh:mm.mmTZD extended reduced accuracy time
hh.hhTZD basic reduced accuracy time
TZD is the time zone designator which can be in the following format:
no designator indicates local time zone
Z UTC
+-hhmm basic hours and minutes
+-hh:mm extended hours and minutes
+-hh hours
'''
isotimes = build_time_regexps()
for pattern in isotimes:
match = pattern.match(timestring)
if match:
groups = match.groupdict()
for key, value in groups.items():
if value is not None:
groups[key] = value.replace(',', '.')
tzinfo = build_tzinfo(groups['tzname'], groups['tzsign'],
int(groups['tzhour'] or 0),
int(groups['tzmin'] or 0))
if 'second' in groups:
# round to microseconds if fractional seconds are more precise
second = Decimal(groups['second']).quantize(Decimal('.000001'))
microsecond = (second - int(second)) * long(1e6)
# int(...) ... no rounding
# to_integral() ... rounding
return time(int(groups['hour']), int(groups['minute']),
int(second), int(microsecond.to_integral()), tzinfo)
if 'minute' in groups:
minute = Decimal(groups['minute'])
second = (minute - int(minute)) * 60
microsecond = (second - int(second)) * long(1e6)
return time(int(groups['hour']), int(minute), int(second),
int(microsecond.to_integral()), tzinfo)
else:
microsecond, second, minute = 0, 0, 0
hour = Decimal(groups['hour'])
minute = (hour - int(hour)) * 60
second = (minute - int(minute)) * 60
microsecond = (second - int(second)) * long(1e6)
return time(int(hour), int(minute), int(second),
int(microsecond.to_integral()), tzinfo)
raise ISO8601Error('Unrecognised ISO 8601 time format: %r' % timestring)
def time_isoformat(ttime, format=TIME_EXT_COMPLETE + TZ_EXT):
'''
Format time strings.
This method is just a wrapper around isodate.isostrf.strftime and uses
Time-Extended-Complete with extended time zone as default format.
'''
return strftime(ttime, format)

View File

@ -0,0 +1,109 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
This module provides an ISO 8601:2004 time zone info parser.
It offers a function to parse the time zone offset as specified by ISO 8601.
'''
import re
from isodate.isoerror import ISO8601Error
from isodate.tzinfo import UTC, FixedOffset, ZERO
TZ_REGEX = r"(?P<tzname>(Z|(?P<tzsign>[+-])"\
r"(?P<tzhour>[0-9]{2})(:(?P<tzmin>[0-9]{2}))?)?)"
TZ_RE = re.compile(TZ_REGEX)
def build_tzinfo(tzname, tzsign='+', tzhour=0, tzmin=0):
'''
create a tzinfo instance according to given parameters.
tzname:
'Z' ... return UTC
'' | None ... return None
other ... return FixedOffset
'''
if tzname is None or tzname == '':
return None
if tzname == 'Z':
return UTC
tzsign = ((tzsign == '-') and -1) or 1
return FixedOffset(tzsign * tzhour, tzsign * tzmin, tzname)
def parse_tzinfo(tzstring):
'''
Parses ISO 8601 time zone designators to tzinfo objecs.
A time zone designator can be in the following format:
no designator indicates local time zone
Z UTC
+-hhmm basic hours and minutes
+-hh:mm extended hours and minutes
+-hh hours
'''
match = TZ_RE.match(tzstring)
if match:
groups = match.groupdict()
return build_tzinfo(groups['tzname'], groups['tzsign'],
int(groups['tzhour'] or 0),
int(groups['tzmin'] or 0))
raise ISO8601Error('%s not a valid time zone info' % tzstring)
def tz_isoformat(dt, format='%Z'):
'''
return time zone offset ISO 8601 formatted.
The various ISO formats can be chosen with the format parameter.
if tzinfo is None returns ''
if tzinfo is UTC returns 'Z'
else the offset is rendered to the given format.
format:
%h ... +-HH
%z ... +-HHMM
%Z ... +-HH:MM
'''
tzinfo = dt.tzinfo
if (tzinfo is None) or (tzinfo.utcoffset(dt) is None):
return ''
if tzinfo.utcoffset(dt) == ZERO and tzinfo.dst(dt) == ZERO:
return 'Z'
tdelta = tzinfo.utcoffset(dt)
seconds = tdelta.days * 24 * 60 * 60 + tdelta.seconds
sign = ((seconds < 0) and '-') or '+'
seconds = abs(seconds)
minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)
if hours > 99:
raise OverflowError('can not handle differences > 99 hours')
if format == '%Z':
return '%s%02d:%02d' % (sign, hours, minutes)
elif format == '%z':
return '%s%02d%02d' % (sign, hours, minutes)
elif format == '%h':
return '%s%02d' % (sign, hours)
raise ValueError('unknown format string "%s"' % format)

View File

@ -0,0 +1,49 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
Collect all test suites into one TestSuite instance.
'''
import unittest
from isodate.tests import (test_date, test_time, test_datetime, test_duration,
test_strf, test_pickle)
def test_suite():
'''
Return a new TestSuite instance consisting of all available TestSuites.
'''
return unittest.TestSuite([
test_date.test_suite(),
test_time.test_suite(),
test_datetime.test_suite(),
test_duration.test_suite(),
test_strf.test_suite(),
test_pickle.test_suite(),
])
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')

View File

@ -0,0 +1,126 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
Test cases for the isodate module.
'''
import unittest
from datetime import date
from isodate import parse_date, ISO8601Error, date_isoformat
from isodate import DATE_CENTURY, DATE_YEAR, DATE_MONTH
from isodate import DATE_EXT_COMPLETE, DATE_BAS_COMPLETE
from isodate import DATE_BAS_ORD_COMPLETE, DATE_EXT_ORD_COMPLETE
from isodate import DATE_BAS_WEEK, DATE_BAS_WEEK_COMPLETE
from isodate import DATE_EXT_WEEK, DATE_EXT_WEEK_COMPLETE
# the following list contains tuples of ISO date strings and the expected
# result from the parse_date method. A result of None means an ISO8601Error
# is expected. The test cases are grouped into dates with 4 digit years
# and 6 digit years.
TEST_CASES = {4: [('19', date(1901, 1, 1), DATE_CENTURY),
('1985', date(1985, 1, 1), DATE_YEAR),
('1985-04', date(1985, 4, 1), DATE_MONTH),
('1985-04-12', date(1985, 4, 12), DATE_EXT_COMPLETE),
('19850412', date(1985, 4, 12), DATE_BAS_COMPLETE),
('1985102', date(1985, 4, 12), DATE_BAS_ORD_COMPLETE),
('1985-102', date(1985, 4, 12), DATE_EXT_ORD_COMPLETE),
('1985W155', date(1985, 4, 12), DATE_BAS_WEEK_COMPLETE),
('1985-W15-5', date(1985, 4, 12), DATE_EXT_WEEK_COMPLETE),
('1985W15', date(1985, 4, 8), DATE_BAS_WEEK),
('1985-W15', date(1985, 4, 8), DATE_EXT_WEEK),
('1989-W15', date(1989, 4, 10), DATE_EXT_WEEK),
('1989-W15-5', date(1989, 4, 14), DATE_EXT_WEEK_COMPLETE),
('1-W1-1', None, DATE_BAS_WEEK_COMPLETE)],
6: [('+0019', date(1901, 1, 1), DATE_CENTURY),
('+001985', date(1985, 1, 1), DATE_YEAR),
('+001985-04', date(1985, 4, 1), DATE_MONTH),
('+001985-04-12', date(1985, 4, 12), DATE_EXT_COMPLETE),
('+0019850412', date(1985, 4, 12), DATE_BAS_COMPLETE),
('+001985102', date(1985, 4, 12), DATE_BAS_ORD_COMPLETE),
('+001985-102', date(1985, 4, 12), DATE_EXT_ORD_COMPLETE),
('+001985W155', date(1985, 4, 12), DATE_BAS_WEEK_COMPLETE),
('+001985-W15-5', date(1985, 4, 12), DATE_EXT_WEEK_COMPLETE),
('+001985W15', date(1985, 4, 8), DATE_BAS_WEEK),
('+001985-W15', date(1985, 4, 8), DATE_EXT_WEEK)]}
def create_testcase(yeardigits, datestring, expectation, format):
'''
Create a TestCase class for a specific test.
This allows having a separate TestCase for each test tuple from the
TEST_CASES list, so that a failed test won't stop other tests.
'''
class TestDate(unittest.TestCase):
'''
A test case template to parse an ISO date string into a date
object.
'''
def test_parse(self):
'''
Parse an ISO date string and compare it to the expected value.
'''
if expectation is None:
self.assertRaises(ISO8601Error, parse_date, datestring,
yeardigits)
else:
result = parse_date(datestring, yeardigits)
self.assertEqual(result, expectation)
def test_format(self):
'''
Take date object and create ISO string from it.
This is the reverse test to test_parse.
'''
if expectation is None:
self.assertRaises(AttributeError,
date_isoformat, expectation, format,
yeardigits)
else:
self.assertEqual(date_isoformat(expectation, format,
yeardigits),
datestring)
return unittest.TestLoader().loadTestsFromTestCase(TestDate)
def test_suite():
'''
Construct a TestSuite instance for all test cases.
'''
suite = unittest.TestSuite()
for yeardigits, tests in TEST_CASES.items():
for datestring, expectation, format in tests:
suite.addTest(create_testcase(yeardigits, datestring,
expectation, format))
return suite
# load_tests Protocol
def load_tests(loader, tests, pattern):
return test_suite()
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')

View File

@ -0,0 +1,138 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
Test cases for the isodatetime module.
'''
import unittest
from datetime import datetime
from isodate import parse_datetime, UTC, FixedOffset, datetime_isoformat
from isodate import DATE_BAS_COMPLETE, TIME_BAS_MINUTE, TIME_BAS_COMPLETE
from isodate import DATE_EXT_COMPLETE, TIME_EXT_MINUTE, TIME_EXT_COMPLETE
from isodate import TZ_BAS, TZ_EXT, TZ_HOUR
from isodate import DATE_BAS_ORD_COMPLETE, DATE_EXT_ORD_COMPLETE
from isodate import DATE_BAS_WEEK_COMPLETE, DATE_EXT_WEEK_COMPLETE
# the following list contains tuples of ISO datetime strings and the expected
# result from the parse_datetime method. A result of None means an ISO8601Error
# is expected.
TEST_CASES = [('19850412T1015', datetime(1985, 4, 12, 10, 15),
DATE_BAS_COMPLETE + 'T' + TIME_BAS_MINUTE,
'19850412T1015'),
('1985-04-12T10:15', datetime(1985, 4, 12, 10, 15),
DATE_EXT_COMPLETE + 'T' + TIME_EXT_MINUTE,
'1985-04-12T10:15'),
('1985102T1015Z', datetime(1985, 4, 12, 10, 15, tzinfo=UTC),
DATE_BAS_ORD_COMPLETE + 'T' + TIME_BAS_MINUTE + TZ_BAS,
'1985102T1015Z'),
('1985-102T10:15Z', datetime(1985, 4, 12, 10, 15, tzinfo=UTC),
DATE_EXT_ORD_COMPLETE + 'T' + TIME_EXT_MINUTE + TZ_EXT,
'1985-102T10:15Z'),
('1985W155T1015+0400', datetime(1985, 4, 12, 10, 15,
tzinfo=FixedOffset(4, 0,
'+0400')),
DATE_BAS_WEEK_COMPLETE + 'T' + TIME_BAS_MINUTE + TZ_BAS,
'1985W155T1015+0400'),
('1985-W15-5T10:15+04', datetime(1985, 4, 12, 10, 15,
tzinfo=FixedOffset(4, 0,
'+0400'),),
DATE_EXT_WEEK_COMPLETE + 'T' + TIME_EXT_MINUTE + TZ_HOUR,
'1985-W15-5T10:15+04'),
('20110410T101225.123000Z',
datetime(2011, 4, 10, 10, 12, 25, 123000, tzinfo=UTC),
DATE_BAS_COMPLETE + 'T' + TIME_BAS_COMPLETE + ".%f" + TZ_BAS,
'20110410T101225.123000Z'),
('2012-10-12T08:29:46.069178Z',
datetime(2012, 10, 12, 8, 29, 46, 69178, tzinfo=UTC),
DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE + '.%f' + TZ_BAS,
'2012-10-12T08:29:46.069178Z'),
('2012-10-12T08:29:46.691780Z',
datetime(2012, 10, 12, 8, 29, 46, 691780, tzinfo=UTC),
DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE + '.%f' + TZ_BAS,
'2012-10-12T08:29:46.691780Z'),
('2012-10-30T08:55:22.1234567Z',
datetime(2012, 10, 30, 8, 55, 22, 123457, tzinfo=UTC),
DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE + '.%f' + TZ_BAS,
'2012-10-30T08:55:22.123457Z'),
('2012-10-30T08:55:22.1234561Z',
datetime(2012, 10, 30, 8, 55, 22, 123456, tzinfo=UTC),
DATE_EXT_COMPLETE + 'T' + TIME_EXT_COMPLETE + '.%f' + TZ_BAS,
'2012-10-30T08:55:22.123456Z')
]
def create_testcase(datetimestring, expectation, format, output):
"""
Create a TestCase class for a specific test.
This allows having a separate TestCase for each test tuple from the
TEST_CASES list, so that a failed test won't stop other tests.
"""
class TestDateTime(unittest.TestCase):
'''
A test case template to parse an ISO datetime string into a
datetime object.
'''
def test_parse(self):
'''
Parse an ISO datetime string and compare it to the expected value.
'''
result = parse_datetime(datetimestring)
self.assertEqual(result, expectation)
def test_format(self):
'''
Take datetime object and create ISO string from it.
This is the reverse test to test_parse.
'''
if expectation is None:
self.assertRaises(AttributeError,
datetime_isoformat, expectation, format)
else:
self.assertEqual(datetime_isoformat(expectation, format),
output)
return unittest.TestLoader().loadTestsFromTestCase(TestDateTime)
def test_suite():
'''
Construct a TestSuite instance for all test cases.
'''
suite = unittest.TestSuite()
for datetimestring, expectation, format, output in TEST_CASES:
suite.addTest(create_testcase(datetimestring, expectation, format, output))
return suite
# load_tests Protocol
def load_tests(loader, tests, pattern):
return test_suite()
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')

View File

@ -0,0 +1,519 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
Test cases for the isoduration module.
'''
import unittest
import operator
from datetime import timedelta, date, datetime
from isodate import Duration, parse_duration, ISO8601Error
from isodate import D_DEFAULT, D_WEEK, D_ALT_EXT, duration_isoformat
# the following list contains tuples of ISO duration strings and the expected
# result from the parse_duration method. A result of None means an ISO8601Error
# is expected.
PARSE_TEST_CASES = {'P18Y9M4DT11H9M8S': (Duration(4, 8, 0, 0, 9, 11, 0, 9, 18),
D_DEFAULT, None),
'P2W': (timedelta(weeks=2), D_WEEK, None),
'P3Y6M4DT12H30M5S': (Duration(4, 5, 0, 0, 30, 12, 0, 6, 3),
D_DEFAULT, None),
'P23DT23H': (timedelta(hours=23, days=23),
D_DEFAULT, None),
'P4Y': (Duration(years=4), D_DEFAULT, None),
'P1M': (Duration(months=1), D_DEFAULT, None),
'PT1M': (timedelta(minutes=1), D_DEFAULT, None),
'P0.5Y': (Duration(years=0.5), D_DEFAULT, None),
'PT36H': (timedelta(hours=36), D_DEFAULT, 'P1DT12H'),
'P1DT12H': (timedelta(days=1, hours=12), D_DEFAULT, None),
'+P11D': (timedelta(days=11), D_DEFAULT, 'P11D'),
'-P2W': (timedelta(weeks=-2), D_WEEK, None),
'-P2.2W': (timedelta(weeks=-2.2), D_DEFAULT,
'-P15DT9H36M'),
'P1DT2H3M4S': (timedelta(days=1, hours=2, minutes=3,
seconds=4), D_DEFAULT, None),
'P1DT2H3M': (timedelta(days=1, hours=2, minutes=3),
D_DEFAULT, None),
'P1DT2H': (timedelta(days=1, hours=2), D_DEFAULT, None),
'PT2H': (timedelta(hours=2), D_DEFAULT, None),
'PT2.3H': (timedelta(hours=2.3), D_DEFAULT, 'PT2H18M'),
'PT2H3M4S': (timedelta(hours=2, minutes=3, seconds=4),
D_DEFAULT, None),
'PT3M4S': (timedelta(minutes=3, seconds=4), D_DEFAULT,
None),
'PT22S': (timedelta(seconds=22), D_DEFAULT, None),
'PT22.22S': (timedelta(seconds=22.22), 'PT%S.%fS',
'PT22.220000S'),
'-P2Y': (Duration(years=-2), D_DEFAULT, None),
'-P3Y6M4DT12H30M5S': (Duration(-4, -5, 0, 0, -30, -12, 0,
-6, -3), D_DEFAULT, None),
'-P1DT2H3M4S': (timedelta(days=-1, hours=-2, minutes=-3,
seconds=-4), D_DEFAULT, None),
# alternative format
'P0018-09-04T11:09:08': (Duration(4, 8, 0, 0, 9, 11, 0, 9,
18), D_ALT_EXT, None),
#'PT000022.22': timedelta(seconds=22.22),
}
# d1 d2 '+', '-', '>'
# A list of test cases to test addition and subtraction between datetime and
# Duration objects.
# each tuple contains 2 duration strings, and a result string for addition and
# one for subtraction. The last value says, if the first duration is greater
# than the second.
MATH_TEST_CASES = (('P5Y7M1DT9H45M16.72S', 'PT27M24.68S',
'P5Y7M1DT10H12M41.4S', 'P5Y7M1DT9H17M52.04S', None),
('PT28M12.73S', 'PT56M29.92S',
'PT1H24M42.65S', '-PT28M17.19S', False),
('P3Y7M23DT5H25M0.33S', 'PT1H1.95S',
'P3Y7M23DT6H25M2.28S', 'P3Y7M23DT4H24M58.38S', None),
('PT1H1.95S', 'P3Y7M23DT5H25M0.33S',
'P3Y7M23DT6H25M2.28S', '-P3Y7M23DT4H24M58.38S', None),
('P1332DT55M0.33S', 'PT1H1.95S',
'P1332DT1H55M2.28S', 'P1331DT23H54M58.38S', True),
('PT1H1.95S', 'P1332DT55M0.33S',
'P1332DT1H55M2.28S', '-P1331DT23H54M58.38S', False))
# A list of test cases to test addition and subtraction of date/datetime
# and Duration objects. They are tested against the results of an
# equal long timedelta duration.
DATE_TEST_CASES = ( (date(2008, 2, 29),
timedelta(days=10, hours=12, minutes=20),
Duration(days=10, hours=12, minutes=20)),
(date(2008, 1, 31),
timedelta(days=10, hours=12, minutes=20),
Duration(days=10, hours=12, minutes=20)),
(datetime(2008, 2, 29),
timedelta(days=10, hours=12, minutes=20),
Duration(days=10, hours=12, minutes=20)),
(datetime(2008, 1, 31),
timedelta(days=10, hours=12, minutes=20),
Duration(days=10, hours=12, minutes=20)),
(datetime(2008, 4, 21),
timedelta(days=10, hours=12, minutes=20),
Duration(days=10, hours=12, minutes=20)),
(datetime(2008, 5, 5),
timedelta(days=10, hours=12, minutes=20),
Duration(days=10, hours=12, minutes=20)),
(datetime(2000, 1, 1),
timedelta(hours=-33),
Duration(hours=-33)),
(datetime(2008, 5, 5),
Duration(years=1, months=1, days=10, hours=12,
minutes=20),
Duration(months=13, days=10, hours=12, minutes=20)),
(datetime(2000, 3, 30),
Duration(years=1, months=1, days=10, hours=12,
minutes=20),
Duration(months=13, days=10, hours=12, minutes=20)),
)
# A list of test cases of additon of date/datetime and Duration. The results
# are compared against a given expected result.
DATE_CALC_TEST_CASES = (
(date(2000, 2, 1),
Duration(years=1, months=1),
date(2001, 3, 1)),
(date(2000, 2, 29),
Duration(years=1, months=1),
date(2001, 3, 29)),
(date(2000, 2, 29),
Duration(years=1),
date(2001, 2, 28)),
(date(1996, 2, 29),
Duration(years=4),
date(2000, 2, 29)),
(date(2096, 2, 29),
Duration(years=4),
date(2100, 2, 28)),
(date(2000, 2, 1),
Duration(years=-1, months=-1),
date(1999, 1, 1)),
(date(2000, 2, 29),
Duration(years=-1, months=-1),
date(1999, 1, 29)),
(date(2000, 2, 1),
Duration(years=1, months=1, days=1),
date(2001, 3, 2)),
(date(2000, 2, 29),
Duration(years=1, months=1, days=1),
date(2001, 3, 30)),
(date(2000, 2, 29),
Duration(years=1, days=1),
date(2001, 3, 1)),
(date(1996, 2, 29),
Duration(years=4, days=1),
date(2000, 3, 1)),
(date(2096, 2, 29),
Duration(years=4, days=1),
date(2100, 3, 1)),
(date(2000, 2, 1),
Duration(years=-1, months=-1, days=-1),
date(1998, 12, 31)),
(date(2000, 2, 29),
Duration(years=-1, months=-1, days=-1),
date(1999, 1, 28)),
(date(2001, 4, 1),
Duration(years=-1, months=-1, days=-1),
date(2000, 2, 29)),
(date(2000, 4, 1),
Duration(years=-1, months=-1, days=-1),
date(1999, 2, 28)),
(Duration(years=1, months=2),
Duration(years=0, months=0, days=1),
Duration(years=1, months=2, days=1)),
(Duration(years=-1, months=-1, days=-1),
date(2000, 4, 1),
date(1999, 2, 28)),
(Duration(years=1, months=1, weeks=5),
date(2000, 1, 30),
date(2001, 4, 4)),
(parse_duration("P1Y1M5W"),
date(2000, 1, 30),
date(2001, 4, 4)),
(parse_duration("P0.5Y"),
date(2000, 1, 30),
None),
(Duration(years=1, months=1, hours=3),
datetime(2000, 1, 30, 12, 15, 00),
datetime(2001, 2, 28, 15, 15, 00)),
(parse_duration("P1Y1MT3H"),
datetime(2000, 1, 30, 12, 15, 00),
datetime(2001, 2, 28, 15, 15, 00)),
(Duration(years=1, months=2),
timedelta(days=1),
Duration(years=1, months=2, days=1)),
(timedelta(days=1),
Duration(years=1, months=2),
Duration(years=1, months=2, days=1)),
(datetime(2008, 1, 1, 0, 2),
Duration(months=1),
datetime(2008, 2, 1, 0, 2)),
(datetime.strptime("200802", "%Y%M"),
parse_duration("P1M"),
datetime(2008, 2, 1, 0, 2)),
(datetime(2008, 2, 1),
Duration(months=1),
datetime(2008, 3, 1)),
(datetime.strptime("200802", "%Y%m"),
parse_duration("P1M"),
datetime(2008, 3, 1)),
# (date(2000, 1, 1),
# Duration(years=1.5),
# date(2001, 6, 1)),
# (date(2000, 1, 1),
# Duration(years=1, months=1.5),
# date(2001, 2, 14)),
)
class DurationTest(unittest.TestCase):
'''
This class tests various other aspects of the isoduration module,
which are not covered with the test cases listed above.
'''
def test_associative(self):
'''
Adding 2 durations to a date is not associative.
'''
days1 = Duration(days=1)
months1 = Duration(months=1)
start = date(2000, 3, 30)
res1 = start + days1 + months1
res2 = start + months1 + days1
self.assertNotEqual(res1, res2)
def test_typeerror(self):
'''
Test if TypError is raised with certain parameters.
'''
self.assertRaises(TypeError, parse_duration, date(2000, 1, 1))
self.assertRaises(TypeError, operator.sub, Duration(years=1),
date(2000, 1, 1))
self.assertRaises(TypeError, operator.sub, 'raise exc',
Duration(years=1))
self.assertRaises(TypeError, operator.add,
Duration(years=1, months=1, weeks=5),
'raise exception')
self.assertRaises(TypeError, operator.add, 'raise exception',
Duration(years=1, months=1, weeks=5))
def test_parseerror(self):
'''
Test for unparseable duration string.
'''
self.assertRaises(ISO8601Error, parse_duration, 'T10:10:10')
def test_repr(self):
'''
Test __repr__ and __str__ for Duration obqects.
'''
dur = Duration(10, 10, years=10, months=10)
self.assertEqual('10 years, 10 months, 10 days, 0:00:10', str(dur))
self.assertEqual('isodate.duration.Duration(10, 10, 0,'
' years=10, months=10)', repr(dur))
def test_neg(self):
'''
Test __neg__ for Duration objects.
'''
self.assertEqual(-Duration(0), Duration(0))
self.assertEqual(-Duration(years=1, months=1),
Duration(years=-1, months=-1))
self.assertEqual(-Duration(years=1, months=1), Duration(months=-13))
self.assertNotEqual(-Duration(years=1), timedelta(days=-365))
self.assertNotEqual(-timedelta(days=365), Duration(years=-1))
# FIXME: this test fails in python 3... it seems like python3
# treats a == b the same b == a
#self.assertNotEqual(-timedelta(days=10), -Duration(days=10))
def test_format(self):
'''
Test various other strftime combinations.
'''
self.assertEqual(duration_isoformat(Duration(0)), 'P0D')
self.assertEqual(duration_isoformat(-Duration(0)), 'P0D')
self.assertEqual(duration_isoformat(Duration(seconds=10)), 'PT10S')
self.assertEqual(duration_isoformat(Duration(years=-1, months=-1)),
'-P1Y1M')
self.assertEqual(duration_isoformat(-Duration(years=1, months=1)),
'-P1Y1M')
self.assertEqual(duration_isoformat(-Duration(years=-1, months=-1)),
'P1Y1M')
self.assertEqual(duration_isoformat(-Duration(years=-1, months=-1)),
'P1Y1M')
dur = Duration(years=3, months=7, days=23, hours=5, minutes=25,
milliseconds=330)
self.assertEqual(duration_isoformat(dur), 'P3Y7M23DT5H25M0.33S')
self.assertEqual(duration_isoformat(-dur), '-P3Y7M23DT5H25M0.33S')
def test_equal(self):
'''
Test __eq__ and __ne__ methods.
'''
self.assertEqual(Duration(years=1, months=1),
Duration(years=1, months=1))
self.assertEqual(Duration(years=1, months=1), Duration(months=13))
self.assertNotEqual(Duration(years=1, months=2),
Duration(years=1, months=1))
self.assertNotEqual(Duration(years=1, months=1), Duration(months=14))
self.assertNotEqual(Duration(years=1), timedelta(days=365))
self.assertFalse(Duration(years=1, months=1) !=
Duration(years=1, months=1))
self.assertFalse(Duration(years=1, months=1) != Duration(months=13))
self.assertTrue(Duration(years=1, months=2) !=
Duration(years=1, months=1))
self.assertTrue(Duration(years=1, months=1) != Duration(months=14))
self.assertTrue(Duration(years=1) != timedelta(days=365))
self.assertEqual(Duration(days=1), timedelta(days=1))
# FIXME: this test fails in python 3... it seems like python3
# treats a != b the same b != a
#self.assertNotEqual(timedelta(days=1), Duration(days=1))
def test_totimedelta(self):
'''
Test conversion form Duration to timedelta.
'''
dur = Duration(years=1, months=2, days=10)
self.assertEqual(dur.totimedelta(datetime(1998, 2, 25)), timedelta(434))
# leap year has one day more in february
self.assertEqual(dur.totimedelta(datetime(2000, 2, 25)), timedelta(435))
dur = Duration(months=2)
# march is longer than february, but april is shorter than march (cause only one day difference compared to 2)
self.assertEqual(dur.totimedelta(datetime(2000, 2, 25)), timedelta(60))
self.assertEqual(dur.totimedelta(datetime(2001, 2, 25)), timedelta(59))
self.assertEqual(dur.totimedelta(datetime(2001, 3, 25)), timedelta(61))
def create_parsetestcase(durationstring, expectation, format, altstr):
"""
Create a TestCase class for a specific test.
This allows having a separate TestCase for each test tuple from the
PARSE_TEST_CASES list, so that a failed test won't stop other tests.
"""
class TestParseDuration(unittest.TestCase):
'''
A test case template to parse an ISO duration string into a
timedelta or Duration object.
'''
def test_parse(self):
'''
Parse an ISO duration string and compare it to the expected value.
'''
result = parse_duration(durationstring)
self.assertEqual(result, expectation)
def test_format(self):
'''
Take duration/timedelta object and create ISO string from it.
This is the reverse test to test_parse.
'''
if altstr:
self.assertEqual(duration_isoformat(expectation, format),
altstr)
else:
# if durationstring == '-P2W':
# import pdb; pdb.set_trace()
self.assertEqual(duration_isoformat(expectation, format),
durationstring)
return unittest.TestLoader().loadTestsFromTestCase(TestParseDuration)
def create_mathtestcase(dur1, dur2, resadd, ressub, resge):
"""
Create a TestCase class for a specific test.
This allows having a separate TestCase for each test tuple from the
MATH_TEST_CASES list, so that a failed test won't stop other tests.
"""
dur1 = parse_duration(dur1)
dur2 = parse_duration(dur2)
resadd = parse_duration(resadd)
ressub = parse_duration(ressub)
class TestMathDuration(unittest.TestCase):
'''
A test case template test addition, subtraction and >
operators for Duration objects.
'''
def test_add(self):
'''
Test operator + (__add__, __radd__)
'''
self.assertEqual(dur1 + dur2, resadd)
def test_sub(self):
'''
Test operator - (__sub__, __rsub__)
'''
self.assertEqual(dur1 - dur2, ressub)
def test_ge(self):
'''
Test operator > and <
'''
def dogetest():
''' Test greater than.'''
return dur1 > dur2
def doletest():
''' Test less than.'''
return dur1 < dur2
if resge is None:
self.assertRaises(TypeError, dogetest)
self.assertRaises(TypeError, doletest)
else:
self.assertEqual(dogetest(), resge)
self.assertEqual(doletest(), not resge)
return unittest.TestLoader().loadTestsFromTestCase(TestMathDuration)
def create_datetestcase(start, tdelta, duration):
"""
Create a TestCase class for a specific test.
This allows having a separate TestCase for each test tuple from the
DATE_TEST_CASES list, so that a failed test won't stop other tests.
"""
class TestDateCalc(unittest.TestCase):
'''
A test case template test addition, subtraction
operators for Duration objects.
'''
def test_add(self):
'''
Test operator +.
'''
self.assertEqual(start + tdelta, start + duration)
def test_sub(self):
'''
Test operator -.
'''
self.assertEqual(start - tdelta, start - duration)
return unittest.TestLoader().loadTestsFromTestCase(TestDateCalc)
def create_datecalctestcase(start, duration, expectation):
"""
Create a TestCase class for a specific test.
This allows having a separate TestCase for each test tuple from the
DATE_CALC_TEST_CASES list, so that a failed test won't stop other tests.
"""
class TestDateCalc(unittest.TestCase):
'''
A test case template test addition operators for Duration objects.
'''
def test_calc(self):
'''
Test operator +.
'''
if expectation is None:
self.assertRaises(ValueError, operator.add, start, duration)
else:
self.assertEqual(start + duration, expectation)
return unittest.TestLoader().loadTestsFromTestCase(TestDateCalc)
def test_suite():
'''
Return a test suite containing all test defined above.
'''
suite = unittest.TestSuite()
for durationstring, (expectation, format, altstr) in PARSE_TEST_CASES.items():
suite.addTest(create_parsetestcase(durationstring, expectation,
format, altstr))
for testdata in MATH_TEST_CASES:
suite.addTest(create_mathtestcase(*testdata))
for testdata in DATE_TEST_CASES:
suite.addTest(create_datetestcase(*testdata))
for testdata in DATE_CALC_TEST_CASES:
suite.addTest(create_datecalctestcase(*testdata))
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(DurationTest))
return suite
# load_tests Protocol
def load_tests(loader, tests, pattern):
return test_suite()
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')

View File

@ -0,0 +1,35 @@
import unittest
import cPickle as pickle
import isodate
class TestPickle(unittest.TestCase):
'''
A test case template to parse an ISO datetime string into a
datetime object.
'''
def test_pickle(self):
'''
Parse an ISO datetime string and compare it to the expected value.
'''
dti = isodate.parse_datetime('2012-10-26T09:33+00:00')
pikl = pickle.dumps(dti, 2)
dto = pickle.loads(pikl)
self.assertEqual(dti, dto)
def test_suite():
'''
Construct a TestSuite instance for all test cases.
'''
suite = unittest.TestSuite()
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestPickle))
return suite
# load_tests Protocol
def load_tests(loader, tests, pattern):
return test_suite()
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')

View File

@ -0,0 +1,130 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
Test cases for the isodate module.
'''
import unittest
import time
from datetime import datetime, timedelta
from isodate import strftime
from isodate import LOCAL
from isodate import DT_EXT_COMPLETE
from isodate import tzinfo
TEST_CASES = ((datetime(2012, 12, 25, 13, 30, 0, 0, LOCAL), DT_EXT_COMPLETE,
"2012-12-25T13:30:00+10:00"),
# DST ON
(datetime(1999, 12, 25, 13, 30, 0, 0, LOCAL), DT_EXT_COMPLETE,
"1999-12-25T13:30:00+11:00"),
# microseconds
(datetime(2012, 10, 12, 8, 29, 46, 69178), "%Y-%m-%dT%H:%M:%S.%f",
"2012-10-12T08:29:46.069178"),
(datetime(2012, 10, 12, 8, 29, 46, 691780), "%Y-%m-%dT%H:%M:%S.%f",
"2012-10-12T08:29:46.691780"),
)
def create_testcase(dt, format, expectation):
"""
Create a TestCase class for a specific test.
This allows having a separate TestCase for each test tuple from the
TEST_CASES list, so that a failed test won't stop other tests.
"""
class TestDate(unittest.TestCase):
'''
A test case template to test ISO date formatting.
'''
# local time zone mock function
def localtime_mock(self, secs):
"""
mock time.localtime so that it always returns a time_struct with tm_idst=1
"""
tt = self.ORIG['localtime'](secs)
# befor 2000 everything is dst, after 2000 no dst.
if tt.tm_year < 2000:
dst = 1
else:
dst = 0
tt = (tt.tm_year, tt.tm_mon, tt.tm_mday,
tt.tm_hour, tt.tm_min, tt.tm_sec,
tt.tm_wday, tt.tm_yday, dst)
return time.struct_time(tt)
def setUp(self):
self.ORIG = {}
self.ORIG['STDOFFSET'] = tzinfo.STDOFFSET
self.ORIG['DSTOFFSET'] = tzinfo.DSTOFFSET
self.ORIG['DSTDIFF'] = tzinfo.DSTDIFF
self.ORIG['localtime'] = time.localtime
# ovveride all saved values with fixtures.
# calculate LOCAL TZ offset, so that this test runs in every time zone
tzinfo.STDOFFSET = timedelta(seconds=36000) # assume we are in +10:00
tzinfo.DSTOFFSET = timedelta(seconds=39600) # assume DST = +11:00
tzinfo.DSTDIFF = tzinfo.DSTOFFSET - tzinfo.STDOFFSET
time.localtime = self.localtime_mock
def tearDown(self):
# restore test fixtures
tzinfo.STDOFFSET = self.ORIG['STDOFFSET']
tzinfo.DSTOFFSET = self.ORIG['DSTOFFSET']
tzinfo.DSTDIFF = self.ORIG['DSTDIFF']
time.localtime = self.ORIG['localtime']
def test_format(self):
'''
Take date object and create ISO string from it.
This is the reverse test to test_parse.
'''
if expectation is None:
self.assertRaises(AttributeError,
strftime(dt, format))
else:
self.assertEqual(strftime(dt, format),
expectation)
return unittest.TestLoader().loadTestsFromTestCase(TestDate)
def test_suite():
'''
Construct a TestSuite instance for all test cases.
'''
suite = unittest.TestSuite()
for dt, format, expectation in TEST_CASES:
suite.addTest(create_testcase(dt, format, expectation))
return suite
# load_tests Protocol
def load_tests(loader, tests, pattern):
return test_suite()
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')

View File

@ -0,0 +1,143 @@
##############################################################################
# Copyright 2009, Gerhard Weis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the authors nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT
##############################################################################
'''
Test cases for the isotime module.
'''
import unittest
from datetime import time
from isodate import parse_time, UTC, FixedOffset, ISO8601Error, time_isoformat
from isodate import TIME_BAS_COMPLETE, TIME_BAS_MINUTE
from isodate import TIME_EXT_COMPLETE, TIME_EXT_MINUTE
from isodate import TIME_HOUR
from isodate import TZ_BAS, TZ_EXT, TZ_HOUR
# the following list contains tuples of ISO time strings and the expected
# result from the parse_time method. A result of None means an ISO8601Error
# is expected.
TEST_CASES = [('232050', time(23, 20, 50), TIME_BAS_COMPLETE + TZ_BAS),
('23:20:50', time(23, 20, 50), TIME_EXT_COMPLETE + TZ_EXT),
('2320', time(23, 20), TIME_BAS_MINUTE),
('23:20', time(23, 20), TIME_EXT_MINUTE),
('23', time(23), TIME_HOUR),
('232050,5', time(23, 20, 50, 500000), None),
('23:20:50.5', time(23, 20, 50, 500000), None),
# test precision
('15:33:42.123456', time(15, 33, 42, 123456), None),
('15:33:42.1234564', time(15, 33, 42, 123456), None),
('15:33:42.1234557', time(15, 33, 42, 123456), None),
('2320,8', time(23, 20, 48), None),
('23:20,8', time(23, 20, 48), None),
('23,3', time(23, 18), None),
('232030Z', time(23, 20, 30, tzinfo=UTC),
TIME_BAS_COMPLETE + TZ_BAS),
('2320Z', time(23, 20, tzinfo=UTC), TIME_BAS_MINUTE + TZ_BAS),
('23Z', time(23, tzinfo=UTC), TIME_HOUR + TZ_BAS),
('23:20:30Z', time(23, 20, 30, tzinfo=UTC),
TIME_EXT_COMPLETE + TZ_EXT),
('23:20Z', time(23, 20, tzinfo=UTC), TIME_EXT_MINUTE + TZ_EXT),
('152746+0100', time(15, 27, 46,
tzinfo=FixedOffset(1, 0, '+0100')),
TIME_BAS_COMPLETE + TZ_BAS),
('152746-0500', time(15, 27, 46,
tzinfo=FixedOffset(-5, 0, '-0500')),
TIME_BAS_COMPLETE + TZ_BAS),
('152746+01', time(15, 27, 46,
tzinfo=FixedOffset(1, 0, '+01:00')),
TIME_BAS_COMPLETE + TZ_HOUR),
('152746-05', time(15, 27, 46,
tzinfo=FixedOffset(-5, -0, '-05:00')),
TIME_BAS_COMPLETE + TZ_HOUR),
('15:27:46+01:00', time(15, 27, 46,
tzinfo=FixedOffset(1, 0, '+01:00')),
TIME_EXT_COMPLETE + TZ_EXT),
('15:27:46-05:00', time(15, 27, 46,
tzinfo=FixedOffset(-5, -0, '-05:00')),
TIME_EXT_COMPLETE + TZ_EXT),
('15:27:46+01', time(15, 27, 46,
tzinfo=FixedOffset(1, 0, '+01:00')),
TIME_EXT_COMPLETE + TZ_HOUR),
('15:27:46-05', time(15, 27, 46,
tzinfo=FixedOffset(-5, -0, '-05:00')),
TIME_EXT_COMPLETE + TZ_HOUR),
('1:17:30', None, TIME_EXT_COMPLETE)]
def create_testcase(timestring, expectation, format):
"""
Create a TestCase class for a specific test.
This allows having a separate TestCase for each test tuple from the
TEST_CASES list, so that a failed test won't stop other tests.
"""
class TestTime(unittest.TestCase):
'''
A test case template to parse an ISO time string into a time
object.
'''
def test_parse(self):
'''
Parse an ISO time string and compare it to the expected value.
'''
if expectation is None:
self.assertRaises(ISO8601Error, parse_time, timestring)
else:
result = parse_time(timestring)
self.assertEqual(result, expectation)
def test_format(self):
'''
Take time object and create ISO string from it.
This is the reverse test to test_parse.
'''
if expectation is None:
self.assertRaises(AttributeError,
time_isoformat, expectation, format)
elif format is not None:
self.assertEqual(time_isoformat(expectation, format),
timestring)
return unittest.TestLoader().loadTestsFromTestCase(TestTime)
def test_suite():
'''
Construct a TestSuite instance for all test cases.
'''
suite = unittest.TestSuite()
for timestring, expectation, format in TEST_CASES:
suite.addTest(create_testcase(timestring, expectation, format))
return suite
# load_tests Protocol
def load_tests(loader, tests, pattern):
return test_suite()
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')

View File

@ -0,0 +1,137 @@
'''
This module provides some datetime.tzinfo implementations.
All those classes are taken from the Python documentation.
'''
from datetime import timedelta, tzinfo
import time
ZERO = timedelta(0)
# constant for zero time offset.
class Utc(tzinfo):
'''UTC
Universal time coordinated time zone.
'''
def utcoffset(self, dt):
'''
Return offset from UTC in minutes east of UTC, which is ZERO for UTC.
'''
return ZERO
def tzname(self, dt):
'''
Return the time zone name corresponding to the datetime object dt, as a string.
'''
return "UTC"
def dst(self, dt):
'''
Return the daylight saving time (DST) adjustment, in minutes east of UTC.
'''
return ZERO
UTC = Utc()
# the default instance for UTC.
class FixedOffset(tzinfo):
'''
A class building tzinfo objects for fixed-offset time zones.
Note that FixedOffset(0, 0, "UTC") or FixedOffset() is a different way to
build a UTC tzinfo object.
'''
def __init__(self, offset_hours=0, offset_minutes=0, name="UTC"):
'''
Initialise an instance with time offset and name.
The time offset should be positive for time zones east of UTC
and negate for time zones west of UTC.
'''
self.__offset = timedelta(hours=offset_hours, minutes=offset_minutes)
self.__name = name
def utcoffset(self, dt):
'''
Return offset from UTC in minutes of UTC.
'''
return self.__offset
def tzname(self, dt):
'''
Return the time zone name corresponding to the datetime object dt, as a
string.
'''
return self.__name
def dst(self, dt):
'''
Return the daylight saving time (DST) adjustment, in minutes east of
UTC.
'''
return ZERO
def __repr__(self):
'''
Return nicely formatted repr string.
'''
return "<FixedOffset %r>" % self.__name
STDOFFSET = timedelta(seconds = -time.timezone)
# locale time zone offset
# calculate local daylight saving offset if any.
if time.daylight:
DSTOFFSET = timedelta(seconds = -time.altzone)
else:
DSTOFFSET = STDOFFSET
DSTDIFF = DSTOFFSET - STDOFFSET
# difference between local time zone and local DST time zone
class LocalTimezone(tzinfo):
"""
A class capturing the platform's idea of local time.
"""
def utcoffset(self, dt):
'''
Return offset from UTC in minutes of UTC.
'''
if self._isdst(dt):
return DSTOFFSET
else:
return STDOFFSET
def dst(self, dt):
'''
Return daylight saving offset.
'''
if self._isdst(dt):
return DSTDIFF
else:
return ZERO
def tzname(self, dt):
'''
Return the time zone name corresponding to the datetime object dt, as a
string.
'''
return time.tzname[self._isdst(dt)]
def _isdst(self, dt):
'''
Returns true if DST is active for given datetime object dt.
'''
tt = (dt.year, dt.month, dt.day,
dt.hour, dt.minute, dt.second,
dt.weekday(), 0, -1)
stamp = time.mktime(tt)
tt = time.localtime(stamp)
return tt.tm_isdst > 0
LOCAL = LocalTimezone()
# the default instance for local time zone.

View File

@ -0,0 +1,29 @@
from winrm.protocol import Protocol
class Response(object):
"""Response from a remote command execution"""
def __init__(self, args):
self.std_out, self.std_err, self.status_code = args
def __repr__(self):
#TODO put tree dots at the end if out/err was truncated
return '<Response code {0}, out "{1}", err "{2}">'.format(
self.status_code, self.std_out[:20], self.std_err[:20])
class Session(object):
#TODO implement context manager methods
def __init__(self, url, auth):
#TODO convert short urls into well-formed endpoint
username, password = auth
self.protocol = Protocol(url, username=username, password=password)
def run_cmd(self, command, args=()):
#TODO optimize perf. Do not call open/close shell every time
shell_id = self.protocol.open_shell()
command_id = self.protocol.run_command(shell_id, command, args)
rs = Response(self.protocol.get_command_output(shell_id, command_id))
self.protocol.cleanup_command(shell_id, command_id)
self.protocol.close_shell(shell_id)
return rs

View File

@ -0,0 +1,18 @@
class WinRMWebServiceError(Exception):
"""Generic WinRM SOAP Error"""
pass
class WinRMAuthorizationError(Exception):
"""Authorization Error"""
pass
class WinRMWSManFault(Exception):
"""A Fault returned in the SOAP response. The XML node is a WSManFault"""
pass
class WinRMTransportError(Exception):
""""Transport-level error"""
pass

View File

@ -0,0 +1,318 @@
import base64
from datetime import timedelta
import uuid
import xml.etree.ElementTree as ET
from isodate.isoduration import duration_isoformat
import xmltodict
from winrm.transport import HttpPlaintext, HttpKerberos, HttpSSL
class Protocol(object):
"""
This is the main class that does the SOAP request/response logic. There are a few helper classes, but pretty
much everything comes through here first.
"""
DEFAULT_TIMEOUT = 'PT60S'
DEFAULT_MAX_ENV_SIZE = 153600
DEFAULT_LOCALE = 'en-US'
def __init__(self, endpoint, transport='plaintext', username=None, password=None, realm=None, service=None, keytab=None, ca_trust_path=None, cert_pem=None, cert_key_pem=None):
"""
@param string endpoint: the WinRM webservice endpoint
@param string transport: transport type, one of 'kerberos' (default), 'ssl', 'plaintext'
@param string username: username
@param string password: password
@param string realm: the Kerberos realm we are authenticating to
@param string service: the service name, default is HTTP
@param string keytab: the path to a keytab file if you are using one
@param string ca_trust_path: Certification Authority trust path
@param string cert_pem: client authentication certificate file path in PEM format
@param string cert_key_pem: client authentication certificate key file path in PEM format
"""
self.endpoint = endpoint
self.timeout = Protocol.DEFAULT_TIMEOUT
self.max_env_sz = Protocol.DEFAULT_MAX_ENV_SIZE
self.locale = Protocol.DEFAULT_LOCALE
if transport == 'plaintext':
self.transport = HttpPlaintext(endpoint, username, password)
elif transport == 'kerberos':
self.transport = HttpKerberos(endpoint)
elif transport == 'ssl':
self.transport = HttpSSL(endpoint, username, password, cert_pem=cert_pem, cert_key_pem=cert_key_pem)
else:
raise NotImplementedError()
self.username = username
self.password = password
self.service = service
self.keytab = keytab
self.ca_trust_path = ca_trust_path
def set_timeout(self, seconds):
"""
Operation timeout, see http://msdn.microsoft.com/en-us/library/ee916629(v=PROT.13).aspx
@param int seconds: the number of seconds to set the timeout to. It will be converted to an ISO8601 format.
"""
# in original library there is an alias - op_timeout method
return duration_isoformat(timedelta(seconds))
def open_shell(self, i_stream='stdin', o_stream='stdout stderr', working_directory=None, env_vars=None, noprofile=False, codepage=437, lifetime=None, idle_timeout=None):
"""
Create a Shell on the destination host
@param string i_stream: Which input stream to open. Leave this alone unless you know what you're doing (default: stdin)
@param string o_stream: Which output stream to open. Leave this alone unless you know what you're doing (default: stdout stderr)
@param string working_directory: the directory to create the shell in
@param dict env_vars: environment variables to set for the shell. Fir instance: {'PATH': '%PATH%;c:/Program Files (x86)/Git/bin/', 'CYGWIN': 'nontsec codepage:utf8'}
@returns The ShellId from the SOAP response. This is our open shell instance on the remote machine.
@rtype string
"""
rq = {'env:Envelope': self._get_soap_header(
resource_uri='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd',
action='http://schemas.xmlsoap.org/ws/2004/09/transfer/Create')}
header = rq['env:Envelope']['env:Header']
header['w:OptionSet'] = {
'w:Option': [
{
'@Name': 'WINRS_NOPROFILE',
'#text': str(noprofile).upper() #TODO remove str call
},
{
'@Name': 'WINRS_CODEPAGE',
'#text': str(codepage) #TODO remove str call
}
]
}
shell = rq['env:Envelope'].setdefault('env:Body', {}).setdefault('rsp:Shell', {})
shell['rsp:InputStreams'] = i_stream
shell['rsp:OutputStreams'] = o_stream
if working_directory:
#TODO ensure that rsp:WorkingDirectory should be nested within rsp:Shell
shell['rsp:WorkingDirectory'] = working_directory
# TODO: research Lifetime a bit more: http://msdn.microsoft.com/en-us/library/cc251546(v=PROT.13).aspx
#if lifetime:
# shell['rsp:Lifetime'] = iso8601_duration.sec_to_dur(lifetime)
# TODO: make it so the input is given in milliseconds and converted to xs:duration
if idle_timeout:
shell['rsp:IdleTimeOut'] = idle_timeout
if env_vars:
env = shell.setdefault('rsp:Environment', {})
for key, value in env_vars.items():
env['rsp:Variable'] = {'@Name': key, '#text': value}
rs = self.send_message(xmltodict.unparse(rq))
#rs = xmltodict.parse(rs)
#return rs['s:Envelope']['s:Body']['x:ResourceCreated']['a:ReferenceParameters']['w:SelectorSet']['w:Selector']['#text']
root = ET.fromstring(rs)
return next(node for node in root.findall('.//*') if node.get('Name') == 'ShellId').text
# Helper method for building SOAP Header
def _get_soap_header(self, action=None, resource_uri=None, shell_id=None, message_id=None):
if not message_id:
message_id = uuid.uuid4()
header = {
'@xmlns:xsd': 'http://www.w3.org/2001/XMLSchema',
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'@xmlns:env': 'http://www.w3.org/2003/05/soap-envelope',
'@xmlns:a': 'http://schemas.xmlsoap.org/ws/2004/08/addressing',
'@xmlns:b': 'http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd',
'@xmlns:n': 'http://schemas.xmlsoap.org/ws/2004/09/enumeration',
'@xmlns:x': 'http://schemas.xmlsoap.org/ws/2004/09/transfer',
'@xmlns:w': 'http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd',
'@xmlns:p': 'http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd',
'@xmlns:rsp': 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell',
'@xmlns:cfg': 'http://schemas.microsoft.com/wbem/wsman/1/config',
'env:Header': {
'a:To': 'http://windows-host:5985/wsman',
'a:ReplyTo': {
'a:Address': {
'@mustUnderstand': 'true',
'#text': 'http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous'
}
},
'w:MaxEnvelopeSize': {
'@mustUnderstand': 'true',
'#text': '153600'
},
'a:MessageID': 'uuid:{0}'.format(message_id),
'w:Locale': {
'@mustUnderstand': 'false',
'@xml:lang': 'en-US'
},
'p:DataLocale': {
'@mustUnderstand': 'false',
'@xml:lang': 'en-US'
},
# TODO: research this a bit http://msdn.microsoft.com/en-us/library/cc251561(v=PROT.13).aspx
#'cfg:MaxTimeoutms': 600
'w:OperationTimeout': 'PT60S',
'w:ResourceURI': {
'@mustUnderstand': 'true',
'#text': resource_uri
},
'a:Action': {
'@mustUnderstand': 'true',
'#text': action
}
}
}
if shell_id:
header['env:Header']['w:SelectorSet'] = {
'w:Selector': {
'@Name': 'ShellId',
'#text': shell_id
}
}
return header
def send_message(self, message):
# TODO add message_id vs relates_to checking
# TODO port error handling code
return self.transport.send_message(message)
def close_shell(self, shell_id):
"""
Close the shell
@param string shell_id: The shell id on the remote machine. See #open_shell
@returns This should have more error checking but it just returns true for now.
@rtype bool
"""
message_id = uuid.uuid4()
rq = {'env:Envelope': self._get_soap_header(
resource_uri='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd',
action='http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete',
shell_id=shell_id,
message_id=message_id)}
# SOAP message requires empty env:Body
rq['env:Envelope'].setdefault('env:Body', {})
rs = self.send_message(xmltodict.unparse(rq))
root = ET.fromstring(rs)
relates_to = next(node for node in root.findall('.//*') if node.tag.endswith('RelatesTo')).text
# TODO change assert into user-friendly exception
assert uuid.UUID(relates_to.replace('uuid:', '')) == message_id
def run_command(self, shell_id, command, arguments=(), console_mode_stdin=True, skip_cmd_shell=False):
"""
Run a command on a machine with an open shell
@param string shell_id: The shell id on the remote machine. See #open_shell
@param string command: The command to run on the remote machine
@param iterable of string arguments: An array of arguments for this command
@param bool console_mode_stdin: (default: True)
@param bool skip_cmd_shell: (default: False)
@return: The CommandId from the SOAP response. This is the ID we need to query in order to get output.
@rtype string
"""
rq = {'env:Envelope': self._get_soap_header(
resource_uri='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd',
action='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command',
shell_id=shell_id)}
header = rq['env:Envelope']['env:Header']
header['w:OptionSet'] = {
'w:Option': [
{
'@Name': 'WINRS_CONSOLEMODE_STDIN',
'#text': str(console_mode_stdin).upper()
},
{
'@Name': 'WINRS_SKIP_CMD_SHELL',
'#text': str(skip_cmd_shell).upper()
}
]
}
cmd_line = rq['env:Envelope'].setdefault('env:Body', {})\
.setdefault('rsp:CommandLine', {})
cmd_line['rsp:Command'] = {'#text': command}
if arguments:
cmd_line['rsp:Arguments'] = ' '.join(arguments)
rs = self.send_message(xmltodict.unparse(rq))
root = ET.fromstring(rs)
command_id = next(node for node in root.findall('.//*') if node.tag.endswith('CommandId')).text
return command_id
def cleanup_command(self, shell_id, command_id):
"""
Clean-up after a command. @see #run_command
@param string shell_id: The shell id on the remote machine. See #open_shell
@param string command_id: The command id on the remote machine. See #run_command
@returns: This should have more error checking but it just returns true for now.
@rtype bool
"""
message_id = uuid.uuid4()
rq = {'env:Envelope': self._get_soap_header(
resource_uri='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd',
action='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Signal',
shell_id=shell_id,
message_id=message_id)}
# Signal the Command references to terminate (close stdout/stderr)
signal = rq['env:Envelope'].setdefault('env:Body', {}).setdefault('rsp:Signal', {})
signal['@CommandId'] = command_id
signal['rsp:Code'] = \
'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate'
rs = self.send_message(xmltodict.unparse(rq))
root = ET.fromstring(rs)
relates_to = next(node for node in root.findall('.//*') if node.tag.endswith('RelatesTo')).text
# TODO change assert into user-friendly exception
assert uuid.UUID(relates_to.replace('uuid:', '')) == message_id
def get_command_output(self, shell_id, command_id):
"""
Get the Output of the given shell and command
@param string shell_id: The shell id on the remote machine. See #open_shell
@param string command_id: The command id on the remote machine. See #run_command
#@return [Hash] Returns a Hash with a key :exitcode and :data. Data is an Array of Hashes where the cooresponding key
# is either :stdout or :stderr. The reason it is in an Array so so we can get the output in the order it ocurrs on
# the console.
"""
stdout_buffer, stderr_buffer = [], []
command_done = False
while not command_done:
stdout, stderr, return_code, command_done = \
self._raw_get_command_output(shell_id, command_id)
stdout_buffer.append(stdout)
stderr_buffer.append(stderr)
return ''.join(stdout_buffer), ''.join(stderr_buffer), return_code
def _raw_get_command_output(self, shell_id, command_id):
rq = {'env:Envelope': self._get_soap_header(
resource_uri='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd',
action='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive',
shell_id=shell_id)}
stream = rq['env:Envelope'].setdefault('env:Body', {}).setdefault('rsp:Receive', {})\
.setdefault('rsp:DesiredStream', {})
stream['@CommandId'] = command_id
stream['#text'] = 'stdout stderr'
rs = self.send_message(xmltodict.unparse(rq))
root = ET.fromstring(rs)
stream_nodes = [node for node in root.findall('.//*') if node.tag.endswith('Stream')]
stdout = stderr = ''
return_code = -1
for stream_node in stream_nodes:
if stream_node.text:
if stream_node.attrib['Name'] == 'stdout':
stdout += str(base64.b64decode(stream_node.text.encode('ascii')))
elif stream_node.attrib['Name'] == 'stderr':
stderr += str(base64.b64decode(stream_node.text.encode('ascii')))
# We may need to get additional output if the stream has not finished.
# The CommandState will change from Running to Done like so:
# @example
# from...
# <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running"/>
# to...
# <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
# <rsp:ExitCode>0</rsp:ExitCode>
# </rsp:CommandState>
command_done = len([node for node in root.findall('.//*') if node.get('State', '').endswith('CommandState/Done')]) == 1
if command_done:
return_code = int(next(node for node in root.findall('.//*') if node.tag.endswith('ExitCode')).text)
return stdout, stderr, return_code, command_done

View File

@ -0,0 +1,6 @@
{
"endpoint": "http://windows-host:5985/wsman",
"transport": "plaintext",
"username": "username_without_domain",
"password": "password_as_plain_text"
}

View File

@ -0,0 +1,332 @@
import os
import json
import uuid
import xmltodict
from pytest import skip, fixture
from mock import patch
open_shell_request = """\
<?xml version="1.0" encoding="utf-8"?>
<env:Envelope xmlns:x="http://schemas.xmlsoap.org/ws/2004/09/transfer" xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:cfg="http://schemas.microsoft.com/wbem/wsman/1/config" xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd" xmlns:n="http://schemas.xmlsoap.org/ws/2004/09/enumeration" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:b="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing">
<env:Header>
<a:To>http://windows-host:5985/wsman</a:To>
<a:ReplyTo>
<a:Address mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
</a:ReplyTo>
<w:MaxEnvelopeSize mustUnderstand="true">153600</w:MaxEnvelopeSize>
<a:MessageID>uuid:11111111-1111-1111-1111-111111111111</a:MessageID>
<w:Locale mustUnderstand="false" xml:lang="en-US" />
<p:DataLocale mustUnderstand="false" xml:lang="en-US" />
<w:OperationTimeout>PT60S</w:OperationTimeout>
<w:ResourceURI mustUnderstand="true">http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd</w:ResourceURI>
<a:Action mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/transfer/Create</a:Action>
<w:OptionSet>
<w:Option Name="WINRS_NOPROFILE">FALSE</w:Option>
<w:Option Name="WINRS_CODEPAGE">437</w:Option>
</w:OptionSet>
</env:Header>
<env:Body>
<rsp:Shell>
<rsp:InputStreams>stdin</rsp:InputStreams>
<rsp:OutputStreams>stdout stderr</rsp:OutputStreams>
</rsp:Shell>
</env:Body>
</env:Envelope>"""
open_shell_response = """\
<?xml version="1.0" ?>
<s:Envelope xml:lang="en-US" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd" xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:x="http://schemas.xmlsoap.org/ws/2004/09/transfer">
<s:Header>
<a:Action>http://schemas.xmlsoap.org/ws/2004/09/transfer/CreateResponse</a:Action>
<a:MessageID>uuid:11111111-1111-1111-1111-111111111112</a:MessageID>
<a:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:To>
<a:RelatesTo>uuid:11111111-1111-1111-1111-111111111111</a:RelatesTo>
</s:Header>
<s:Body>
<x:ResourceCreated>
<a:Address>http://windows-host:5985/wsman</a:Address>
<a:ReferenceParameters>
<w:ResourceURI>http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd</w:ResourceURI>
<w:SelectorSet>
<w:Selector Name="ShellId">11111111-1111-1111-1111-111111111113</w:Selector>
</w:SelectorSet>
</a:ReferenceParameters>
</x:ResourceCreated>
</s:Body>
</s:Envelope>"""
close_shell_request = """\
<?xml version="1.0" encoding="utf-8"?>
<env:Envelope xmlns:x="http://schemas.xmlsoap.org/ws/2004/09/transfer" xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:cfg="http://schemas.microsoft.com/wbem/wsman/1/config" xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd" xmlns:n="http://schemas.xmlsoap.org/ws/2004/09/enumeration" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:b="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing">
<env:Header>
<a:To>http://windows-host:5985/wsman</a:To>
<a:ReplyTo>
<a:Address mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
</a:ReplyTo>
<w:MaxEnvelopeSize mustUnderstand="true">153600</w:MaxEnvelopeSize>
<a:MessageID>uuid:11111111-1111-1111-1111-111111111111</a:MessageID>
<w:Locale mustUnderstand="false" xml:lang="en-US" />
<p:DataLocale mustUnderstand="false" xml:lang="en-US" />
<w:OperationTimeout>PT60S</w:OperationTimeout>
<w:ResourceURI mustUnderstand="true">http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd</w:ResourceURI>
<a:Action mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete</a:Action>
<w:SelectorSet>
<w:Selector Name="ShellId">11111111-1111-1111-1111-111111111113</w:Selector>
</w:SelectorSet>
</env:Header>
<env:Body>
</env:Body>
</env:Envelope>"""
close_shell_response = """\
<?xml version="1.0" ?>
<s:Envelope xml:lang="en-US" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">
<s:Header>
<a:Action>http://schemas.xmlsoap.org/ws/2004/09/transfer/DeleteResponse</a:Action>
<a:MessageID>uuid:11111111-1111-1111-1111-111111111112</a:MessageID>
<a:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:To>
<a:RelatesTo>uuid:11111111-1111-1111-1111-111111111111</a:RelatesTo>
</s:Header>
<s:Body/>
</s:Envelope>
"""
run_cmd_with_args_request = """\
<?xml version="1.0" encoding="utf-8"?>
<env:Envelope xmlns:x="http://schemas.xmlsoap.org/ws/2004/09/transfer" xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:cfg="http://schemas.microsoft.com/wbem/wsman/1/config" xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd" xmlns:n="http://schemas.xmlsoap.org/ws/2004/09/enumeration" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:b="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing">
<env:Header>
<a:To>http://windows-host:5985/wsman</a:To>
<a:ReplyTo>
<a:Address mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
</a:ReplyTo>
<w:MaxEnvelopeSize mustUnderstand="true">153600</w:MaxEnvelopeSize>
<a:MessageID>uuid:11111111-1111-1111-1111-111111111111</a:MessageID>
<w:Locale mustUnderstand="false" xml:lang="en-US" />
<p:DataLocale mustUnderstand="false" xml:lang="en-US" />
<w:OperationTimeout>PT60S</w:OperationTimeout>
<w:ResourceURI mustUnderstand="true">http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd</w:ResourceURI>
<a:Action mustUnderstand="true">http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command</a:Action>
<w:SelectorSet>
<w:Selector Name="ShellId">11111111-1111-1111-1111-111111111113</w:Selector>
</w:SelectorSet>
<w:OptionSet>
<w:Option Name="WINRS_CONSOLEMODE_STDIN">TRUE</w:Option>
<w:Option Name="WINRS_SKIP_CMD_SHELL">FALSE</w:Option>
</w:OptionSet>
</env:Header>
<env:Body>
<rsp:CommandLine>
<rsp:Command>ipconfig</rsp:Command>
<rsp:Arguments>/all</rsp:Arguments>
</rsp:CommandLine>
</env:Body>
</env:Envelope>"""
run_cmd_wo_args_request = """\
<?xml version="1.0" encoding="utf-8"?>
<env:Envelope xmlns:x="http://schemas.xmlsoap.org/ws/2004/09/transfer" xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:cfg="http://schemas.microsoft.com/wbem/wsman/1/config" xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd" xmlns:n="http://schemas.xmlsoap.org/ws/2004/09/enumeration" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:b="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing">
<env:Header>
<a:To>http://windows-host:5985/wsman</a:To>
<a:ReplyTo>
<a:Address mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
</a:ReplyTo>
<w:MaxEnvelopeSize mustUnderstand="true">153600</w:MaxEnvelopeSize>
<a:MessageID>uuid:11111111-1111-1111-1111-111111111111</a:MessageID>
<w:Locale mustUnderstand="false" xml:lang="en-US" />
<p:DataLocale mustUnderstand="false" xml:lang="en-US" />
<w:OperationTimeout>PT60S</w:OperationTimeout>
<w:ResourceURI mustUnderstand="true">http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd</w:ResourceURI>
<a:Action mustUnderstand="true">http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command</a:Action>
<w:SelectorSet>
<w:Selector Name="ShellId">11111111-1111-1111-1111-111111111113</w:Selector>
</w:SelectorSet>
<w:OptionSet>
<w:Option Name="WINRS_CONSOLEMODE_STDIN">TRUE</w:Option>
<w:Option Name="WINRS_SKIP_CMD_SHELL">FALSE</w:Option>
</w:OptionSet>
</env:Header>
<env:Body>
<rsp:CommandLine>
<rsp:Command>hostname</rsp:Command>
</rsp:CommandLine>
</env:Body>
</env:Envelope>"""
run_cmd_response = """\
<?xml version="1.0" ?>
<s:Envelope xml:lang="en-US" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd" xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:x="http://schemas.xmlsoap.org/ws/2004/09/transfer">
<s:Header>
<a:Action>http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandResponse</a:Action>
<a:MessageID>uuid:11111111-1111-1111-1111-111111111112</a:MessageID>
<a:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:To>
<a:RelatesTo>uuid:11111111-1111-1111-1111-111111111111</a:RelatesTo>
</s:Header>
<s:Body>
<rsp:CommandResponse>
<rsp:CommandId>11111111-1111-1111-1111-111111111114</rsp:CommandId>
</rsp:CommandResponse>
</s:Body>
</s:Envelope>"""
cleanup_cmd_request = """\
<?xml version="1.0" encoding="utf-8"?>
<env:Envelope xmlns:x="http://schemas.xmlsoap.org/ws/2004/09/transfer" xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:cfg="http://schemas.microsoft.com/wbem/wsman/1/config" xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd" xmlns:n="http://schemas.xmlsoap.org/ws/2004/09/enumeration" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:b="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing">
<env:Header>
<a:To>http://windows-host:5985/wsman</a:To>
<a:ReplyTo>
<a:Address mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
</a:ReplyTo>
<w:MaxEnvelopeSize mustUnderstand="true">153600</w:MaxEnvelopeSize>
<a:MessageID>uuid:11111111-1111-1111-1111-111111111111</a:MessageID>
<w:Locale mustUnderstand="false" xml:lang="en-US" />
<p:DataLocale mustUnderstand="false" xml:lang="en-US" />
<w:OperationTimeout>PT60S</w:OperationTimeout>
<w:ResourceURI mustUnderstand="true">http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd</w:ResourceURI>
<a:Action mustUnderstand="true">http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Signal</a:Action>
<w:SelectorSet>
<w:Selector Name="ShellId">11111111-1111-1111-1111-111111111113</w:Selector>
</w:SelectorSet>
</env:Header>
<env:Body>
<rsp:Signal CommandId="11111111-1111-1111-1111-111111111114">
<rsp:Code>http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate</rsp:Code>
</rsp:Signal>
</env:Body>
</env:Envelope>"""
cleanup_cmd_response = """\
<?xml version="1.0" ?>
<s:Envelope xml:lang="en-US" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd" xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:x="http://schemas.xmlsoap.org/ws/2004/09/transfer">
<s:Header>
<a:Action>http://schemas.microsoft.com/wbem/wsman/1/windows/shell/SignalResponse</a:Action>
<a:MessageID>uuid:11111111-1111-1111-1111-111111111112</a:MessageID>
<a:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:To>
<a:RelatesTo>uuid:11111111-1111-1111-1111-111111111111</a:RelatesTo>
</s:Header>
<s:Body>
<rsp:SignalResponse/>
</s:Body>
</s:Envelope>"""
get_cmd_output_request = """\
<?xml version="1.0" encoding="utf-8"?>
<env:Envelope xmlns:x="http://schemas.xmlsoap.org/ws/2004/09/transfer" xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:cfg="http://schemas.microsoft.com/wbem/wsman/1/config" xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd" xmlns:n="http://schemas.xmlsoap.org/ws/2004/09/enumeration" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:b="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing">
<env:Header>
<a:To>http://windows-host:5985/wsman</a:To>
<a:ReplyTo>
<a:Address mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
</a:ReplyTo>
<w:MaxEnvelopeSize mustUnderstand="true">153600</w:MaxEnvelopeSize>
<a:MessageID>uuid:11111111-1111-1111-1111-111111111111</a:MessageID>
<w:Locale mustUnderstand="false" xml:lang="en-US" />
<p:DataLocale mustUnderstand="false" xml:lang="en-US" />
<w:OperationTimeout>PT60S</w:OperationTimeout>
<w:ResourceURI mustUnderstand="true">http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd</w:ResourceURI>
<a:Action mustUnderstand="true">http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive</a:Action>
<w:SelectorSet>
<w:Selector Name="ShellId">11111111-1111-1111-1111-111111111113</w:Selector>
</w:SelectorSet>
</env:Header>
<env:Body>
<rsp:Receive>
<rsp:DesiredStream CommandId="11111111-1111-1111-1111-111111111114">stdout stderr</rsp:DesiredStream>
</rsp:Receive>
</env:Body>
</env:Envelope>"""
get_cmd_output_response = """\
<?xml version="1.0" ?>
<s:Envelope xml:lang="en-US" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd" xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd">
<s:Header>
<a:Action>http://schemas.microsoft.com/wbem/wsman/1/windows/shell/ReceiveResponse</a:Action>
<a:MessageID>uuid:11111111-1111-1111-1111-111111111112</a:MessageID>
<a:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:To>
<a:RelatesTo>uuid:11111111-1111-1111-1111-111111111111</a:RelatesTo>
</s:Header>
<s:Body>
<rsp:ReceiveResponse>
<rsp:Stream CommandId="11111111-1111-1111-1111-111111111114" Name="stdout">DQpXaW5kb3dzIElQIENvbmZpZ3VyYXRpb24NCg0K</rsp:Stream>
<rsp:Stream CommandId="11111111-1111-1111-1111-111111111114" Name="stdout">ICAgSG9zdCBOYW1lIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiAuIDogV0lORE9XUy1IT1NUCiAgIFByaW1hcnkgRG5zIFN1ZmZpeCAgLiAuIC4gLiAuIC4gLiA6IAogICBOb2RlIFR5cGUgLiAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBIeWJyaWQKICAgSVAgUm91dGluZyBFbmFibGVkLiAuIC4gLiAuIC4gLiAuIDogTm8KICAgV0lOUyBQcm94eSBFbmFibGVkLiAuIC4gLiAuIC4gLiAuIDogTm8KCkV0aGVybmV0IGFkYXB0ZXIgTG9jYWwgQXJlYSBDb25uZWN0aW9uOgoKICAgQ29ubmVjdGlvbi1zcGVjaWZpYyBETlMgU3VmZml4ICAuIDogCiAgIERlc2NyaXB0aW9uIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IEludGVsKFIpIDgyNTY3Vi0yIEdpZ2FiaXQgTmV0d29yayBDb25uZWN0aW9uCiAgIFBoeXNpY2FsIEFkZHJlc3MuIC4gLiAuIC4gLiAuIC4gLiA6IEY4LTBGLTQxLTE2LTg4LUU4CiAgIERIQ1AgRW5hYmxlZC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IE5vCiAgIEF1dG9jb25maWd1cmF0aW9uIEVuYWJsZWQgLiAuIC4gLiA6IFllcwogICBMaW5rLWxvY2FsIElQdjYgQWRkcmVzcyAuIC4gLiAuIC4gOiBmZTgwOjphOTkwOjM1ZTM6YTZhYjpmYzE1JTEwKFByZWZlcnJlZCkgCiAgIElQdjQgQWRkcmVzcy4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IDE3My4xODUuMTUzLjkzKFByZWZlcnJlZCkgCiAgIFN1Ym5ldCBNYXNrIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IDI1NS4yNTUuMjU1LjI0OAogICBEZWZhdWx0IEdhdGV3YXkgLiAuIC4gLiAuIC4gLiAuIC4gOiAxNzMuMTg1LjE1My44OQogICBESENQdjYgSUFJRCAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiAyNTExMzc4NTcKICAgREhDUHY2IENsaWVudCBEVUlELiAuIC4gLiAuIC4gLiAuIDogMDAtMDEtMDAtMDEtMTYtM0ItM0YtQzItRjgtMEYtNDEtMTYtODgtRTgKICAgRE5TIFNlcnZlcnMgLiAuIC4gLiAuIC4gLiAuIC4gLiAuIDogMjA3LjkxLjUuMzIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjA4LjY3LjIyMi4yMjIKICAgTmV0QklPUyBvdmVyIFRjcGlwLiAuIC4gLiAuIC4gLiAuIDogRW5hYmxlZAoKRXRoZXJuZXQgYWRhcHRlciBMb2NhbCBBcmVhIENvbm5lY3Rpb24qIDk6CgogICBNZWRpYSBTdGF0ZSAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBNZWRpYSBkaXNjb25uZWN0ZWQKICAgQ29ubmVjdGlvbi1zcGVjaWZpYyBETlMgU3VmZml4ICAuIDogCiAgIERlc2NyaXB0aW9uIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IEp1bmlwZXIgTmV0d29yayBDb25uZWN0IFZpcnR1YWwgQWRhcHRlcgogICBQaHlzaWNhbCBBZGRyZXNzLiAuIC4gLiAuIC4gLiAuIC4gOiAwMC1GRi1BMC04My00OC0wNAogICBESENQIEVuYWJsZWQuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBZZXMKICAgQXV0b2NvbmZpZ3VyYXRpb24gRW5hYmxlZCAuIC4gLiAuIDogWWVzCgpUdW5uZWwgYWRhcHRlciBpc2F0YXAue0FBNDI2QjM3LTM2OTUtNEVCOC05OTBGLTRDRkFDODQ1RkQxN306CgogICBNZWRpYSBTdGF0ZSAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBNZWRpYSBkaXNjb25uZWN0ZWQKICAgQ29ubmVjdGlvbi1zcGVjaWZpYyBETlMgU3VmZml4ICAuIDogCiAgIERlc2NyaXB0aW9uIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IE1pY3Jvc29mdCBJU0FUQVAgQWRhcHRlcgogICBQaHlzaWNhbCBBZGRyZXNzLiAuIC4gLiAuIC4gLiAuIC4gOiAwMC0wMC0wMC0wMC0wMC0wMC0wMC1FMAogICBESENQIEVuYWJsZWQuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBObwogICBBdXRvY29uZmlndXJhdGlvbiBFbmFibGVkIC4gLiAuIC4gOiBZZXMKClR1bm5lbCBhZGFwdGVyIFRlcmVkbyBUdW5uZWxpbmcgUHNldWRvLUludGVyZmFjZToKCiAgIENvbm5lY3Rpb24tc3BlY2lmaWMgRE5TIFN1ZmZpeCAgLiA6IAogICBEZXNjcmlwdGlvbiAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBUZXJlZG8gVHVubmVsaW5nIFBzZXVkby1JbnRlcmZhY2UKICAgUGh5c2ljYWwgQWRkcmVzcy4gLiAuIC4gLiAuIC4gLiAuIDogMDAtMDAtMDAtMDAtMDAtMDAtMDAtRTAKICAgREhDUCBFbmFibGVkLiAuIC4gLiAuIC4gLiAuIC4gLiAuIDogTm8KICAgQXV0b2NvbmZpZ3VyYXRpb24gRW5hYmxlZCAuIC4gLiAuIDogWWVzCiAgIElQdjYgQWRkcmVzcy4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IDIwMDE6MDo5ZDM4Ojk1M2M6MmNlZjo3ZmM6NTI0Njo2NmEyKFByZWZlcnJlZCkgCiAgIExpbmstbG9jYWwgSVB2NiBBZGRyZXNzIC4gLiAuIC4gLiA6IGZlODA6OjJjZWY6N2ZjOjUyNDY6NjZhMiUxMyhQcmVmZXJyZWQpIAogICBEZWZhdWx0IEdhdGV3YXkgLiAuIC4gLiAuIC4gLiAuIC4gOiAKICAgTmV0QklPUyBvdmVyIFRjcGlwLiAuIC4gLiAuIC4gLiAuIDogRGlzYWJsZWQKClR1bm5lbCBhZGFwdGVyIDZUTzQgQWRhcHRlcjoKCiAgIENvbm5lY3Rpb24tc3BlY2lmaWMgRE5TIFN1ZmZpeCAgLiA6IAogICBEZXNjcmlwdGlvbiAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBNaWNyb3NvZnQgNnRvNCBBZGFwdGVyICMyCiAgIFBoeXNpY2FsIEFkZHJlc3MuIC4gLiAuIC4gLiAuIC4gLiA6IDAwLTAwLTAwLTAwLTAwLTAwLTAwLUUwCiAgIERIQ1AgRW5hYmxlZC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IE5vCiAgIEF1dG9jb25maWd1cmF0aW9uIEVuYWJsZWQgLiAuIC4gLiA6IFllcwogICBJUHY2IEFkZHJlc3MuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiAyMDAyOmFkYjk6OTk1ZDo6YWRiOTo5OTVkKFByZWZlcnJlZCkgCiAgIERlZmF1bHQgR2F0ZXdheSAuIC4gLiAuIC4gLiAuIC4gLiA6IDIwMDI6YzA1ODo2MzAxOjpjMDU4OjYzMDEKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjAwMjpjMDU4OjYzMDE6OjEKICAgRE5TIFNlcnZlcnMgLiAuIC4gLiAuIC4gLiAuIC4gLiAuIDogMjA3LjkxLjUuMzIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjA4LjY3LjIyMi4yMjIKICAgTmV0QklPUyBvdmVyIFRjcGlwLiAuIC4gLiAuIC4gLiAuIDogRGlzYWJsZWQKClR1bm5lbCBhZGFwdGVyIGlzYXRhcC57QkExNjBGQzUtNzAyOC00QjFGLUEwNEItMUFDODAyQjBGRjVBfToKCiAgIE1lZGlhIFN0YXRlIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IE1lZGlhIGRpc2Nvbm5lY3RlZAogICBDb25uZWN0aW9uLXNwZWNpZmljIEROUyBTdWZmaXggIC4gOiAKICAgRGVzY3JpcHRpb24gLiAuIC4gLiAuIC4gLiAuIC4gLiAuIDogTWljcm9zb2Z0IElTQVRBUCBBZGFwdGVyICMyCiAgIFBoeXNpY2FsIEFkZHJlc3MuIC4gLiAuIC4gLiAuIC4gLiA6IDAwLTAwLTAwLTAwLTAwLTAwLTAwLUUwCiAgIERIQ1AgRW5hYmxlZC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IE5vCiAgIEF1dG9jb25maWd1cmF0aW9uIEVuYWJsZWQgLiAuIC4gLiA6IFllcwo=</rsp:Stream>
<rsp:Stream CommandId="11111111-1111-1111-1111-111111111114" End="true" Name="stdout"/>
<rsp:Stream CommandId="11111111-1111-1111-1111-111111111114" End="true" Name="stderr"/>
<rsp:CommandState CommandId="11111111-1111-1111-1111-111111111114" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
<rsp:ExitCode>0</rsp:ExitCode>
</rsp:CommandState>
</rsp:ReceiveResponse>
</s:Body>
</s:Envelope>"""
def sort_dict(ordered_dict):
items = sorted(ordered_dict.items(), key=lambda x: x[0])
ordered_dict.clear()
for key, value in items:
if isinstance(value, dict):
sort_dict(value)
ordered_dict[key] = value
def xml_str_compare(first, second):
first_dict = xmltodict.parse(first)
second_dict = xmltodict.parse(second)
sort_dict(first_dict)
sort_dict(second_dict)
return first_dict == second_dict
class TransportStub(object):
def send_message(self, message):
if xml_str_compare(message, open_shell_request):
return open_shell_response
elif xml_str_compare(message, close_shell_request):
return close_shell_response
elif xml_str_compare(
message, run_cmd_with_args_request) or xml_str_compare(
message, run_cmd_wo_args_request):
return run_cmd_response
elif xml_str_compare(message, cleanup_cmd_request):
return cleanup_cmd_response
elif xml_str_compare(message, get_cmd_output_request):
return get_cmd_output_response
else:
raise Exception('Message was not expected')
@fixture(scope='module')
def protocol_fake(request):
uuid4_patcher = patch('uuid.uuid4')
uuid4_mock = uuid4_patcher.start()
uuid4_mock.return_value = uuid.UUID(
'11111111-1111-1111-1111-111111111111')
from winrm.protocol import Protocol
protocol_fake = Protocol(
endpoint='http://windows-host:5985/wsman',
transport='plaintext',
username='john.smith',
password='secret')
protocol_fake.transport = TransportStub()
def uuid4_patch_stop():
uuid4_patcher.stop()
request.addfinalizer(uuid4_patch_stop)
return protocol_fake
@fixture(scope='module')
def protocol_real():
config_path = os.path.join(os.path.dirname(__file__), 'config.json')
if os.path.isfile(config_path):
# TODO consider replace json with yaml for integration test settings
# TODO json does not support comments
settings = json.load(open(config_path))
from winrm.protocol import Protocol
protocol = Protocol(**settings)
return protocol
else:
skip('config.json was not found. Integration tests will be skipped')

View File

@ -0,0 +1,71 @@
import re
import pytest
xfail = pytest.mark.xfail
def test_open_shell_and_close_shell(protocol_real):
shell_id = protocol_real.open_shell()
assert re.match('^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$', shell_id)
protocol_real.close_shell(shell_id)
def test_run_command_with_arguments_and_cleanup_command(protocol_real):
shell_id = protocol_real.open_shell()
command_id = protocol_real.run_command(shell_id, 'ipconfig', ['/all'])
assert re.match('^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$', command_id)
protocol_real.cleanup_command(shell_id, command_id)
protocol_real.close_shell(shell_id)
def test_run_command_without_arguments_and_cleanup_command(protocol_real):
shell_id = protocol_real.open_shell()
command_id = protocol_real.run_command(shell_id, 'hostname')
assert re.match('^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$', command_id)
protocol_real.cleanup_command(shell_id, command_id)
protocol_real.close_shell(shell_id)
def test_get_command_output(protocol_real):
shell_id = protocol_real.open_shell()
command_id = protocol_real.run_command(shell_id, 'ipconfig', ['/all'])
std_out, std_err, status_code = protocol_real.get_command_output(
shell_id, command_id)
assert status_code == 0
assert 'Windows IP Configuration' in std_out
assert len(std_err) == 0
protocol_real.cleanup_command(shell_id, command_id)
protocol_real.close_shell(shell_id)
def test_run_command_taking_more_than_60_seconds(protocol_real):
shell_id = protocol_real.open_shell()
command_id = protocol_real.run_command(shell_id, 'PowerShell -Command Start-Sleep -s 75')
assert re.match('^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$', command_id)
std_out, std_err, status_code = protocol_real.get_command_output(
shell_id, command_id)
assert status_code == 0
assert len(std_err) == 0
protocol_real.cleanup_command(shell_id, command_id)
protocol_real.close_shell(shell_id)
@xfail()
def test_set_timeout(protocol_real):
raise NotImplementedError()
@xfail()
def test_set_max_env_size(protocol_real):
raise NotImplementedError()
@xfail()
def test_set_locale(protocol_real):
raise NotImplementedError()

View File

@ -0,0 +1,8 @@
import pytest
from winrm import Session
xfail = pytest.mark.xfail
@xfail()
def test_run_cmd():
raise NotImplementedError()

View File

@ -0,0 +1,35 @@
def test_open_shell_and_close_shell(protocol_fake):
shell_id = protocol_fake.open_shell()
assert shell_id == '11111111-1111-1111-1111-111111111113'
protocol_fake.close_shell(shell_id)
def test_run_command_with_arguments_and_cleanup_command(protocol_fake):
shell_id = protocol_fake.open_shell()
command_id = protocol_fake.run_command(shell_id, 'ipconfig', ['/all'])
assert command_id == '11111111-1111-1111-1111-111111111114'
protocol_fake.cleanup_command(shell_id, command_id)
protocol_fake.close_shell(shell_id)
def test_run_command_without_arguments_and_cleanup_command(protocol_fake):
shell_id = protocol_fake.open_shell()
command_id = protocol_fake.run_command(shell_id, 'hostname')
assert command_id == '11111111-1111-1111-1111-111111111114'
protocol_fake.cleanup_command(shell_id, command_id)
protocol_fake.close_shell(shell_id)
def test_get_command_output(protocol_fake):
shell_id = protocol_fake.open_shell()
command_id = protocol_fake.run_command(shell_id, 'ipconfig', ['/all'])
std_out, std_err, status_code = protocol_fake.get_command_output(shell_id, command_id)
assert status_code == 0
assert 'Windows IP Configuration' in std_out
assert len(std_err) == 0
protocol_fake.cleanup_command(shell_id, command_id)
protocol_fake.close_shell(shell_id)

View File

@ -0,0 +1,13 @@
from winrm import Session
def test_run_cmd(protocol_fake):
#TODO this test should cover __init__ method
s = Session('windows-host', auth=('john.smith', 'secret'))
s.protocol = protocol_fake
r = s.run_cmd('ipconfig', ['/all'])
assert r.status_code == 0
assert 'Windows IP Configuration' in r.std_out
assert len(r.std_err) == 0

View File

@ -0,0 +1,229 @@
import sys
import base64
from winrm.exceptions import WinRMTransportError
HAVE_KERBEROS=False
try:
import kerberos
HAVE_KERBEROS=True
except ImportError:
pass
is_py2 = sys.version[0] == '2'
if is_py2:
from urllib2 import Request, URLError, HTTPError, HTTPBasicAuthHandler, HTTPPasswordMgrWithDefaultRealm, HTTPSHandler
from urllib2 import urlopen, build_opener, install_opener
from urlparse import urlparse
from httplib import HTTPSConnection
else:
from urllib.request import Request, URLError, HTTPError, HTTPBasicAuthHandler, HTTPPasswordMgrWithDefaultRealm, HTTPSHandler
from urllib.request import urlopen, build_opener, install_opener
from urllib.parse import urlparse
from http.client import HTTPSConnection
class HttpTransport(object):
def __init__(self, endpoint, username, password):
self.endpoint = endpoint
self.username = username
self.password = password
self.user_agent = 'Python WinRM client'
self.timeout = 3600 # Set this to an unreasonable amount for now because WinRM has timeouts
def basic_auth_only(self):
#here we should remove handler for any authentication handlers other than basic
# but maybe leave original credentials
# auths = @httpcli.www_auth.instance_variable_get('@authenticator')
# auths.delete_if {|i| i.scheme !~ /basic/i}
# drop all variables in auths if they not contains "basic" as insensitive.
pass
def no_sspi_auth(self):
# here we should remove handler for Negotiate/NTLM negotiation
# but maybe leave original credentials
pass
class HttpPlaintext(HttpTransport):
def __init__(self, endpoint, username='', password='', disable_sspi=True, basic_auth_only=True):
super(HttpPlaintext, self).__init__(endpoint, username, password)
if disable_sspi:
self.no_sspi_auth()
if basic_auth_only:
self.basic_auth_only()
self._headers = {'Content-Type' : 'application/soap+xml;charset=UTF-8',
'User-Agent' : 'Python WinRM client'}
def _setup_opener(self):
password_manager = HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(None, self.endpoint, self.username, self.password)
auth_manager = HTTPBasicAuthHandler(password_manager)
opener = build_opener(auth_manager)
install_opener(opener)
def send_message(self, message):
headers = self._headers.copy()
headers['Content-Length'] = len(message)
self._setup_opener()
request = Request(self.endpoint, data=message, headers=headers)
try:
response = urlopen(request, timeout=self.timeout)
# Version 1.1 of WinRM adds the namespaces in the document instead of the envelope so we have to
# add them ourselves here. This should have no affect version 2.
response_text = response.read()
return response_text
#doc = ElementTree.fromstring(response.read())
#Ruby
#doc = Nokogiri::XML(resp.http_body.content)
#doc.collect_namespaces.each_pair do |k,v|
# doc.root.add_namespace((k.split(/:/).last),v) unless doc.namespaces.has_key?(k)
#end
#return doc
#return doc
except HTTPError as ex:
response_text = ex.read()
# Per http://msdn.microsoft.com/en-us/library/cc251676.aspx rule 3,
# should handle this 500 error and retry receiving command output.
if 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive' in message and 'Code="2150858793"' in response_text:
return response_text
error_message = 'Bad HTTP response returned from server. Code {0}'.format(ex.code)
if ex.msg:
error_message += ', {0}'.format(ex.msg)
raise WinRMTransportError(error_message)
except URLError as ex:
raise WinRMTransportError(ex.reason)
class HTTPSClientAuthHandler(HTTPSHandler):
def __init__(self, cert, key):
HTTPSHandler.__init__(self)
self.cert = cert
self.key = key
def https_open(self, req):
return self.do_open(self.getConnection, req)
def getConnection(self, host, timeout=300):
return HTTPSConnection(host, key_file=self.key, cert_file=self.cert)
class HttpSSL(HttpPlaintext):
"""Uses SSL to secure the transport"""
def __init__(self, endpoint, username, password, ca_trust_path=None, disable_sspi=True, basic_auth_only=True,
cert_pem=None, cert_key_pem=None):
super(HttpSSL, self).__init__(endpoint, username, password)
self._cert_pem = cert_pem
self._cert_key_pem = cert_key_pem
#Ruby
#@httpcli.set_auth(endpoint, user, pass)
#@httpcli.ssl_config.set_trust_ca(ca_trust_path) unless ca_trust_path.nil?
if disable_sspi:
self.no_sspi_auth()
if basic_auth_only:
self.basic_auth_only()
if self._cert_pem:
self._headers['Authorization'] = "http://schemas.dmtf.org/wbem/wsman/1/wsman/secprofile/https/mutual"
def _setup_opener(self):
if not self._cert_pem:
super(HttpSSL, self)._setup_opener()
else:
opener = build_opener(HTTPSClientAuthHandler(self._cert_pem, self._cert_key_pem))
install_opener(opener)
class KerberosTicket:
"""
Implementation based on http://ncoghlan_devs-python-notes.readthedocs.org/en/latest/python_kerberos.html
"""
def __init__(self, service):
ignored_code, krb_context = kerberos.authGSSClientInit(service)
kerberos.authGSSClientStep(krb_context, '')
# TODO authGSSClientStep may raise following error:
#GSSError: (('Unspecified GSS failure. Minor code may provide more information', 851968), ("Credentials cache file '/tmp/krb5cc_1000' not found", -1765328189))
self._krb_context = krb_context
gss_response = kerberos.authGSSClientResponse(krb_context)
self.auth_header = 'Negotiate {0}'.format(gss_response)
def verify_response(self, auth_header):
# Handle comma-separated lists of authentication fields
for field in auth_header.split(','):
kind, ignored_space, details = field.strip().partition(' ')
if kind.lower() == 'negotiate':
auth_details = details.strip()
break
else:
raise ValueError('Negotiate not found in {0}'.format(auth_header))
# Finish the Kerberos handshake
krb_context = self._krb_context
if krb_context is None:
raise RuntimeError('Ticket already used for verification')
self._krb_context = None
kerberos.authGSSClientStep(krb_context, auth_details)
#print('User {0} authenticated successfully using Kerberos authentication'.format(kerberos.authGSSClientUserName(krb_context)))
kerberos.authGSSClientClean(krb_context)
class HttpKerberos(HttpTransport):
def __init__(self, endpoint, realm=None, service='HTTP', keytab=None):
"""
Uses Kerberos/GSS-API to authenticate and encrypt messages
@param string endpoint: the WinRM webservice endpoint
@param string realm: the Kerberos realm we are authenticating to
@param string service: the service name, default is HTTP
@param string keytab: the path to a keytab file if you are using one
"""
if not HAVE_KERBEROS:
raise WinRMTransportError('kerberos is not installed')
super(HttpKerberos, self).__init__(endpoint, None, None)
self.krb_service = '{0}@{1}'.format(service, urlparse(endpoint).hostname)
#self.krb_ticket = KerberosTicket(krb_service)
def set_auth(self, username, password):
raise NotImplementedError
def send_message(self, message):
# TODO current implementation does negotiation on each HTTP request which is not efficient
# TODO support kerberos session with message encryption
krb_ticket = KerberosTicket(self.krb_service)
headers = {'Authorization': krb_ticket.auth_header,
'Connection': 'Keep-Alive',
'Content-Type': 'application/soap+xml;charset=UTF-8',
'User-Agent': 'Python WinRM client'}
request = Request(self.endpoint, data=message, headers=headers)
try:
response = urlopen(request, timeout=self.timeout)
krb_ticket.verify_response(response.headers['WWW-Authenticate'])
response_text = response.read()
return response_text
except HTTPError as ex:
response_text = ex.read()
# Per http://msdn.microsoft.com/en-us/library/cc251676.aspx rule 3,
# should handle this 500 error and retry receiving command output.
if 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive' in message and 'Code="2150858793"' in response_text:
return response_text
#if ex.code == 401 and ex.headers['WWW-Authenticate'] == 'Negotiate, Basic realm="WSMAN"':
error_message = 'Kerberos-based authentication was failed. Code {0}'.format(ex.code)
if ex.msg:
error_message += ', {0}'.format(ex.msg)
raise WinRMTransportError(error_message)
except URLError as ex:
raise WinRMTransportError(ex.reason)
def _winrm_encrypt(self, string):
"""
@returns the encrypted request string
@rtype string
"""
raise NotImplementedError
def _winrm_decrypt(self, string):
raise NotImplementedError

View File

@ -0,0 +1,359 @@
#!/usr/bin/env python
"Makes working with XML feel like you are working with JSON"
from xml.parsers import expat
from xml.sax.saxutils import XMLGenerator
from xml.sax.xmlreader import AttributesImpl
try: # pragma no cover
from cStringIO import StringIO
except ImportError: # pragma no cover
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
try: # pragma no cover
from collections import OrderedDict
except ImportError: # pragma no cover
try:
from ordereddict import OrderedDict
except ImportError:
OrderedDict = dict
try: # pragma no cover
_basestring = basestring
except NameError: # pragma no cover
_basestring = str
try: # pragma no cover
_unicode = unicode
except NameError: # pragma no cover
_unicode = str
__author__ = 'Martin Blech'
__version__ = '0.9.0'
__license__ = 'MIT'
class ParsingInterrupted(Exception):
pass
class _DictSAXHandler(object):
def __init__(self,
item_depth=0,
item_callback=lambda *args: True,
xml_attribs=True,
attr_prefix='@',
cdata_key='#text',
force_cdata=False,
cdata_separator='',
postprocessor=None,
dict_constructor=OrderedDict,
strip_whitespace=True,
namespace_separator=':',
namespaces=None):
self.path = []
self.stack = []
self.data = None
self.item = None
self.item_depth = item_depth
self.xml_attribs = xml_attribs
self.item_callback = item_callback
self.attr_prefix = attr_prefix
self.cdata_key = cdata_key
self.force_cdata = force_cdata
self.cdata_separator = cdata_separator
self.postprocessor = postprocessor
self.dict_constructor = dict_constructor
self.strip_whitespace = strip_whitespace
self.namespace_separator = namespace_separator
self.namespaces = namespaces
def _build_name(self, full_name):
if not self.namespaces:
return full_name
i = full_name.rfind(self.namespace_separator)
if i == -1:
return full_name
namespace, name = full_name[:i], full_name[i+1:]
short_namespace = self.namespaces.get(namespace, namespace)
if not short_namespace:
return name
else:
return self.namespace_separator.join((short_namespace, name))
def _attrs_to_dict(self, attrs):
if isinstance(attrs, dict):
return attrs
return self.dict_constructor(zip(attrs[0::2], attrs[1::2]))
def startElement(self, full_name, attrs):
name = self._build_name(full_name)
attrs = self._attrs_to_dict(attrs)
self.path.append((name, attrs or None))
if len(self.path) > self.item_depth:
self.stack.append((self.item, self.data))
if self.xml_attribs:
attrs = self.dict_constructor(
(self.attr_prefix+key, value)
for (key, value) in attrs.items())
else:
attrs = None
self.item = attrs or None
self.data = None
def endElement(self, full_name):
name = self._build_name(full_name)
if len(self.path) == self.item_depth:
item = self.item
if item is None:
item = self.data
should_continue = self.item_callback(self.path, item)
if not should_continue:
raise ParsingInterrupted()
if len(self.stack):
item, data = self.item, self.data
self.item, self.data = self.stack.pop()
if self.strip_whitespace and data is not None:
data = data.strip() or None
if data and self.force_cdata and item is None:
item = self.dict_constructor()
if item is not None:
if data:
self.push_data(item, self.cdata_key, data)
self.item = self.push_data(self.item, name, item)
else:
self.item = self.push_data(self.item, name, data)
else:
self.item = self.data = None
self.path.pop()
def characters(self, data):
if not self.data:
self.data = data
else:
self.data += self.cdata_separator + data
def push_data(self, item, key, data):
if self.postprocessor is not None:
result = self.postprocessor(self.path, key, data)
if result is None:
return item
key, data = result
if item is None:
item = self.dict_constructor()
try:
value = item[key]
if isinstance(value, list):
value.append(data)
else:
item[key] = [value, data]
except KeyError:
item[key] = data
return item
def parse(xml_input, encoding=None, expat=expat, process_namespaces=False,
namespace_separator=':', **kwargs):
"""Parse the given XML input and convert it into a dictionary.
`xml_input` can either be a `string` or a file-like object.
If `xml_attribs` is `True`, element attributes are put in the dictionary
among regular child elements, using `@` as a prefix to avoid collisions. If
set to `False`, they are just ignored.
Simple example::
>>> import xmltodict
>>> doc = xmltodict.parse(\"\"\"
... <a prop="x">
... <b>1</b>
... <b>2</b>
... </a>
... \"\"\")
>>> doc['a']['@prop']
u'x'
>>> doc['a']['b']
[u'1', u'2']
If `item_depth` is `0`, the function returns a dictionary for the root
element (default behavior). Otherwise, it calls `item_callback` every time
an item at the specified depth is found and returns `None` in the end
(streaming mode).
The callback function receives two parameters: the `path` from the document
root to the item (name-attribs pairs), and the `item` (dict). If the
callback's return value is false-ish, parsing will be stopped with the
:class:`ParsingInterrupted` exception.
Streaming example::
>>> def handle(path, item):
... print 'path:%s item:%s' % (path, item)
... return True
...
>>> xmltodict.parse(\"\"\"
... <a prop="x">
... <b>1</b>
... <b>2</b>
... </a>\"\"\", item_depth=2, item_callback=handle)
path:[(u'a', {u'prop': u'x'}), (u'b', None)] item:1
path:[(u'a', {u'prop': u'x'}), (u'b', None)] item:2
The optional argument `postprocessor` is a function that takes `path`,
`key` and `value` as positional arguments and returns a new `(key, value)`
pair where both `key` and `value` may have changed. Usage example::
>>> def postprocessor(path, key, value):
... try:
... return key + ':int', int(value)
... except (ValueError, TypeError):
... return key, value
>>> xmltodict.parse('<a><b>1</b><b>2</b><b>x</b></a>',
... postprocessor=postprocessor)
OrderedDict([(u'a', OrderedDict([(u'b:int', [1, 2]), (u'b', u'x')]))])
You can pass an alternate version of `expat` (such as `defusedexpat`) by
using the `expat` parameter. E.g:
>>> import defusedexpat
>>> xmltodict.parse('<a>hello</a>', expat=defusedexpat.pyexpat)
OrderedDict([(u'a', u'hello')])
"""
handler = _DictSAXHandler(namespace_separator=namespace_separator,
**kwargs)
if isinstance(xml_input, _unicode):
if not encoding:
encoding = 'utf-8'
xml_input = xml_input.encode(encoding)
if not process_namespaces:
namespace_separator = None
parser = expat.ParserCreate(
encoding,
namespace_separator
)
try:
parser.ordered_attributes = True
except AttributeError:
# Jython's expat does not support ordered_attributes
pass
parser.StartElementHandler = handler.startElement
parser.EndElementHandler = handler.endElement
parser.CharacterDataHandler = handler.characters
parser.buffer_text = True
try:
parser.ParseFile(xml_input)
except (TypeError, AttributeError):
parser.Parse(xml_input, True)
return handler.item
def _emit(key, value, content_handler,
attr_prefix='@',
cdata_key='#text',
depth=0,
preprocessor=None,
pretty=False,
newl='\n',
indent='\t'):
if preprocessor is not None:
result = preprocessor(key, value)
if result is None:
return
key, value = result
if not isinstance(value, (list, tuple)):
value = [value]
if depth == 0 and len(value) > 1:
raise ValueError('document with multiple roots')
for v in value:
if v is None:
v = OrderedDict()
elif not isinstance(v, dict):
v = _unicode(v)
if isinstance(v, _basestring):
v = OrderedDict(((cdata_key, v),))
cdata = None
attrs = OrderedDict()
children = []
for ik, iv in v.items():
if ik == cdata_key:
cdata = iv
continue
if ik.startswith(attr_prefix):
attrs[ik[len(attr_prefix):]] = iv
continue
children.append((ik, iv))
if pretty:
content_handler.ignorableWhitespace(depth * indent)
content_handler.startElement(key, AttributesImpl(attrs))
if pretty and children:
content_handler.ignorableWhitespace(newl)
for child_key, child_value in children:
_emit(child_key, child_value, content_handler,
attr_prefix, cdata_key, depth+1, preprocessor,
pretty, newl, indent)
if cdata is not None:
content_handler.characters(cdata)
if pretty and children:
content_handler.ignorableWhitespace(depth * indent)
content_handler.endElement(key)
if pretty and depth:
content_handler.ignorableWhitespace(newl)
def unparse(input_dict, output=None, encoding='utf-8', full_document=True,
**kwargs):
"""Emit an XML document for the given `input_dict` (reverse of `parse`).
The resulting XML document is returned as a string, but if `output` (a
file-like object) is specified, it is written there instead.
Dictionary keys prefixed with `attr_prefix` (default=`'@'`) are interpreted
as XML node attributes, whereas keys equal to `cdata_key`
(default=`'#text'`) are treated as character data.
The `pretty` parameter (default=`False`) enables pretty-printing. In this
mode, lines are terminated with `'\n'` and indented with `'\t'`, but this
can be customized with the `newl` and `indent` parameters.
"""
((key, value),) = input_dict.items()
must_return = False
if output is None:
output = StringIO()
must_return = True
content_handler = XMLGenerator(output, encoding)
if full_document:
content_handler.startDocument()
_emit(key, value, content_handler, **kwargs)
if full_document:
content_handler.endDocument()
if must_return:
value = output.getvalue()
try: # pragma no cover
value = value.decode(encoding)
except AttributeError: # pragma no cover
pass
return value
if __name__ == '__main__': # pragma: no cover
import sys
import marshal
(item_depth,) = sys.argv[1:]
item_depth = int(item_depth)
def handle_item(path, item):
marshal.dump((path, item), sys.stdout)
return True
try:
root = parse(sys.stdin,
item_depth=item_depth,
item_callback=handle_item,
dict_constructor=dict)
if item_depth == 0:
handle_item([], root)
except KeyboardInterrupt:
pass

Binary file not shown.

Binary file not shown.

Binary file not shown.