summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--awxkit/awxkit/__init__.py8
-rw-r--r--awxkit/awxkit/api/mixins/has_status.py4
-rw-r--r--awxkit/awxkit/api/pages/page.py44
-rw-r--r--awxkit/awxkit/cli/__init__.py10
-rwxr-xr-xawxkit/awxkit/cli/client.py13
-rw-r--r--awxkit/awxkit/cli/custom.py4
-rw-r--r--awxkit/awxkit/cli/format.py11
-rw-r--r--awxkit/awxkit/cli/resource.py27
-rw-r--r--awxkit/awxkit/cli/stdout.py9
-rw-r--r--awxkit/awxkit/cli/utils.py2
-rw-r--r--awxkit/awxkit/utils/__init__.py19
-rw-r--r--awxkit/awxkit/ws.py7
-rw-r--r--awxkit/requirements.txt1
-rw-r--r--awxkit/setup.py2
-rw-r--r--awxkit/test/cli/test_client.py10
-rw-r--r--awxkit/test/cli/test_options.py5
-rw-r--r--awxkit/test/test_credentials.py5
-rw-r--r--awxkit/test/test_utils.py17
-rw-r--r--awxkit/test/test_ws.py5
-rw-r--r--awxkit/tox.ini1
21 files changed, 154 insertions, 52 deletions
diff --git a/Makefile b/Makefile
index b3466589d9..1a7a6d63aa 100644
--- a/Makefile
+++ b/Makefile
@@ -376,7 +376,7 @@ test:
. $(VENV_BASE)/awx/bin/activate; \
fi; \
PYTHONDONTWRITEBYTECODE=1 py.test -p no:cacheprovider -n auto $(TEST_DIRS)
- cd awxkit && $(VENV_BASE)/awx/bin/tox -re py3
+ cd awxkit && $(VENV_BASE)/awx/bin/tox -re py2,py3
awx-manage check_migrations --dry-run --check -n 'vNNN_missing_migration_file'
test_unit:
diff --git a/awxkit/awxkit/__init__.py b/awxkit/awxkit/__init__.py
index 195b0a5cf2..23e0598237 100644
--- a/awxkit/awxkit/__init__.py
+++ b/awxkit/awxkit/__init__.py
@@ -1,4 +1,4 @@
-from .api import pages, client, resources # NOQA
-from .config import config # NOQA
-from . import awx # NOQA
-from .ws import WSClient # NOQA
+from awxkit.api import pages, client, resources # NOQA
+from awxkit.config import config # NOQA
+from awxkit import awx # NOQA
+from awxkit.ws import WSClient # NOQA
diff --git a/awxkit/awxkit/api/mixins/has_status.py b/awxkit/awxkit/api/mixins/has_status.py
index 19ff513410..3f88539988 100644
--- a/awxkit/awxkit/api/mixins/has_status.py
+++ b/awxkit/awxkit/api/mixins/has_status.py
@@ -1,6 +1,8 @@
from datetime import datetime
import json
+import six
+
from awxkit.utils import poll_until
@@ -43,7 +45,7 @@ class HasStatus(object):
return self.wait_until_status(self.started_statuses, interval=interval, timeout=timeout)
def assert_status(self, status_list, msg=None):
- if isinstance(status_list, str):
+ if isinstance(status_list, six.text_type):
status_list = [status_list]
if self.status in status_list:
# include corner cases in is_successful logic
diff --git a/awxkit/awxkit/api/pages/page.py b/awxkit/awxkit/api/pages/page.py
index a88c9e36bd..f81f9da3e7 100644
--- a/awxkit/awxkit/api/pages/page.py
+++ b/awxkit/awxkit/api/pages/page.py
@@ -1,10 +1,11 @@
-import http.client
import inspect
import logging
import json
import re
from requests import Response
+import six
+from six.moves import http_client as http
from awxkit.utils import (
PseudoNamespace,
@@ -12,7 +13,8 @@ from awxkit.utils import (
are_same_endpoint,
super_dir_set,
suppress,
- is_list_or_tuple
+ is_list_or_tuple,
+ to_str
)
from awxkit.api.client import Connection
from awxkit.api.registry import URLRegistry
@@ -167,7 +169,11 @@ class Page(object):
@classmethod
def from_json(cls, raw):
resp = Response()
- resp._content = bytes(json.dumps(raw), 'utf-8')
+ data = json.dumps(raw)
+ if six.PY3:
+ resp._content = bytes(data, 'utf-8')
+ else:
+ resp._content = data
resp.encoding = 'utf-8'
resp.status_code = 200
return cls(r=resp)
@@ -199,16 +205,16 @@ class Page(object):
"Unable to parse JSON response ({0.status_code}): {1} - '{2}'".format(response, e, text))
exc_str = "%s (%s) received" % (
- http.client.responses[response.status_code], response.status_code)
+ http.responses[response.status_code], response.status_code)
exception = exception_from_status_code(response.status_code)
if exception:
raise exception(exc_str, data)
if response.status_code in (
- http.client.OK,
- http.client.CREATED,
- http.client.ACCEPTED):
+ http.OK,
+ http.CREATED,
+ http.ACCEPTED):
# Not all JSON responses include a URL. Grab it from the request
# object, if needed.
@@ -235,7 +241,7 @@ class Page(object):
r=response,
ds=ds)
- elif response.status_code == http.client.FORBIDDEN:
+ elif response.status_code == http.FORBIDDEN:
if is_license_invalid(response):
raise exc.LicenseInvalid(exc_str, data)
elif is_license_exceeded(response):
@@ -243,7 +249,7 @@ class Page(object):
else:
raise exc.Forbidden(exc_str, data)
- elif response.status_code == http.client.BAD_REQUEST:
+ elif response.status_code == http.BAD_REQUEST:
if is_license_invalid(response):
raise exc.LicenseInvalid(exc_str, data)
if is_duplicate_error(response):
@@ -314,14 +320,14 @@ class Page(object):
return page_cls(self.connection, endpoint=endpoint).get(**kw)
-_exception_map = {http.client.NO_CONTENT: exc.NoContent,
- http.client.NOT_FOUND: exc.NotFound,
- http.client.INTERNAL_SERVER_ERROR: exc.InternalServerError,
- http.client.BAD_GATEWAY: exc.BadGateway,
- http.client.METHOD_NOT_ALLOWED: exc.MethodNotAllowed,
- http.client.UNAUTHORIZED: exc.Unauthorized,
- http.client.PAYMENT_REQUIRED: exc.PaymentRequired,
- http.client.CONFLICT: exc.Conflict}
+_exception_map = {http.NO_CONTENT: exc.NoContent,
+ http.NOT_FOUND: exc.NotFound,
+ http.INTERNAL_SERVER_ERROR: exc.InternalServerError,
+ http.BAD_GATEWAY: exc.BadGateway,
+ http.METHOD_NOT_ALLOWED: exc.MethodNotAllowed,
+ http.UNAUTHORIZED: exc.Unauthorized,
+ http.PAYMENT_REQUIRED: exc.PaymentRequired,
+ http.CONFLICT: exc.Conflict}
def exception_from_status_code(status_code):
@@ -376,10 +382,10 @@ class PageList(object):
class TentativePage(str):
def __new__(cls, endpoint, connection):
- return super(TentativePage, cls).__new__(cls, endpoint)
+ return super(TentativePage, cls).__new__(cls, to_str(endpoint))
def __init__(self, endpoint, connection):
- self.endpoint = endpoint
+ self.endpoint = to_str(endpoint)
self.connection = connection
def _create(self):
diff --git a/awxkit/awxkit/cli/__init__.py b/awxkit/awxkit/cli/__init__.py
index 40964c5a45..71e766841a 100644
--- a/awxkit/awxkit/cli/__init__.py
+++ b/awxkit/awxkit/cli/__init__.py
@@ -6,6 +6,7 @@ import yaml
from requests.exceptions import ConnectionError, SSLError
from .client import CLI
+from awxkit.utils import to_str
from awxkit.exceptions import Unauthorized, Common
from awxkit.cli.utils import cprint
@@ -46,7 +47,14 @@ def run(stdout=sys.stdout, stderr=sys.stderr, argv=[]):
if cli.get_config('format') == 'json':
json.dump(e.msg, sys.stdout)
elif cli.get_config('format') == 'yaml':
- sys.stdout.write(yaml.dump(e.msg))
+ sys.stdout.write(to_str(
+ yaml.safe_dump(
+ e.msg,
+ default_flow_style=False,
+ encoding='utf-8',
+ allow_unicode=True
+ )
+ ))
sys.exit(1)
except Exception as e:
if cli.verbose:
diff --git a/awxkit/awxkit/cli/client.py b/awxkit/awxkit/cli/client.py
index a0b006761a..0c130c079e 100755
--- a/awxkit/awxkit/cli/client.py
+++ b/awxkit/awxkit/cli/client.py
@@ -1,9 +1,12 @@
+from __future__ import print_function
+
import logging
import os
import pkg_resources
import sys
from requests.exceptions import RequestException
+import six
from .custom import handle_custom_actions
from .format import (add_authentication_arguments,
@@ -160,10 +163,18 @@ class CLI(object):
changed=self.original_action in ('modify', 'create')
)
if formatted:
- print(formatted, file=self.stdout)
+ print(utils.to_str(formatted), file=self.stdout)
else:
self.parser.print_help()
+ if six.PY2 and not self.help:
+ # Unfortunately, argparse behavior between py2 and py3
+ # changed in a notable way when required subparsers
+ # have invalid (or missing) arguments specified
+ # see: https://github.com/python/cpython/commit/f97c59aaba2d93e48cbc6d25f7ff9f9c87f8d0b2
+ print('\nargument resource: invalid choice')
+ raise SystemExit(2)
+
def parse_action(self, page, from_sphinx=False):
"""Perform an HTTP OPTIONS request
diff --git a/awxkit/awxkit/cli/custom.py b/awxkit/awxkit/cli/custom.py
index 8127edd3cf..545c920f06 100644
--- a/awxkit/awxkit/cli/custom.py
+++ b/awxkit/awxkit/cli/custom.py
@@ -1,3 +1,5 @@
+from six import with_metaclass
+
from .stdout import monitor, monitor_workflow
from .utils import CustomRegistryMeta, color_enabled
@@ -17,7 +19,7 @@ class CustomActionRegistryMeta(CustomRegistryMeta):
return ' '.join([self.resource, self.action])
-class CustomAction(object, metaclass=CustomActionRegistryMeta):
+class CustomAction(with_metaclass(CustomActionRegistryMeta)):
"""Base class for defining a custom action for a resource."""
def __init__(self, page):
diff --git a/awxkit/awxkit/cli/format.py b/awxkit/awxkit/cli/format.py
index 1a99a6eaf7..8ddc7d8fff 100644
--- a/awxkit/awxkit/cli/format.py
+++ b/awxkit/awxkit/cli/format.py
@@ -1,6 +1,7 @@
import json
from distutils.util import strtobool
+import six
import yaml
from awxkit.cli.utils import colored
@@ -79,7 +80,7 @@ def add_output_formatting_arguments(parser, env):
def format_response(response, fmt='json', filter='.', changed=False):
if response is None:
return # HTTP 204
- if isinstance(response, str):
+ if isinstance(response, six.text_type):
return response
if 'results' in response.__dict__:
@@ -113,7 +114,7 @@ def format_jq(output, fmt):
results = []
for x in jq.jq(fmt).transform(output, multiple_output=True):
if x not in (None, ''):
- if isinstance(x, str):
+ if isinstance(x, six.text_type):
results.append(x)
else:
results.append(json.dumps(x))
@@ -126,9 +127,11 @@ def format_json(output, fmt):
def format_yaml(output, fmt):
output = json.loads(json.dumps(output))
- return yaml.dump(
+ return yaml.safe_dump(
output,
- default_flow_style=False
+ default_flow_style=False,
+ encoding='utf-8',
+ allow_unicode=True
)
diff --git a/awxkit/awxkit/cli/resource.py b/awxkit/awxkit/cli/resource.py
index e7f44c51c1..d8565de614 100644
--- a/awxkit/awxkit/cli/resource.py
+++ b/awxkit/awxkit/cli/resource.py
@@ -1,5 +1,7 @@
import os
+from six import PY3, with_metaclass
+
from awxkit import api, config
from awxkit.api.pages import Page
from awxkit.cli.format import format_response, add_authentication_arguments
@@ -40,7 +42,7 @@ DEPRECATED_RESOURCES_REVERSE = dict(
)
-class CustomCommand(object, metaclass=CustomRegistryMeta):
+class CustomCommand(with_metaclass(CustomRegistryMeta)):
"""Base class for implementing custom commands.
Custom commands represent static code which should run - they are
@@ -121,15 +123,28 @@ def parse_resource(client, skip_deprecated=False):
if k in ('dashboard',):
# the Dashboard API is deprecated and not supported
continue
- aliases = []
- if not skip_deprecated:
+
+ # argparse aliases are *only* supported in Python3 (not 2.7)
+ kwargs = {}
+ if not skip_deprecated and PY3:
if k in DEPRECATED_RESOURCES:
- aliases = [DEPRECATED_RESOURCES[k]]
+ kwargs['aliases'] = [DEPRECATED_RESOURCES[k]]
client.subparsers[k] = subparsers.add_parser(
- k, help='', aliases=aliases
+ k, help='', **kwargs
)
- resource = client.parser.parse_known_args()[0].resource
+ try:
+ resource = client.parser.parse_known_args()[0].resource
+ except SystemExit:
+ if PY3:
+ raise
+ else:
+ # Unfortunately, argparse behavior between py2 and py3
+ # changed in a notable way when required subparsers
+ # have invalid (or missing) arguments specified
+ # see: https://github.com/python/cpython/commit/f97c59aaba2d93e48cbc6d25f7ff9f9c87f8d0b2
+ # In py2, this raises a SystemExit; which we want to _ignore_
+ resource = None
if resource in DEPRECATED_RESOURCES.values():
client.argv[
client.argv.index(resource)
diff --git a/awxkit/awxkit/cli/stdout.py b/awxkit/awxkit/cli/stdout.py
index 6fbeea2ef0..47ca7f79f6 100644
--- a/awxkit/awxkit/cli/stdout.py
+++ b/awxkit/awxkit/cli/stdout.py
@@ -1,8 +1,12 @@
+# -*- coding: utf-8 -*-
+from __future__ import print_function
+
import sys
import time
from .utils import cprint, color_enabled, STATUS_COLORS
+from awxkit.utils import to_str
def monitor_workflow(response, session, print_stdout=True, timeout=None,
@@ -25,6 +29,7 @@ def monitor_workflow(response, session, print_stdout=True, timeout=None,
sys.stdout.write('\x1b[2K')
for result in results:
+ result['name'] = to_str(result['name'])
if print_stdout:
print(' ↳ {id} - {name} '.format(**result), end='')
status = result['status']
@@ -39,7 +44,7 @@ def monitor_workflow(response, session, print_stdout=True, timeout=None,
cprint('------Starting Standard Out Stream------', 'red')
if print_stdout:
- print('Launching {}...'.format(get().json.name))
+ print('Launching {}...'.format(to_str(get().json.name)))
started = time.time()
seen = set()
@@ -84,7 +89,7 @@ def monitor(response, session, print_stdout=True, timeout=None, interval=.25):
# skip it for now and wait until the prior lines arrive and are
# printed
continue
- stdout = result.get('stdout')
+ stdout = to_str(result.get('stdout'))
if stdout and print_stdout:
print(stdout)
next_line = result['end_line']
diff --git a/awxkit/awxkit/cli/utils.py b/awxkit/awxkit/cli/utils.py
index 1ea7fcff9d..3048ef6a6a 100644
--- a/awxkit/awxkit/cli/utils.py
+++ b/awxkit/awxkit/cli/utils.py
@@ -1,3 +1,5 @@
+from __future__ import print_function
+
from argparse import ArgumentParser
import os
import sys
diff --git a/awxkit/awxkit/utils/__init__.py b/awxkit/awxkit/utils/__init__.py
index 2750115685..f426872272 100644
--- a/awxkit/awxkit/utils/__init__.py
+++ b/awxkit/awxkit/utils/__init__.py
@@ -10,6 +10,7 @@ import sys
import re
import os
+import six
import yaml
from awxkit.words import words
@@ -132,7 +133,7 @@ class PseudoNamespace(dict):
def is_relative_endpoint(candidate):
- return isinstance(candidate, (str,)) and candidate.startswith('/api/')
+ return isinstance(candidate, (six.text_type,)) and candidate.startswith('/api/')
def is_class_or_instance(obj, cls):
@@ -320,6 +321,22 @@ def update_payload(payload, fields, kwargs):
return payload
+def to_str(obj):
+ if six.PY3:
+ if isinstance(obj, bytes):
+ return obj.decode('utf-8')
+ return obj
+ if not isinstance(obj, six.text_type):
+ try:
+ return str(obj)
+ except UnicodeDecodeError:
+ try:
+ obj = six.text_type(obj, 'utf8')
+ except UnicodeDecodeError:
+ obj = obj.decode('latin1')
+ return obj.encode('utf8')
+
+
def to_bool(obj):
if isinstance(obj, (str,)):
return obj.lower() not in ('false', 'off', 'no', 'n', '0', '')
diff --git a/awxkit/awxkit/ws.py b/awxkit/awxkit/ws.py
index 4c39906914..8005a8ef66 100644
--- a/awxkit/awxkit/ws.py
+++ b/awxkit/awxkit/ws.py
@@ -1,11 +1,12 @@
-from queue import Queue, Empty
import time
import threading
import logging
import atexit
import json
import ssl
-import urllib.parse
+
+from six.moves.queue import Queue, Empty
+from six.moves.urllib.parse import urlparse
from awxkit.config import config
@@ -55,7 +56,7 @@ class WSClient(object):
import websocket
if not hostname:
- result = urllib.parse.urlparse(config.base_url)
+ result = urlparse(config.base_url)
secure = result.scheme == 'https'
port = result.port
if port is None:
diff --git a/awxkit/requirements.txt b/awxkit/requirements.txt
index 6c9fdba970..dd2d30b1de 100644
--- a/awxkit/requirements.txt
+++ b/awxkit/requirements.txt
@@ -1,2 +1,3 @@
PyYAML
requests
+six
diff --git a/awxkit/setup.py b/awxkit/setup.py
index 2a13705b3b..3e76944fa0 100644
--- a/awxkit/setup.py
+++ b/awxkit/setup.py
@@ -67,7 +67,7 @@ setup(
},
include_package_data=True,
install_requires=requirements,
- python_requires=">= 3.5",
+ python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*",
extras_require={
'formatting': ['jq', 'tabulate'],
'websockets': ['websocket-client>0.54.0'],
diff --git a/awxkit/test/cli/test_client.py b/awxkit/test/cli/test_client.py
index db16e37393..e792b6c267 100644
--- a/awxkit/test/cli/test_client.py
+++ b/awxkit/test/cli/test_client.py
@@ -50,8 +50,14 @@ def test_list_resources(capfd, resource):
cli.parse_args(['awx {}'.format(resource)])
cli.connect()
- cli.parse_resource()
- out, err = capfd.readouterr()
+ try:
+ cli.parse_resource()
+ out, err = capfd.readouterr()
+ except SystemExit:
+ # python2 argparse raises SystemExit for invalid/missing required args,
+ # py3 doesn't
+ _, out = capfd.readouterr()
+
assert "usage:" in out
for snippet in (
'--conf.host https://example.awx.org]',
diff --git a/awxkit/test/cli/test_options.py b/awxkit/test/cli/test_options.py
index 3012349fd4..f0b22f1178 100644
--- a/awxkit/test/cli/test_options.py
+++ b/awxkit/test/cli/test_options.py
@@ -1,7 +1,10 @@
import argparse
import json
import unittest
-from io import StringIO
+try:
+ from StringIO import StringIO
+except ImportError:
+ from io import StringIO
import pytest
from requests import Response
diff --git a/awxkit/test/test_credentials.py b/awxkit/test/test_credentials.py
index 714550119e..bd1331016d 100644
--- a/awxkit/test/test_credentials.py
+++ b/awxkit/test/test_credentials.py
@@ -1,4 +1,7 @@
-from unittest.mock import patch
+try:
+ from unittest.mock import patch
+except ImportError:
+ from mock import patch
import pytest
diff --git a/awxkit/test/test_utils.py b/awxkit/test/test_utils.py
index df4081ccf9..20efa2c640 100644
--- a/awxkit/test/test_utils.py
+++ b/awxkit/test/test_utils.py
@@ -1,8 +1,13 @@
# -*- coding: utf-8 -*-
from datetime import datetime
+import sys
-from unittest import mock
+try:
+ from unittest import mock
+except ImportError:
+ import mock
import pytest
+import six
from awxkit import utils
from awxkit import exceptions as exc
@@ -73,11 +78,19 @@ def test_load_invalid_json_or_yaml(inp):
@pytest.mark.parametrize('non_ascii', [True, False])
+@pytest.mark.skipif(
+ sys.version_info < (3, 6),
+ reason='this is only intended to be used in py3, not the CLI'
+)
def test_random_titles_are_unicode(non_ascii):
- assert isinstance(utils.random_title(non_ascii=non_ascii), str)
+ assert isinstance(utils.random_title(non_ascii=non_ascii), six.text_type)
@pytest.mark.parametrize('non_ascii', [True, False])
+@pytest.mark.skipif(
+ sys.version_info < (3, 6),
+ reason='this is only intended to be used in py3, not the CLI'
+)
def test_random_titles_generates_correct_characters(non_ascii):
title = utils.random_title(non_ascii=non_ascii)
if non_ascii:
diff --git a/awxkit/test/test_ws.py b/awxkit/test/test_ws.py
index afc6b42fc5..ea55ba8ffa 100644
--- a/awxkit/test/test_ws.py
+++ b/awxkit/test/test_ws.py
@@ -1,7 +1,10 @@
# -*- coding: utf-8 -*-
from collections import namedtuple
-from unittest.mock import patch
+try:
+ from unittest.mock import patch
+except ImportError:
+ from mock import patch
import pytest
from awxkit.ws import WSClient
diff --git a/awxkit/tox.ini b/awxkit/tox.ini
index d2769d66c8..7f9509259c 100644
--- a/awxkit/tox.ini
+++ b/awxkit/tox.ini
@@ -14,6 +14,7 @@ setenv =
deps =
websocket-client
coverage
+ mock
pytest
pytest-mock