diff options
37 files changed, 3942 insertions, 0 deletions
diff --git a/awx/lib/site-packages/isodate/__init__.py b/awx/lib/site-packages/isodate/__init__.py new file mode 100644 index 0000000000..091af0aeba --- /dev/null +++ b/awx/lib/site-packages/isodate/__init__.py @@ -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 diff --git a/awx/lib/site-packages/isodate/duration.py b/awx/lib/site-packages/isodate/duration.py new file mode 100644 index 0000000000..4484919773 --- /dev/null +++ b/awx/lib/site-packages/isodate/duration.py @@ -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) diff --git a/awx/lib/site-packages/isodate/isodates.py b/awx/lib/site-packages/isodate/isodates.py new file mode 100644 index 0000000000..8bafa207fc --- /dev/null +++ b/awx/lib/site-packages/isodate/isodates.py @@ -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) diff --git a/awx/lib/site-packages/isodate/isodatetime.py b/awx/lib/site-packages/isodate/isodatetime.py new file mode 100644 index 0000000000..7e4d570411 --- /dev/null +++ b/awx/lib/site-packages/isodate/isodatetime.py @@ -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) diff --git a/awx/lib/site-packages/isodate/isoduration.py b/awx/lib/site-packages/isodate/isoduration.py new file mode 100644 index 0000000000..97affdc10c --- /dev/null +++ b/awx/lib/site-packages/isodate/isoduration.py @@ -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 diff --git a/awx/lib/site-packages/isodate/isoerror.py b/awx/lib/site-packages/isodate/isoerror.py new file mode 100644 index 0000000000..edbc5aaa8f --- /dev/null +++ b/awx/lib/site-packages/isodate/isoerror.py @@ -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.''' diff --git a/awx/lib/site-packages/isodate/isostrf.py b/awx/lib/site-packages/isodate/isostrf.py new file mode 100644 index 0000000000..5f3169f4b4 --- /dev/null +++ b/awx/lib/site-packages/isodate/isostrf.py @@ -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) diff --git a/awx/lib/site-packages/isodate/isotime.py b/awx/lib/site-packages/isodate/isotime.py new file mode 100644 index 0000000000..7ded2d4878 --- /dev/null +++ b/awx/lib/site-packages/isodate/isotime.py @@ -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) diff --git a/awx/lib/site-packages/isodate/isotzinfo.py b/awx/lib/site-packages/isodate/isotzinfo.py new file mode 100644 index 0000000000..97dbe8cace --- /dev/null +++ b/awx/lib/site-packages/isodate/isotzinfo.py @@ -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) diff --git a/awx/lib/site-packages/isodate/tests/__init__.py b/awx/lib/site-packages/isodate/tests/__init__.py new file mode 100644 index 0000000000..bc1867df94 --- /dev/null +++ b/awx/lib/site-packages/isodate/tests/__init__.py @@ -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') diff --git a/awx/lib/site-packages/isodate/tests/test_date.py b/awx/lib/site-packages/isodate/tests/test_date.py new file mode 100644 index 0000000000..3a1b4a60f1 --- /dev/null +++ b/awx/lib/site-packages/isodate/tests/test_date.py @@ -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') diff --git a/awx/lib/site-packages/isodate/tests/test_datetime.py b/awx/lib/site-packages/isodate/tests/test_datetime.py new file mode 100644 index 0000000000..f6aaa51a67 --- /dev/null +++ b/awx/lib/site-packages/isodate/tests/test_datetime.py @@ -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') diff --git a/awx/lib/site-packages/isodate/tests/test_duration.py b/awx/lib/site-packages/isodate/tests/test_duration.py new file mode 100644 index 0000000000..e69ae17d24 --- /dev/null +++ b/awx/lib/site-packages/isodate/tests/test_duration.py @@ -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') diff --git a/awx/lib/site-packages/isodate/tests/test_pickle.py b/awx/lib/site-packages/isodate/tests/test_pickle.py new file mode 100644 index 0000000000..952c238336 --- /dev/null +++ b/awx/lib/site-packages/isodate/tests/test_pickle.py @@ -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') diff --git a/awx/lib/site-packages/isodate/tests/test_strf.py b/awx/lib/site-packages/isodate/tests/test_strf.py new file mode 100644 index 0000000000..c7f1c554b7 --- /dev/null +++ b/awx/lib/site-packages/isodate/tests/test_strf.py @@ -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') diff --git a/awx/lib/site-packages/isodate/tests/test_time.py b/awx/lib/site-packages/isodate/tests/test_time.py new file mode 100644 index 0000000000..cdce704c33 --- /dev/null +++ b/awx/lib/site-packages/isodate/tests/test_time.py @@ -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') diff --git a/awx/lib/site-packages/isodate/tzinfo.py b/awx/lib/site-packages/isodate/tzinfo.py new file mode 100644 index 0000000000..820c88da62 --- /dev/null +++ b/awx/lib/site-packages/isodate/tzinfo.py @@ -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. diff --git a/awx/lib/site-packages/winrm/__init__.py b/awx/lib/site-packages/winrm/__init__.py new file mode 100644 index 0000000000..e389796356 --- /dev/null +++ b/awx/lib/site-packages/winrm/__init__.py @@ -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 diff --git a/awx/lib/site-packages/winrm/exceptions.py b/awx/lib/site-packages/winrm/exceptions.py new file mode 100644 index 0000000000..2480a7bdb9 --- /dev/null +++ b/awx/lib/site-packages/winrm/exceptions.py @@ -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 diff --git a/awx/lib/site-packages/winrm/protocol.py b/awx/lib/site-packages/winrm/protocol.py new file mode 100644 index 0000000000..73c238e750 --- /dev/null +++ b/awx/lib/site-packages/winrm/protocol.py @@ -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 diff --git a/awx/lib/site-packages/winrm/tests/__init__.py b/awx/lib/site-packages/winrm/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/awx/lib/site-packages/winrm/tests/__init__.py diff --git a/awx/lib/site-packages/winrm/tests/config_example.json b/awx/lib/site-packages/winrm/tests/config_example.json new file mode 100644 index 0000000000..a7f259af6f --- /dev/null +++ b/awx/lib/site-packages/winrm/tests/config_example.json @@ -0,0 +1,6 @@ +{ + "endpoint": "http://windows-host:5985/wsman", + "transport": "plaintext", + "username": "username_without_domain", + "password": "password_as_plain_text" +}
\ No newline at end of file diff --git a/awx/lib/site-packages/winrm/tests/conftest.py b/awx/lib/site-packages/winrm/tests/conftest.py new file mode 100644 index 0000000000..07f5228163 --- /dev/null +++ b/awx/lib/site-packages/winrm/tests/conftest.py @@ -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')
\ No newline at end of file diff --git a/awx/lib/site-packages/winrm/tests/sample_script.ps1 b/awx/lib/site-packages/winrm/tests/sample_script.ps1 new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/awx/lib/site-packages/winrm/tests/sample_script.ps1 diff --git a/awx/lib/site-packages/winrm/tests/test_cmd.py b/awx/lib/site-packages/winrm/tests/test_cmd.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/awx/lib/site-packages/winrm/tests/test_cmd.py diff --git a/awx/lib/site-packages/winrm/tests/test_integration_protocol.py b/awx/lib/site-packages/winrm/tests/test_integration_protocol.py new file mode 100644 index 0000000000..b1e36edd12 --- /dev/null +++ b/awx/lib/site-packages/winrm/tests/test_integration_protocol.py @@ -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()
\ No newline at end of file diff --git a/awx/lib/site-packages/winrm/tests/test_integration_session.py b/awx/lib/site-packages/winrm/tests/test_integration_session.py new file mode 100644 index 0000000000..9cedb5a9e5 --- /dev/null +++ b/awx/lib/site-packages/winrm/tests/test_integration_session.py @@ -0,0 +1,8 @@ +import pytest +from winrm import Session +xfail = pytest.mark.xfail + + +@xfail() +def test_run_cmd(): + raise NotImplementedError()
\ No newline at end of file diff --git a/awx/lib/site-packages/winrm/tests/test_nori_type_casting.py b/awx/lib/site-packages/winrm/tests/test_nori_type_casting.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/awx/lib/site-packages/winrm/tests/test_nori_type_casting.py diff --git a/awx/lib/site-packages/winrm/tests/test_powershell.py b/awx/lib/site-packages/winrm/tests/test_powershell.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/awx/lib/site-packages/winrm/tests/test_powershell.py diff --git a/awx/lib/site-packages/winrm/tests/test_protocol.py b/awx/lib/site-packages/winrm/tests/test_protocol.py new file mode 100644 index 0000000000..a495ab237e --- /dev/null +++ b/awx/lib/site-packages/winrm/tests/test_protocol.py @@ -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)
\ No newline at end of file diff --git a/awx/lib/site-packages/winrm/tests/test_session.py b/awx/lib/site-packages/winrm/tests/test_session.py new file mode 100644 index 0000000000..093701480a --- /dev/null +++ b/awx/lib/site-packages/winrm/tests/test_session.py @@ -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
\ No newline at end of file diff --git a/awx/lib/site-packages/winrm/tests/test_wql.py b/awx/lib/site-packages/winrm/tests/test_wql.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/awx/lib/site-packages/winrm/tests/test_wql.py diff --git a/awx/lib/site-packages/winrm/transport.py b/awx/lib/site-packages/winrm/transport.py new file mode 100644 index 0000000000..5ddf8c78a2 --- /dev/null +++ b/awx/lib/site-packages/winrm/transport.py @@ -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 diff --git a/awx/lib/site-packages/xmltodict.py b/awx/lib/site-packages/xmltodict.py new file mode 100644 index 0000000000..4fdbb16a2a --- /dev/null +++ b/awx/lib/site-packages/xmltodict.py @@ -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 diff --git a/requirements/isodate-0.5.0.tar.gz b/requirements/isodate-0.5.0.tar.gz Binary files differnew file mode 100644 index 0000000000..236bec4ec3 --- /dev/null +++ b/requirements/isodate-0.5.0.tar.gz diff --git a/requirements/pywinrm-master.zip b/requirements/pywinrm-master.zip Binary files differnew file mode 100644 index 0000000000..2f5c1aef13 --- /dev/null +++ b/requirements/pywinrm-master.zip diff --git a/requirements/xmltodict-0.9.0.tar.gz b/requirements/xmltodict-0.9.0.tar.gz Binary files differnew file mode 100644 index 0000000000..190b9d8b87 --- /dev/null +++ b/requirements/xmltodict-0.9.0.tar.gz |