diff --git a/.isort.cfg b/.isort.cfg index 4606391..ba76e3b 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,5 +1,4 @@ [settings] -not_skip = __init__.py known_third_party= pytest, pytz, diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index afe3272..173d406 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,4 @@ +repos: - repo: https://github.com/pre-commit/pre-commit-hooks sha: v0.9.2 hooks: @@ -10,3 +11,5 @@ sha: b57843b0b874df1d16eb0bef00b868792cb245c2 hooks: - id: python-import-sorter + args: + - --diff diff --git a/.travis.yml b/.travis.yml index dd286fd..0380799 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,7 @@ language: python cache: pip -dist: xenial +dist: bionic python: - - "2.7" - - "3.4" - "3.5" - "3.6" - "3.7" @@ -12,9 +10,9 @@ install: - pip install -U pip setuptools - pip install tox-travis - pip install codecov - - if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then pip install pre-commit; fi + - pip install pre-commit script: - tox - - if [[ $TRAVIS_PYTHON_VERSION != 3.4 ]]; then pre-commit run --all-files; fi + - pre-commit run --all-files after_success: - codecov diff --git a/CHANGES.txt b/CHANGES.txt index 2c1a8b7..09c3155 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,23 @@ +Version 2.2.2 +2020-09-10 +- #85 Provide more details when a Server Response status code is 406. (v20, v21) +- #84 Fallback "Range" HTTP Header for pagination requests. (@teizenman) (v20) +- #81 More flexibility for the Status Endpoint validation. It no longer fails for optional successes, pendings, failures lists (@maybe-sybr) (v20, v21) + +Version 2.2.1 +2020-07-17 +- #78 Prevent KeyError when TAXII Server Response does not include 'Content-Range' Header (v20) + +Version 2.2.0 +2020-07-02 +- #76 drop python versions older than 3.5 +- #75 fixes construction of Range Header for paginated requests RFC 7233. (@dougle) + +Version 2.1.0 +2020-06-09 +- #68 Support for user-specified authentication including token-based authentication. +- #71 Don't log a warning when the total number of available objects is less than the per request size limit. + Version 2.0.0 2020-04-01 Base release of the TAXII Client for 2.1. Unsupported features: diff --git a/docs/conf.py b/docs/conf.py index e2a6828..d617ffe 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,9 +61,9 @@ # built documents. # # The short X.Y version. -version = '2.0.0' +version = '2.2.2' # The full version, including alpha/beta/rc tags. -release = '2.0.0' +release = '2.2.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.cfg b/setup.cfg index 843d59a..025fc6e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.0.0 +current_version = 2.2.2 commit = True tag = True diff --git a/setup.py b/setup.py index 2b30488..aaf7612 100644 --- a/setup.py +++ b/setup.py @@ -39,11 +39,7 @@ def get_long_description(): 'Intended Audience :: Developers', 'Topic :: Security', 'License :: OSI Approved :: BSD License', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', diff --git a/taxii2client/__init__.py b/taxii2client/__init__.py index 6f046ca..cf9c146 100644 --- a/taxii2client/__init__.py +++ b/taxii2client/__init__.py @@ -1,8 +1,21 @@ """Python TAXII 2 Client""" # flake8: noqa +# isort:skip_file -DEFAULT_USER_AGENT = "taxii2-client/2.0.0" +import logging + +# Console Handler for taxii2client messages +ch = logging.StreamHandler() +ch.setFormatter(logging.Formatter("[%(name)s] [%(levelname)-8s] [%(asctime)s] %(message)s")) + +# Module-level logger +log = logging.getLogger(__name__) +log.propagate = False +log.addHandler(ch) + + +DEFAULT_USER_AGENT = "taxii2-client/2.2.2" MEDIA_TYPE_STIX_V20 = "application/vnd.oasis.stix+json; version=2.0" MEDIA_TYPE_TAXII_V20 = "application/vnd.oasis.taxii+json; version=2.0" MEDIA_TYPE_TAXII_V21 = "application/taxii+json; version=2.1" diff --git a/taxii2client/common.py b/taxii2client/common.py index cfd9d05..c20a716 100644 --- a/taxii2client/common.py +++ b/taxii2client/common.py @@ -1,4 +1,5 @@ import datetime +import logging import re import pytz @@ -12,6 +13,9 @@ InvalidArgumentsError, InvalidJSONError, TAXIIServiceException ) +# Module-level logger +log = logging.getLogger(__name__) + def _format_datetime(dttm): """Convert a datetime object into a valid STIX timestamp string. @@ -131,10 +135,22 @@ def _grab_total_items(resp): try: results = re.match(r"^items (\d+)-(\d+)/(\d+)$", resp.headers["Content-Range"]) return int(results.group(2)) - int(results.group(1)) + 1, int(results.group(3)) - except ValueError as e: + except (ValueError, IndexError) as e: six.raise_from(InvalidJSONError( "Invalid Content-Range was received from " + resp.request.url ), e) + except KeyError: + log.warning("TAXII Server Response did not include 'Content-Range' header - results could be incomplete") + return 0, 0 + + +class TokenAuth(requests.auth.AuthBase): + def __init__(self, key): + self.key = key + + def __call__(self, r): + r.headers['Authorization'] = 'Token {}'.format(self.key) + return r class _TAXIIEndpoint(object): @@ -145,7 +161,7 @@ class _TAXIIEndpoint(object): """ def __init__(self, url, conn=None, user=None, password=None, verify=True, - proxies=None, version="2.0"): + proxies=None, version="2.0", auth=None): """Create a TAXII endpoint. Args: @@ -158,13 +174,13 @@ def __init__(self, url, conn=None, user=None, password=None, verify=True, version (str): The spec version this connection is meant to follow. """ - if conn and (user or password): - raise InvalidArgumentsError("A connection and user/password may" - " not both be provided.") + if (conn and ((user or password) or auth)) or ((user or password) and auth): + raise InvalidArgumentsError("Only one of a connection, username/password, or auth object may" + " be provided.") elif conn: self._conn = conn else: - self._conn = _HTTPConnection(user, password, verify, proxies, version=version) + self._conn = _HTTPConnection(user, password, verify, proxies, version=version, auth=auth) # Add trailing slash to TAXII endpoint if missing # https://github.com/oasis-open/cti-taxii-client/issues/50 @@ -201,7 +217,7 @@ class _HTTPConnection(object): """ def __init__(self, user=None, password=None, verify=True, proxies=None, - user_agent=DEFAULT_USER_AGENT, version="2.0"): + user_agent=DEFAULT_USER_AGENT, version="2.0", auth=None): """Create a connection session. Args: @@ -219,8 +235,12 @@ def __init__(self, user=None, password=None, verify=True, proxies=None, self.session.verify = verify # enforce that we always have a connection-default user agent. self.user_agent = user_agent or DEFAULT_USER_AGENT + if user and password: self.session.auth = requests.auth.HTTPBasicAuth(user, password) + elif auth: + self.session.auth = auth + if proxies: self.session.proxies.update(proxies) self.version = version @@ -275,12 +295,26 @@ def get(self, url, headers=None, params=None): resp = self.session.get(url, headers=merged_headers, params=params) - resp.raise_for_status() + try: + resp.raise_for_status() + except requests.exceptions.HTTPError as e: + if resp.status_code == 406: + # Provide more details about this error since its usually an import problem. + # Import the correct version of the TAXII Client. + logging.error( + "Server Response: 406 Client Error " + "If you are trying to contact a TAXII 2.0 Server use 'from taxii2client.v20 import X'. " + "If you are trying to contact a TAXII 2.1 Server use 'from taxii2client.v21 import X'" + ) + raise e content_type = resp.headers["Content-Type"] - if not self.valid_content_type(content_type=content_type, accept=accept): - msg = "Unexpected Response. Got Content-Type: '{}' for Accept: '{}'" + msg = ( + "Unexpected Response. Got Content-Type: '{}' for Accept: '{}'\n" + "If you are trying to contact a TAXII 2.0 Server use 'from taxii2client.v20 import X'\n" + "If you are trying to contact a TAXII 2.1 Server use 'from taxii2client.v21 import X'" + ) raise TAXIIServiceException(msg.format(content_type, accept)) if "Range" in merged_headers and self.version == "2.0": diff --git a/taxii2client/test/test_client_v20.py b/taxii2client/test/test_client_v20.py index c37d286..a298e6b 100644 --- a/taxii2client/test/test_client_v20.py +++ b/taxii2client/test/test_client_v20.py @@ -2,6 +2,7 @@ import json import pytest +import requests import responses import six @@ -9,7 +10,7 @@ DEFAULT_USER_AGENT, MEDIA_TYPE_STIX_V20, MEDIA_TYPE_TAXII_V20 ) from taxii2client.common import ( - _filter_kwargs_to_query_params, _HTTPConnection, _TAXIIEndpoint + TokenAuth, _filter_kwargs_to_query_params, _HTTPConnection, _TAXIIEndpoint ) from taxii2client.exceptions import ( AccessError, InvalidArgumentsError, InvalidJSONError, @@ -193,6 +194,19 @@ BAD_DISCOVERY_RESPONSE = """{"title":""" +ERROR_MESSAGE = """{ + "title": "Error condition XYZ", + "description": "This error is caused when the application tries to access data...", + "error_id": "1234", + "error_code": "581234", + "http_status": "%s", + "external_details": "http://example.com/ticketnumber1/errorid-1234", + "details": { + "somekey1": "somevalue", + "somekey2": "some other value" + } +}""" + @pytest.fixture def status_dict(): @@ -714,11 +728,28 @@ def test_params_filter_unknown(): def test_taxii_endpoint_raises_exception(): """Test exception is raised when conn and (user or pass) is provided""" conn = _HTTPConnection(user="foo", password="bar", verify=False) + error_str = "Only one of a connection, username/password, or auth object may be provided." + fake_url = "https://example.com/api1/collections/" + + with pytest.raises(InvalidArgumentsError) as excinfo: + _TAXIIEndpoint(fake_url, conn, "other", "test") + + assert error_str in str(excinfo.value) with pytest.raises(InvalidArgumentsError) as excinfo: - _TAXIIEndpoint("https://example.com/api1/collections/", conn, "other", "test") + _TAXIIEndpoint(fake_url, conn, auth=TokenAuth('abcd')) + + assert error_str in str(excinfo.value) - assert "A connection and user/password may not both be provided." in str(excinfo.value) + with pytest.raises(InvalidArgumentsError) as excinfo: + _TAXIIEndpoint(fake_url, user="other", password="test", auth=TokenAuth('abcd')) + + assert error_str in str(excinfo.value) + + with pytest.raises(InvalidArgumentsError) as excinfo: + _TAXIIEndpoint(fake_url, conn, "other", "test", auth=TokenAuth('abcd')) + + assert error_str in str(excinfo.value) @responses.activate @@ -747,7 +778,18 @@ def test_invalid_content_type_for_connection(): assert ("Unexpected Response. Got Content-Type: 'application/vnd.oasis.taxii+json; " "version=2.0' for Accept: 'application/vnd.oasis.taxii+json; version=2.0; " - "charset=utf-8'") == str(excinfo.value) + "charset=utf-8'") in str(excinfo.value) + + +@responses.activate +def test_invalid_accept_for_connection(): + responses.add(responses.GET, COLLECTION_URL, COLLECTIONS_RESPONSE, + status=406, content_type=MEDIA_TYPE_TAXII_V20) + + with pytest.raises(requests.exceptions.HTTPError): + conn = _HTTPConnection(user="foo", password="bar", verify=False) + conn.get("https://example.com/api1/collections/91a7b528-80eb-42ed-a74d-c6fbd5a26116/", + headers={"Accept": "application/taxii+json; version=2.1"}) def test_status_missing_id_property(status_dict): @@ -977,3 +1019,75 @@ def test_collection_missing_trailing_slash(): response = collection.get_object("indicator--252c7c11-daf2-42bd-843b-be65edca9f61") indicator = response["objects"][0] assert indicator["id"] == "indicator--252c7c11-daf2-42bd-843b-be65edca9f61" + + +@responses.activate +def test_get_objects_pagination_success(collection): + responses.add(responses.GET, GET_OBJECTS_URL, GET_OBJECTS_RESPONSE, + status=200, content_type=MEDIA_TYPE_STIX_V20) + + response = collection.get_objects(per_request=50).json() + indicator = response["objects"][0] + assert len(response["objects"]) == 1 + assert indicator["id"] == "indicator--252c7c11-daf2-42bd-843b-be65edca9f61" + + +@responses.activate +def test_get_objects_pagination_fail(collection): + error = ERROR_MESSAGE % "400" + responses.add(responses.GET, GET_OBJECTS_URL, error, + status=400, content_type=MEDIA_TYPE_STIX_V20) + + with pytest.raises(requests.exceptions.HTTPError) as e: + collection.get_objects(per_request=50).json() + + assert e.value.response.status_code == 400 + + +@responses.activate +def test_get_objects_pagination_fail_no_page(collection): + error = ERROR_MESSAGE % "400" + responses.add(responses.GET, GET_OBJECTS_URL, error, + status=400, content_type=MEDIA_TYPE_STIX_V20) + + with pytest.raises(requests.exceptions.HTTPError) as e: + collection.get_objects().json() + + assert e.value.response.status_code == 400 + + +@responses.activate +def test_get_manifests_pagination_success(collection): + responses.add(responses.GET, MANIFEST_URL, GET_MANIFEST_RESPONSE, + status=200, content_type=MEDIA_TYPE_TAXII_V20) + + response = collection.get_manifest(per_request=50).json() + assert len(response["objects"]) == 2 + obj = response["objects"][0] + assert obj["id"] == "indicator--29aba82c-5393-42a8-9edb-6a2cb1df070b" + assert len(obj["versions"]) == 2 + assert obj["media_types"][0] == MEDIA_TYPE_STIX_V20 + + +@responses.activate +def test_get_manifests_pagination_fail(collection): + error = ERROR_MESSAGE % "400" + responses.add(responses.GET, MANIFEST_URL, error, + status=400, content_type=MEDIA_TYPE_TAXII_V20) + + with pytest.raises(requests.exceptions.HTTPError) as e: + collection.get_manifest(per_request=50).json() + + assert e.value.response.status_code == 400 + + +@responses.activate +def test_get_manifests_pagination_fail_no_page(collection): + error = ERROR_MESSAGE % "400" + responses.add(responses.GET, MANIFEST_URL, error, + status=400, content_type=MEDIA_TYPE_TAXII_V20) + + with pytest.raises(requests.exceptions.HTTPError) as e: + collection.get_manifest().json() + + assert e.value.response.status_code == 400 diff --git a/taxii2client/test/test_client_v21.py b/taxii2client/test/test_client_v21.py index b61df81..53ae710 100644 --- a/taxii2client/test/test_client_v21.py +++ b/taxii2client/test/test_client_v21.py @@ -2,12 +2,13 @@ import json import pytest +import requests import responses import six from taxii2client import DEFAULT_USER_AGENT, MEDIA_TYPE_TAXII_V21 from taxii2client.common import ( - _filter_kwargs_to_query_params, _HTTPConnection, _TAXIIEndpoint + TokenAuth, _filter_kwargs_to_query_params, _HTTPConnection, _TAXIIEndpoint ) from taxii2client.exceptions import ( AccessError, InvalidArgumentsError, InvalidJSONError, @@ -733,11 +734,28 @@ def test_params_filter_unknown(): def test_taxii_endpoint_raises_exception(): """Test exception is raised when conn and (user or pass) is provided""" conn = _HTTPConnection(user="foo", password="bar", verify=False) + error_str = "Only one of a connection, username/password, or auth object may be provided." + fake_url = "https://example.com/api1/collections/" with pytest.raises(InvalidArgumentsError) as excinfo: - _TAXIIEndpoint("https://example.com/api1/collections/", conn, "other", "test") + _TAXIIEndpoint(fake_url, conn, "other", "test") - assert "A connection and user/password may not both be provided." in str(excinfo.value) + assert error_str in str(excinfo.value) + + with pytest.raises(InvalidArgumentsError) as excinfo: + _TAXIIEndpoint(fake_url, conn, auth=TokenAuth('abcd')) + + assert error_str in str(excinfo.value) + + with pytest.raises(InvalidArgumentsError) as excinfo: + _TAXIIEndpoint(fake_url, user="other", password="test", auth=TokenAuth('abcd')) + + assert error_str in str(excinfo.value) + + with pytest.raises(InvalidArgumentsError) as excinfo: + _TAXIIEndpoint(fake_url, conn, "other", "test", auth=TokenAuth('abcd')) + + assert error_str in str(excinfo.value) @responses.activate @@ -766,7 +784,18 @@ def test_invalid_content_type_for_connection(): assert ("Unexpected Response. Got Content-Type: 'application/taxii+json; " "version=2.1' for Accept: 'application/taxii+json; version=2.1; " - "charset=utf-8'") == str(excinfo.value) + "charset=utf-8'") in str(excinfo.value) + + +@responses.activate +def test_invalid_accept_for_connection(): + responses.add(responses.GET, COLLECTION_URL, COLLECTIONS_RESPONSE, + status=406, content_type=MEDIA_TYPE_TAXII_V21) + + with pytest.raises(requests.exceptions.HTTPError): + conn = _HTTPConnection(user="foo", password="bar", verify=False) + conn.get("https://example.com/api1/collections/91a7b528-80eb-42ed-a74d-c6fbd5a26116/", + headers={"Accept": "application/taxii+json; version=2.1"}) def test_status_missing_id_property(status_dict): diff --git a/taxii2client/v20/__init__.py b/taxii2client/v20/__init__.py index 1df3a2d..5104c9f 100644 --- a/taxii2client/v20/__init__.py +++ b/taxii2client/v20/__init__.py @@ -1,13 +1,13 @@ """Python TAXII 2.0 Client API""" - from __future__ import unicode_literals import json import logging import time +import requests.exceptions import six -import six.moves.urllib.parse as urlparse +from six.moves.urllib import parse as urlparse from .. import MEDIA_TYPE_STIX_V20, MEDIA_TYPE_TAXII_V20 from ..common import ( @@ -18,24 +18,24 @@ # Module-level logger log = logging.getLogger(__name__) -log.propagate = False - -formatter = logging.Formatter("[%(name)s] [%(levelname)s] [%(asctime)s] %(message)s") - -# Console Handler for taxii2client messages -ch = logging.StreamHandler() -ch.setFormatter(formatter) -log.addHandler(ch) def as_pages(func, start=0, per_request=0, *args, **kwargs): - """Creates a generator for TAXII 2.0 endpoints that support pagination.""" + """Creates a generator for TAXII 2.0 endpoints that support pagination. + Args: + func (callable): A v20 function call that supports paged requests. + Currently Get Objects and Get Manifest. + start (int): The starting point for the page request. Default 0. + per_request (int): How many items per request. Default 0. + + Use args or kwargs to pass filter information or other arguments required to make the call. + """ resp = func(start=start, per_request=per_request, *args, **kwargs) yield _to_json(resp) total_obtained, total_available = _grab_total_items(resp) - if total_obtained != per_request: - log.warning("TAXII Server response with different amount of objects! Setting per_request=%s", total_obtained) + if total_available > per_request and total_obtained != per_request: + log.warning("TAXII Server Response with different amount of objects! Setting per_request=%s", total_obtained) per_request = total_obtained start += per_request @@ -62,7 +62,7 @@ class Status(_TAXIIEndpoint): # aren't other endpoints to call on the Status object. def __init__(self, url, conn=None, user=None, password=None, verify=True, - proxies=None, status_info=None): + proxies=None, status_info=None, auth=None): """Create an API root resource endpoint. Args: @@ -79,7 +79,7 @@ def __init__(self, url, conn=None, user=None, password=None, verify=True, (optional) """ - super(Status, self).__init__(url, conn, user, password, verify, proxies) + super(Status, self).__init__(url, conn, user, password, verify, proxies, auth=auth) self.__raw = None if status_info: self._populate_fields(**status_info) @@ -173,19 +173,19 @@ def _validate_status(self): msg = "No 'pending_count' in Status for request '{}'" raise ValidationError(msg.format(self.url)) - if len(self.successes) != self.success_count: + if self.successes and len(self.successes) != self.success_count: msg = "Found successes={}, but success_count={} in status '{}'" raise ValidationError(msg.format(self.successes, self.success_count, self.id)) - if len(self.pendings) != self.pending_count: + if self.pendings and len(self.pendings) != self.pending_count: msg = "Found pendings={}, but pending_count={} in status '{}'" raise ValidationError(msg.format(self.pendings, self.pending_count, self.id)) - if len(self.failures) != self.failure_count: + if self.failures and len(self.failures) != self.failure_count: msg = "Found failures={}, but failure_count={} in status '{}'" raise ValidationError(msg.format(self.failures, self.failure_count, @@ -223,7 +223,7 @@ class Collection(_TAXIIEndpoint): """ def __init__(self, url, conn=None, user=None, password=None, verify=True, - proxies=None, collection_info=None): + proxies=None, collection_info=None, auth=None): """ Initialize a new Collection. Either user/password or conn may be given, but not both. The latter is intended for internal use, when @@ -247,7 +247,7 @@ def __init__(self, url, conn=None, user=None, password=None, verify=True, """ - super(Collection, self).__init__(url, conn, user, password, verify, proxies) + super(Collection, self).__init__(url, conn, user, password, verify, proxies, auth=auth) self._loaded = False self.__raw = None @@ -376,9 +376,19 @@ def get_objects(self, accept=MEDIA_TYPE_STIX_V20, start=0, per_request=0, **filt headers = {"Accept": accept} if per_request > 0: - headers["Range"] = "items {}-{}".format(start, (start + per_request) - 1) - - return self._conn.get(self.objects_url, headers=headers, params=query_params) + headers["Range"] = "items={}-{}".format(start, (start + per_request) - 1) + + try: + response = self._conn.get(self.objects_url, headers=headers, params=query_params) + except requests.exceptions.HTTPError as e: + if per_request > 0: + # This is believed to be an error in TAXII 2.0 + # http://docs.oasis-open.org/cti/taxii/v2.0/cs01/taxii-v2.0-cs01.html#_Toc496542716 + headers["Range"] = "items {}-{}".format(start, (start + per_request) - 1) + response = self._conn.get(self.objects_url, headers=headers, params=query_params) + else: + raise e + return response def get_object(self, obj_id, version=None, accept=MEDIA_TYPE_STIX_V20): """Implement the ``Get an Object`` endpoint (section 5.5)""" @@ -467,15 +477,26 @@ def add_objects(self, bundle, wait_for_completion=True, poll_interval=1, return status def get_manifest(self, accept=MEDIA_TYPE_TAXII_V20, start=0, per_request=0, **filter_kwargs): - """Implement the ``Get Object Manifests`` endpoint (section 5.6). For pagination requests use ``as_pages`` method.""" + """Implement the ``Get Object Manifests`` endpoint (section 5.6). + For pagination requests use ``as_pages`` method.""" self._verify_can_read() query_params = _filter_kwargs_to_query_params(filter_kwargs) headers = {"Accept": accept} if per_request > 0: - headers["Range"] = "items {}-{}".format(start, (start + per_request) - 1) + headers["Range"] = "items={}-{}".format(start, (start + per_request) - 1) - return self._conn.get(self.manifest_url, headers=headers, params=query_params) + try: + response = self._conn.get(self.manifest_url, headers=headers, params=query_params) + except requests.exceptions.HTTPError as e: + if per_request > 0: + # This is believed to be an error in TAXII 2.0 + # http://docs.oasis-open.org/cti/taxii/v2.0/cs01/taxii-v2.0-cs01.html#_Toc496542716 + headers["Range"] = "items {}-{}".format(start, (start + per_request) - 1) + response = self._conn.get(self.manifest_url, headers=headers, params=query_params) + else: + raise e + return response class ApiRoot(_TAXIIEndpoint): @@ -496,7 +517,7 @@ class ApiRoot(_TAXIIEndpoint): """ def __init__(self, url, conn=None, user=None, password=None, verify=True, - proxies=None): + proxies=None, auth=None): """Create an API root resource endpoint. Args: @@ -510,7 +531,7 @@ def __init__(self, url, conn=None, user=None, password=None, verify=True, (optional) """ - super(ApiRoot, self).__init__(url, conn, user, password, verify, proxies) + super(ApiRoot, self).__init__(url, conn, user, password, verify, proxies, auth=auth) self._loaded_collections = False self._loaded_information = False @@ -639,7 +660,7 @@ class Server(_TAXIIEndpoint): """ def __init__(self, url, conn=None, user=None, password=None, verify=True, - proxies=None): + proxies=None, auth=None): """Create a server discovery endpoint. Args: @@ -653,7 +674,7 @@ def __init__(self, url, conn=None, user=None, password=None, verify=True, (optional) """ - super(Server, self).__init__(url, conn, user, password, verify, proxies) + super(Server, self).__init__(url, conn, user, password, verify, proxies, auth=auth) self._user = user self._password = password @@ -661,6 +682,7 @@ def __init__(self, url, conn=None, user=None, password=None, verify=True, self._proxies = proxies self._loaded = False self.__raw = None + self._auth = auth @property def title(self): @@ -719,7 +741,8 @@ def _populate_fields(self, title=None, description=None, contact=None, user=self._user, password=self._password, verify=self._verify, - proxies=self._proxies) + proxies=self._proxies, + auth=self._auth) for url in roots] # If 'default' is one of the existing API Roots, reuse that object # rather than creating a duplicate. The TAXII 2.0 spec says that the diff --git a/taxii2client/v21/__init__.py b/taxii2client/v21/__init__.py index 0be971b..c6648bc 100644 --- a/taxii2client/v21/__init__.py +++ b/taxii2client/v21/__init__.py @@ -1,12 +1,11 @@ """Python TAXII 2.1 Client API""" - from __future__ import unicode_literals import json import time import six -import six.moves.urllib.parse as urlparse +from six.moves.urllib import parse as urlparse from .. import MEDIA_TYPE_TAXII_V21 from ..common import _filter_kwargs_to_query_params, _TAXIIEndpoint @@ -26,7 +25,7 @@ class Status(_TAXIIEndpoint): # aren't other endpoints to call on the Status object. def __init__(self, url, conn=None, user=None, password=None, verify=True, - proxies=None, status_info=None): + proxies=None, status_info=None, auth=None): """Create an API root resource endpoint. Args: @@ -43,7 +42,7 @@ def __init__(self, url, conn=None, user=None, password=None, verify=True, (optional) """ - super(Status, self).__init__(url, conn, user, password, verify, proxies, "2.1") + super(Status, self).__init__(url, conn, user, password, verify, proxies, "2.1", auth=auth) self.__raw = None if status_info: self._populate_fields(**status_info) @@ -136,19 +135,19 @@ def _validate_status(self): msg = "No 'pending_count' in Status for request '{}'" raise ValidationError(msg.format(self.url)) - if len(self.successes) != self.success_count: + if self.successes and len(self.successes) != self.success_count: msg = "Found successes={}, but success_count={} in status '{}'" raise ValidationError(msg.format(self.successes, self.success_count, self.id)) - if len(self.pendings) != self.pending_count: + if self.pendings and len(self.pendings) != self.pending_count: msg = "Found pendings={}, but pending_count={} in status '{}'" raise ValidationError(msg.format(self.pendings, self.pending_count, self.id)) - if len(self.failures) != self.failure_count: + if self.failures and len(self.failures) != self.failure_count: msg = "Found failures={}, but failure_count={} in status '{}'" raise ValidationError(msg.format(self.failures, self.failure_count, @@ -186,7 +185,7 @@ class Collection(_TAXIIEndpoint): """ def __init__(self, url, conn=None, user=None, password=None, verify=True, - proxies=None, collection_info=None): + proxies=None, collection_info=None, auth=None): """ Initialize a new Collection. Either user/password or conn may be given, but not both. The latter is intended for internal use, when @@ -210,7 +209,7 @@ def __init__(self, url, conn=None, user=None, password=None, verify=True, """ - super(Collection, self).__init__(url, conn, user, password, verify, proxies, "2.1") + super(Collection, self).__init__(url, conn, user, password, verify, proxies, "2.1", auth=auth) self._loaded = False self.__raw = None @@ -461,7 +460,7 @@ class ApiRoot(_TAXIIEndpoint): """ def __init__(self, url, conn=None, user=None, password=None, verify=True, - proxies=None): + proxies=None, auth=None): """Create an API root resource endpoint. Args: @@ -475,7 +474,7 @@ def __init__(self, url, conn=None, user=None, password=None, verify=True, (optional) """ - super(ApiRoot, self).__init__(url, conn, user, password, verify, proxies, "2.1") + super(ApiRoot, self).__init__(url, conn, user, password, verify, proxies, "2.1", auth=auth) self._loaded_collections = False self._loaded_information = False @@ -604,7 +603,7 @@ class Server(_TAXIIEndpoint): """ def __init__(self, url, conn=None, user=None, password=None, verify=True, - proxies=None): + proxies=None, auth=None): """Create a server discovery endpoint. Args: @@ -618,7 +617,7 @@ def __init__(self, url, conn=None, user=None, password=None, verify=True, (optional) """ - super(Server, self).__init__(url, conn, user, password, verify, proxies, "2.1") + super(Server, self).__init__(url, conn, user, password, verify, proxies, "2.1", auth=auth) self._user = user self._password = password @@ -626,6 +625,7 @@ def __init__(self, url, conn=None, user=None, password=None, verify=True, self._proxies = proxies self._loaded = False self.__raw = None + self._auth = auth @property def title(self): @@ -685,7 +685,8 @@ def _populate_fields(self, title=None, description=None, contact=None, user=self._user, password=self._password, verify=self._verify, - proxies=self._proxies) + proxies=self._proxies, + auth=self._auth) for url in roots ] # If 'default' is one of the existing API Roots, reuse that object diff --git a/taxii2client/version.py b/taxii2client/version.py index 8c0d5d5..ba51ced 100644 --- a/taxii2client/version.py +++ b/taxii2client/version.py @@ -1 +1 @@ -__version__ = "2.0.0" +__version__ = "2.2.2" diff --git a/tox.ini b/tox.ini index 6243a49..8100a6b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py34,py35,py36,py37,py38,pycodestyle,isort-check,packaging +envlist = py35,py36,py37,py38,pycodestyle,isort-check,packaging [testenv] deps = @@ -34,8 +34,8 @@ exclude=taxii2client/__init__.py [testenv:isort-check] deps = isort commands = - isort -rc taxii2client -df - isort -rc taxii2client -c + isort taxii2client --df + isort taxii2client -c [testenv:packaging] deps = @@ -45,9 +45,7 @@ commands = [travis] python = - 2.7: py27, pycodestyle, packaging - 3.4: py34, pycodestyle - 3.5: py35, pycodestyle - 3.6: py36, pycodestyle, isort-check, packaging - 3.7: py37, pycodestyle - 3.8: py38, pycodestyle + 3.5: py35 + 3.6: py36 + 3.7: py37 + 3.8: py38, pycodestyle, isort-check, packaging