From 356532424e85c48874052567f137c8494bdded95 Mon Sep 17 00:00:00 2001 From: Luke Sneeringer Date: Wed, 20 Aug 2014 09:35:40 -0400 Subject: [PATCH] Vendor pywinrm (and dependencies). https://trello.com/c/FQ9AkmRV/46-install-vendored-pywinrm-for-ansible-us e --- awx/lib/site-packages/isodate/__init__.py | 55 ++ awx/lib/site-packages/isodate/duration.py | 280 ++++++++++ awx/lib/site-packages/isodate/isodates.py | 201 +++++++ awx/lib/site-packages/isodate/isodatetime.py | 61 ++ awx/lib/site-packages/isodate/isoduration.py | 145 +++++ awx/lib/site-packages/isodate/isoerror.py | 32 ++ awx/lib/site-packages/isodate/isostrf.py | 207 +++++++ awx/lib/site-packages/isodate/isotime.py | 157 ++++++ awx/lib/site-packages/isodate/isotzinfo.py | 109 ++++ .../site-packages/isodate/tests/__init__.py | 49 ++ .../site-packages/isodate/tests/test_date.py | 126 +++++ .../isodate/tests/test_datetime.py | 138 +++++ .../isodate/tests/test_duration.py | 519 ++++++++++++++++++ .../isodate/tests/test_pickle.py | 35 ++ .../site-packages/isodate/tests/test_strf.py | 130 +++++ .../site-packages/isodate/tests/test_time.py | 143 +++++ awx/lib/site-packages/isodate/tzinfo.py | 137 +++++ awx/lib/site-packages/winrm/__init__.py | 29 + awx/lib/site-packages/winrm/exceptions.py | 18 + awx/lib/site-packages/winrm/protocol.py | 318 +++++++++++ awx/lib/site-packages/winrm/tests/__init__.py | 0 .../winrm/tests/config_example.json | 6 + awx/lib/site-packages/winrm/tests/conftest.py | 332 +++++++++++ .../winrm/tests/sample_script.ps1 | 0 awx/lib/site-packages/winrm/tests/test_cmd.py | 0 .../winrm/tests/test_integration_protocol.py | 71 +++ .../winrm/tests/test_integration_session.py | 8 + .../winrm/tests/test_nori_type_casting.py | 0 .../winrm/tests/test_powershell.py | 0 .../winrm/tests/test_protocol.py | 35 ++ .../site-packages/winrm/tests/test_session.py | 13 + awx/lib/site-packages/winrm/tests/test_wql.py | 0 awx/lib/site-packages/winrm/transport.py | 229 ++++++++ awx/lib/site-packages/xmltodict.py | 359 ++++++++++++ requirements/isodate-0.5.0.tar.gz | Bin 0 -> 25917 bytes requirements/pywinrm-master.zip | Bin 0 -> 21375 bytes requirements/xmltodict-0.9.0.tar.gz | Bin 0 -> 37078 bytes 37 files changed, 3942 insertions(+) create mode 100644 awx/lib/site-packages/isodate/__init__.py create mode 100644 awx/lib/site-packages/isodate/duration.py create mode 100644 awx/lib/site-packages/isodate/isodates.py create mode 100644 awx/lib/site-packages/isodate/isodatetime.py create mode 100644 awx/lib/site-packages/isodate/isoduration.py create mode 100644 awx/lib/site-packages/isodate/isoerror.py create mode 100644 awx/lib/site-packages/isodate/isostrf.py create mode 100644 awx/lib/site-packages/isodate/isotime.py create mode 100644 awx/lib/site-packages/isodate/isotzinfo.py create mode 100644 awx/lib/site-packages/isodate/tests/__init__.py create mode 100644 awx/lib/site-packages/isodate/tests/test_date.py create mode 100644 awx/lib/site-packages/isodate/tests/test_datetime.py create mode 100644 awx/lib/site-packages/isodate/tests/test_duration.py create mode 100644 awx/lib/site-packages/isodate/tests/test_pickle.py create mode 100644 awx/lib/site-packages/isodate/tests/test_strf.py create mode 100644 awx/lib/site-packages/isodate/tests/test_time.py create mode 100644 awx/lib/site-packages/isodate/tzinfo.py create mode 100644 awx/lib/site-packages/winrm/__init__.py create mode 100644 awx/lib/site-packages/winrm/exceptions.py create mode 100644 awx/lib/site-packages/winrm/protocol.py create mode 100644 awx/lib/site-packages/winrm/tests/__init__.py create mode 100644 awx/lib/site-packages/winrm/tests/config_example.json create mode 100644 awx/lib/site-packages/winrm/tests/conftest.py create mode 100644 awx/lib/site-packages/winrm/tests/sample_script.ps1 create mode 100644 awx/lib/site-packages/winrm/tests/test_cmd.py create mode 100644 awx/lib/site-packages/winrm/tests/test_integration_protocol.py create mode 100644 awx/lib/site-packages/winrm/tests/test_integration_session.py create mode 100644 awx/lib/site-packages/winrm/tests/test_nori_type_casting.py create mode 100644 awx/lib/site-packages/winrm/tests/test_powershell.py create mode 100644 awx/lib/site-packages/winrm/tests/test_protocol.py create mode 100644 awx/lib/site-packages/winrm/tests/test_session.py create mode 100644 awx/lib/site-packages/winrm/tests/test_wql.py create mode 100644 awx/lib/site-packages/winrm/transport.py create mode 100644 awx/lib/site-packages/xmltodict.py create mode 100644 requirements/isodate-0.5.0.tar.gz create mode 100644 requirements/pywinrm-master.zip create mode 100644 requirements/xmltodict-0.9.0.tar.gz 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[+-]){%d}(?P[0-9]{%d})" + r"-(?P[0-9]{2})-(?P[0-9]{2})" + % (sign, yeardigits))) + # YYYYMMDD or +- YYYYYYMMDD... basic date format + cache_entry.append(re.compile(r"(?P[+-]){%d}(?P[0-9]{%d})" + r"(?P[0-9]{2})(?P[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[+-]){%d}(?P[0-9]{%d})" + r"-W(?P[0-9]{2})-(?P[0-9]{1})" + % (sign, yeardigits))) + # YYYYWwwD or +-YYYYYYWwwD ... basic week date + cache_entry.append(re.compile(r"(?P[+-]){%d}(?P[0-9]{%d})W" + r"(?P[0-9]{2})(?P[0-9]{1})" + % (sign, yeardigits))) + # 3. ordinal dates: + # YYYY-DDD or +-YYYYYY-DDD ... extended format + cache_entry.append(re.compile(r"(?P[+-]){%d}(?P[0-9]{%d})" + r"-(?P[0-9]{3})" + % (sign, yeardigits))) + # YYYYDDD or +-YYYYYYDDD ... basic format + cache_entry.append(re.compile(r"(?P[+-]){%d}(?P[0-9]{%d})" + r"(?P[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[+-]){%d}(?P[0-9]{%d})" + r"-W(?P[0-9]{2})" + % (sign, yeardigits))) + # YYYYWww or +-YYYYYYWww ... basic reduced accuracy week date + cache_entry.append(re.compile(r"(?P[+-]){%d}(?P[0-9]{%d})W" + r"(?P[0-9]{2})" + % (sign, yeardigits))) + # 5. month dates: + # YYY-MM or +-YYYYYY-MM ... reduced accuracy specific month + cache_entry.append(re.compile(r"(?P[+-]){%d}(?P[0-9]{%d})" + r"-(?P[0-9]{2})" + % (sign, yeardigits))) + # 6. year dates: + # YYYY or +-YYYYYY ... reduced accuracy specific year + cache_entry.append(re.compile(r"(?P[+-]){%d}(?P[0-9]{%d})" + % (sign, yeardigits))) + # 7. century dates: + # YY or +-YYYY ... reduced accuracy specific century + cache_entry.append(re.compile(r"(?P[+-]){%d}" + r"(?P[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[+-])?" + r"P(?P[0-9]+([,.][0-9]+)?Y)?" + r"(?P[0-9]+([,.][0-9]+)?M)?" + r"(?P[0-9]+([,.][0-9]+)?W)?" + r"(?P[0-9]+([,.][0-9]+)?D)?" + r"((?PT)(?P[0-9]+([,.][0-9]+)?H)?" + r"(?P[0-9]+([,.][0-9]+)?M)?" + r"(?P[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[0-9]{2}):" + r"(?P[0-9]{2}):" + r"(?P[0-9]{2}([,.][0-9]+)?)" + + TZ_REGEX)) + # hhmmss.ss ... basic format + TIME_REGEX_CACHE.append(re.compile(r"T?(?P[0-9]{2})" + r"(?P[0-9]{2})" + r"(?P[0-9]{2}([,.][0-9]+)?)" + + TZ_REGEX)) + # 2. reduced accuracy: + # hh:mm.mm ... extended format + TIME_REGEX_CACHE.append(re.compile(r"T?(?P[0-9]{2}):" + r"(?P[0-9]{2}([,.][0-9]+)?)" + + TZ_REGEX)) + # hhmm.mm ... basic format + TIME_REGEX_CACHE.append(re.compile(r"T?(?P[0-9]{2})" + r"(?P[0-9]{2}([,.][0-9]+)?)" + + TZ_REGEX)) + # hh.hh ... basic format + TIME_REGEX_CACHE.append(re.compile(r"T?(?P[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(Z|(?P[+-])"\ + r"(?P[0-9]{2})(:(?P[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 "" % 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 ''.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... + # + # to... + # + # 0 + # + 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 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 = """\ + + + + http://windows-host:5985/wsman + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + 153600 + uuid:11111111-1111-1111-1111-111111111111 + + + PT60S + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd + http://schemas.xmlsoap.org/ws/2004/09/transfer/Create + + FALSE + 437 + + + + + stdin + stdout stderr + + +""" + +open_shell_response = """\ + + + + http://schemas.xmlsoap.org/ws/2004/09/transfer/CreateResponse + uuid:11111111-1111-1111-1111-111111111112 + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + uuid:11111111-1111-1111-1111-111111111111 + + + + http://windows-host:5985/wsman + + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd + + 11111111-1111-1111-1111-111111111113 + + + + +""" + +close_shell_request = """\ + + + + http://windows-host:5985/wsman + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + 153600 + uuid:11111111-1111-1111-1111-111111111111 + + + PT60S + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd + http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete + + 11111111-1111-1111-1111-111111111113 + + + + +""" + +close_shell_response = """\ + + + + http://schemas.xmlsoap.org/ws/2004/09/transfer/DeleteResponse + uuid:11111111-1111-1111-1111-111111111112 + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + uuid:11111111-1111-1111-1111-111111111111 + + + +""" + +run_cmd_with_args_request = """\ + + + + http://windows-host:5985/wsman + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + 153600 + uuid:11111111-1111-1111-1111-111111111111 + + + PT60S + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command + + 11111111-1111-1111-1111-111111111113 + + + TRUE + FALSE + + + + + ipconfig + /all + + +""" + +run_cmd_wo_args_request = """\ + + + + http://windows-host:5985/wsman + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + 153600 + uuid:11111111-1111-1111-1111-111111111111 + + + PT60S + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command + + 11111111-1111-1111-1111-111111111113 + + + TRUE + FALSE + + + + + hostname + + +""" + +run_cmd_response = """\ + + + + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandResponse + uuid:11111111-1111-1111-1111-111111111112 + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + uuid:11111111-1111-1111-1111-111111111111 + + + + 11111111-1111-1111-1111-111111111114 + + +""" + +cleanup_cmd_request = """\ + + + + http://windows-host:5985/wsman + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + 153600 + uuid:11111111-1111-1111-1111-111111111111 + + + PT60S + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Signal + + 11111111-1111-1111-1111-111111111113 + + + + + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate + + +""" + +cleanup_cmd_response = """\ + + + + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/SignalResponse + uuid:11111111-1111-1111-1111-111111111112 + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + uuid:11111111-1111-1111-1111-111111111111 + + + + +""" + +get_cmd_output_request = """\ + + + + http://windows-host:5985/wsman + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + 153600 + uuid:11111111-1111-1111-1111-111111111111 + + + PT60S + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive + + 11111111-1111-1111-1111-111111111113 + + + + + stdout stderr + + +""" + +get_cmd_output_response = """\ + + + + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/ReceiveResponse + uuid:11111111-1111-1111-1111-111111111112 + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + uuid:11111111-1111-1111-1111-111111111111 + + + + DQpXaW5kb3dzIElQIENvbmZpZ3VyYXRpb24NCg0K + ICAgSG9zdCBOYW1lIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiAuIDogV0lORE9XUy1IT1NUCiAgIFByaW1hcnkgRG5zIFN1ZmZpeCAgLiAuIC4gLiAuIC4gLiA6IAogICBOb2RlIFR5cGUgLiAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBIeWJyaWQKICAgSVAgUm91dGluZyBFbmFibGVkLiAuIC4gLiAuIC4gLiAuIDogTm8KICAgV0lOUyBQcm94eSBFbmFibGVkLiAuIC4gLiAuIC4gLiAuIDogTm8KCkV0aGVybmV0IGFkYXB0ZXIgTG9jYWwgQXJlYSBDb25uZWN0aW9uOgoKICAgQ29ubmVjdGlvbi1zcGVjaWZpYyBETlMgU3VmZml4ICAuIDogCiAgIERlc2NyaXB0aW9uIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IEludGVsKFIpIDgyNTY3Vi0yIEdpZ2FiaXQgTmV0d29yayBDb25uZWN0aW9uCiAgIFBoeXNpY2FsIEFkZHJlc3MuIC4gLiAuIC4gLiAuIC4gLiA6IEY4LTBGLTQxLTE2LTg4LUU4CiAgIERIQ1AgRW5hYmxlZC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IE5vCiAgIEF1dG9jb25maWd1cmF0aW9uIEVuYWJsZWQgLiAuIC4gLiA6IFllcwogICBMaW5rLWxvY2FsIElQdjYgQWRkcmVzcyAuIC4gLiAuIC4gOiBmZTgwOjphOTkwOjM1ZTM6YTZhYjpmYzE1JTEwKFByZWZlcnJlZCkgCiAgIElQdjQgQWRkcmVzcy4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IDE3My4xODUuMTUzLjkzKFByZWZlcnJlZCkgCiAgIFN1Ym5ldCBNYXNrIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IDI1NS4yNTUuMjU1LjI0OAogICBEZWZhdWx0IEdhdGV3YXkgLiAuIC4gLiAuIC4gLiAuIC4gOiAxNzMuMTg1LjE1My44OQogICBESENQdjYgSUFJRCAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiAyNTExMzc4NTcKICAgREhDUHY2IENsaWVudCBEVUlELiAuIC4gLiAuIC4gLiAuIDogMDAtMDEtMDAtMDEtMTYtM0ItM0YtQzItRjgtMEYtNDEtMTYtODgtRTgKICAgRE5TIFNlcnZlcnMgLiAuIC4gLiAuIC4gLiAuIC4gLiAuIDogMjA3LjkxLjUuMzIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjA4LjY3LjIyMi4yMjIKICAgTmV0QklPUyBvdmVyIFRjcGlwLiAuIC4gLiAuIC4gLiAuIDogRW5hYmxlZAoKRXRoZXJuZXQgYWRhcHRlciBMb2NhbCBBcmVhIENvbm5lY3Rpb24qIDk6CgogICBNZWRpYSBTdGF0ZSAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBNZWRpYSBkaXNjb25uZWN0ZWQKICAgQ29ubmVjdGlvbi1zcGVjaWZpYyBETlMgU3VmZml4ICAuIDogCiAgIERlc2NyaXB0aW9uIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IEp1bmlwZXIgTmV0d29yayBDb25uZWN0IFZpcnR1YWwgQWRhcHRlcgogICBQaHlzaWNhbCBBZGRyZXNzLiAuIC4gLiAuIC4gLiAuIC4gOiAwMC1GRi1BMC04My00OC0wNAogICBESENQIEVuYWJsZWQuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBZZXMKICAgQXV0b2NvbmZpZ3VyYXRpb24gRW5hYmxlZCAuIC4gLiAuIDogWWVzCgpUdW5uZWwgYWRhcHRlciBpc2F0YXAue0FBNDI2QjM3LTM2OTUtNEVCOC05OTBGLTRDRkFDODQ1RkQxN306CgogICBNZWRpYSBTdGF0ZSAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBNZWRpYSBkaXNjb25uZWN0ZWQKICAgQ29ubmVjdGlvbi1zcGVjaWZpYyBETlMgU3VmZml4ICAuIDogCiAgIERlc2NyaXB0aW9uIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IE1pY3Jvc29mdCBJU0FUQVAgQWRhcHRlcgogICBQaHlzaWNhbCBBZGRyZXNzLiAuIC4gLiAuIC4gLiAuIC4gOiAwMC0wMC0wMC0wMC0wMC0wMC0wMC1FMAogICBESENQIEVuYWJsZWQuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBObwogICBBdXRvY29uZmlndXJhdGlvbiBFbmFibGVkIC4gLiAuIC4gOiBZZXMKClR1bm5lbCBhZGFwdGVyIFRlcmVkbyBUdW5uZWxpbmcgUHNldWRvLUludGVyZmFjZToKCiAgIENvbm5lY3Rpb24tc3BlY2lmaWMgRE5TIFN1ZmZpeCAgLiA6IAogICBEZXNjcmlwdGlvbiAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBUZXJlZG8gVHVubmVsaW5nIFBzZXVkby1JbnRlcmZhY2UKICAgUGh5c2ljYWwgQWRkcmVzcy4gLiAuIC4gLiAuIC4gLiAuIDogMDAtMDAtMDAtMDAtMDAtMDAtMDAtRTAKICAgREhDUCBFbmFibGVkLiAuIC4gLiAuIC4gLiAuIC4gLiAuIDogTm8KICAgQXV0b2NvbmZpZ3VyYXRpb24gRW5hYmxlZCAuIC4gLiAuIDogWWVzCiAgIElQdjYgQWRkcmVzcy4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IDIwMDE6MDo5ZDM4Ojk1M2M6MmNlZjo3ZmM6NTI0Njo2NmEyKFByZWZlcnJlZCkgCiAgIExpbmstbG9jYWwgSVB2NiBBZGRyZXNzIC4gLiAuIC4gLiA6IGZlODA6OjJjZWY6N2ZjOjUyNDY6NjZhMiUxMyhQcmVmZXJyZWQpIAogICBEZWZhdWx0IEdhdGV3YXkgLiAuIC4gLiAuIC4gLiAuIC4gOiAKICAgTmV0QklPUyBvdmVyIFRjcGlwLiAuIC4gLiAuIC4gLiAuIDogRGlzYWJsZWQKClR1bm5lbCBhZGFwdGVyIDZUTzQgQWRhcHRlcjoKCiAgIENvbm5lY3Rpb24tc3BlY2lmaWMgRE5TIFN1ZmZpeCAgLiA6IAogICBEZXNjcmlwdGlvbiAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBNaWNyb3NvZnQgNnRvNCBBZGFwdGVyICMyCiAgIFBoeXNpY2FsIEFkZHJlc3MuIC4gLiAuIC4gLiAuIC4gLiA6IDAwLTAwLTAwLTAwLTAwLTAwLTAwLUUwCiAgIERIQ1AgRW5hYmxlZC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IE5vCiAgIEF1dG9jb25maWd1cmF0aW9uIEVuYWJsZWQgLiAuIC4gLiA6IFllcwogICBJUHY2IEFkZHJlc3MuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiAyMDAyOmFkYjk6OTk1ZDo6YWRiOTo5OTVkKFByZWZlcnJlZCkgCiAgIERlZmF1bHQgR2F0ZXdheSAuIC4gLiAuIC4gLiAuIC4gLiA6IDIwMDI6YzA1ODo2MzAxOjpjMDU4OjYzMDEKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjAwMjpjMDU4OjYzMDE6OjEKICAgRE5TIFNlcnZlcnMgLiAuIC4gLiAuIC4gLiAuIC4gLiAuIDogMjA3LjkxLjUuMzIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjA4LjY3LjIyMi4yMjIKICAgTmV0QklPUyBvdmVyIFRjcGlwLiAuIC4gLiAuIC4gLiAuIDogRGlzYWJsZWQKClR1bm5lbCBhZGFwdGVyIGlzYXRhcC57QkExNjBGQzUtNzAyOC00QjFGLUEwNEItMUFDODAyQjBGRjVBfToKCiAgIE1lZGlhIFN0YXRlIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IE1lZGlhIGRpc2Nvbm5lY3RlZAogICBDb25uZWN0aW9uLXNwZWNpZmljIEROUyBTdWZmaXggIC4gOiAKICAgRGVzY3JpcHRpb24gLiAuIC4gLiAuIC4gLiAuIC4gLiAuIDogTWljcm9zb2Z0IElTQVRBUCBBZGFwdGVyICMyCiAgIFBoeXNpY2FsIEFkZHJlc3MuIC4gLiAuIC4gLiAuIC4gLiA6IDAwLTAwLTAwLTAwLTAwLTAwLTAwLUUwCiAgIERIQ1AgRW5hYmxlZC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IE5vCiAgIEF1dG9jb25maWd1cmF0aW9uIEVuYWJsZWQgLiAuIC4gLiA6IFllcwo= + + + + 0 + + + +""" + + +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 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 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 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 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 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(\"\"\" + ... + ... 1 + ... 2 + ... + ... \"\"\") + >>> 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(\"\"\" + ... + ... 1 + ... 2 + ... \"\"\", 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('12x', + ... 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('hello', 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 new file mode 100644 index 0000000000000000000000000000000000000000..236bec4ec30f840b455e179553785ba362f2805b GIT binary patch literal 25917 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 literal 0 HcmV?d00001 diff --git a/requirements/pywinrm-master.zip b/requirements/pywinrm-master.zip new file mode 100644 index 0000000000000000000000000000000000000000..2f5c1aef13c500ecb72491023b9a2d6b4d63d7d7 GIT binary patch literal 21375 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 Y7@C?{7?>s}rz9F0rI;HgTN)<<09(>2p8x;= literal 0 HcmV?d00001 diff --git a/requirements/xmltodict-0.9.0.tar.gz b/requirements/xmltodict-0.9.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..190b9d8b8744930af35200b1ecba208a7b976cc8 GIT binary patch literal 37078 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 WcGQmAQ9It3$NvYN0cBVKfC2!cx(`SI literal 0 HcmV?d00001