From 7f33dcc7df8de9a2392e63af27166316f1290772 Mon Sep 17 00:00:00 2001 From: Marc Aubry Date: Mon, 22 Aug 2016 16:43:23 -0400 Subject: [PATCH] Add compatibility to python3 Change-Id: I20251e3dbe495c60a2a17751d84a395d10d38817 --- Dockerfile | 4 +-- almanach/adapters/api_route_v1.py | 35 ++++++++++--------- almanach/adapters/bus_adapter.py | 12 ++++--- almanach/adapters/database_adapter.py | 4 --- almanach/adapters/retry_adapter.py | 5 +-- almanach/auth/keystone_auth.py | 2 +- .../common/exceptions/almanach_exception.py | 3 +- almanach/config.py | 9 +++-- almanach/core/model.py | 31 ++++++++++++++-- almanach/validators/instance_validator.py | 11 +++--- integration_tests/builders/messages.py | 4 +-- integration_tests/test_api_instance_entity.py | 4 ++- .../test_metadata_instance_create.py | 29 +++++++++++++++ requirements.txt | 3 +- tests/adapters/test_bus_adapter.py | 6 ++-- tests/adapters/test_retry_adapter.py | 34 +++++++++--------- tests/api/base_api.py | 3 +- tests/api/test_api_entity.py | 2 +- tests/api/test_api_instance.py | 4 +-- tox.ini | 2 +- 20 files changed, 137 insertions(+), 70 deletions(-) create mode 100644 integration_tests/test_metadata_instance_create.py diff --git a/Dockerfile b/Dockerfile index 2c8e099..914ecb4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -FROM python:2.7 +FROM python:3.4 RUN mkdir -p /opt/almanach/src ADD almanach /opt/almanach/src/almanach ADD setup.* /opt/almanach/src/ -ADD README.md /opt/almanach/src/ +ADD README.rst /opt/almanach/src/ ADD requirements.txt /opt/almanach/src/ ADD LICENSE /opt/almanach/src/ ADD almanach/resources/config/almanach.cfg /etc/almanach.cfg diff --git a/almanach/adapters/api_route_v1.py b/almanach/adapters/api_route_v1.py index dc69f3e..2a68d7a 100644 --- a/almanach/adapters/api_route_v1.py +++ b/almanach/adapters/api_route_v1.py @@ -13,16 +13,17 @@ # limitations under the License. import logging -import json from datetime import datetime from functools import wraps import jsonpickle from flask import Blueprint, Response, request +from oslo_serialization import jsonutils from werkzeug.wrappers import BaseResponse +from almanach.common.exceptions.almanach_exception import AlmanachException from almanach.common.exceptions.almanach_entity_not_found_exception import AlmanachEntityNotFoundException from almanach.common.exceptions.authentication_failure_exception import AuthenticationFailureException from almanach.common.exceptions.multiple_entities_matching_query import MultipleEntitiesMatchingQuery @@ -48,7 +49,7 @@ def to_json(api_call): logging.warning(e.message) return Response(encode({"error": e.message}), 400, {"Content-Type": "application/json"}) except KeyError as e: - message = "The '{param}' param is mandatory for the request you have made.".format(param=e.message) + message = "The {param} param is mandatory for the request you have made.".format(param=e) logging.warning(message) return encode({"error": message}), 400, {"Content-Type": "application/json"} except TypeError: @@ -65,10 +66,12 @@ def to_json(api_call): except AlmanachEntityNotFoundException as e: logging.warning(e.message) return encode({"error": "Entity not found"}), 404, {"Content-Type": "application/json"} - - except Exception as e: + except AlmanachException as e: logging.exception(e) return Response(encode({"error": e.message}), 500, {"Content-Type": "application/json"}) + except Exception as e: + logging.exception(e) + return Response(encode({"error": e}), 500, {"Content-Type": "application/json"}) return decorator @@ -80,7 +83,7 @@ def authenticated(api_call): auth_adapter.validate(request.headers.get('X-Auth-Token')) return api_call(*args, **kwargs) except AuthenticationFailureException as e: - logging.error("Authentication failure: {0}".format(e.message)) + logging.error("Authentication failure: {0}".format(e)) return Response('Unauthorized', 401) return decorator @@ -97,7 +100,7 @@ def get_info(): @authenticated @to_json def create_instance(project_id): - instance = json.loads(request.data) + instance = jsonutils.loads(request.data) logging.info("Creating instance for tenant %s with data %s", project_id, instance) controller.create_instance( tenant_id=project_id, @@ -118,7 +121,7 @@ def create_instance(project_id): @authenticated @to_json def delete_instance(instance_id): - data = json.loads(request.data) + data = jsonutils.loads(request.data) logging.info("Deleting instance with id %s with data %s", instance_id, data) controller.delete_instance( instance_id=instance_id, @@ -132,7 +135,7 @@ def delete_instance(instance_id): @authenticated @to_json def resize_instance(instance_id): - instance = json.loads(request.data) + instance = jsonutils.loads(request.data) logging.info("Resizing instance with id %s with data %s", instance_id, instance) controller.resize_instance( instance_id=instance_id, @@ -147,7 +150,7 @@ def resize_instance(instance_id): @authenticated @to_json def rebuild_instance(instance_id): - instance = json.loads(request.data) + instance = jsonutils.loads(request.data) logging.info("Rebuilding instance with id %s with data %s", instance_id, instance) controller.rebuild_instance( instance_id=instance_id, @@ -173,7 +176,7 @@ def list_instances(project_id): @authenticated @to_json def create_volume(project_id): - volume = json.loads(request.data) + volume = jsonutils.loads(request.data) logging.info("Creating volume for tenant %s with data %s", project_id, volume) controller.create_volume( project_id=project_id, @@ -192,7 +195,7 @@ def create_volume(project_id): @authenticated @to_json def delete_volume(volume_id): - data = json.loads(request.data) + data = jsonutils.loads(request.data) logging.info("Deleting volume with id %s with data %s", volume_id, data) controller.delete_volume( volume_id=volume_id, @@ -206,7 +209,7 @@ def delete_volume(volume_id): @authenticated @to_json def resize_volume(volume_id): - volume = json.loads(request.data) + volume = jsonutils.loads(request.data) logging.info("Resizing volume with id %s with data %s", volume_id, volume) controller.resize_volume( volume_id=volume_id, @@ -221,7 +224,7 @@ def resize_volume(volume_id): @authenticated @to_json def attach_volume(volume_id): - volume = json.loads(request.data) + volume = jsonutils.loads(request.data) logging.info("Attaching volume with id %s with data %s", volume_id, volume) controller.attach_volume( volume_id=volume_id, @@ -236,7 +239,7 @@ def attach_volume(volume_id): @authenticated @to_json def detach_volume(volume_id): - volume = json.loads(request.data) + volume = jsonutils.loads(request.data) logging.info("Detaching volume with id %s with data %s", volume_id, volume) controller.detach_volume( volume_id=volume_id, @@ -269,7 +272,7 @@ def list_entity(project_id): @authenticated @to_json def update_instance_entity(instance_id): - data = json.loads(request.data) + data = jsonutils.loads(request.data) logging.info("Updating instance entity with id %s with data %s", instance_id, data) if 'start' in request.args: start, end = get_period() @@ -316,7 +319,7 @@ def get_volume_type(type_id): @authenticated @to_json def create_volume_type(): - volume_type = json.loads(request.data) + volume_type = jsonutils.loads(request.data) logging.info("Creating volume type with data '%s'", volume_type) controller.create_volume_type( volume_type_id=volume_type['type_id'], diff --git a/almanach/adapters/bus_adapter.py b/almanach/adapters/bus_adapter.py index 85fe3a9..1245e4e 100644 --- a/almanach/adapters/bus_adapter.py +++ b/almanach/adapters/bus_adapter.py @@ -12,11 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json import logging import kombu +import six from kombu.mixins import ConsumerMixin +from oslo_serialization import jsonutils + from almanach import config from almanach.adapters.instance_bus_adapter import InstanceBusAdapter from almanach.adapters.volume_bus_adapter import VolumeBusAdapter @@ -34,14 +36,14 @@ class BusAdapter(ConsumerMixin): try: self._process_notification(notification) except Exception as e: - logging.warning("Sending notification to retry letter exchange {0}".format(json.dumps(notification))) - logging.exception(e.message) + logging.warning("Sending notification to retry letter exchange {0}".format(jsonutils.dumps(notification))) + logging.exception(e) self.retry_adapter.publish_to_dead_letter(message) message.ack() def _process_notification(self, notification): - if isinstance(notification, basestring): - notification = json.loads(notification) + if isinstance(notification, six.string_types): + notification = jsonutils.loads(notification) event_type = notification.get("event_type") logging.info("Received event: '{0}'".format(event_type)) diff --git a/almanach/adapters/database_adapter.py b/almanach/adapters/database_adapter.py index 9fc6006..0fc221f 100644 --- a/almanach/adapters/database_adapter.py +++ b/almanach/adapters/database_adapter.py @@ -16,7 +16,6 @@ import logging import pymongo from pymongo.errors import ConfigurationError -from pymongomodem.utils import decode_output, encode_input from almanach import config from almanach.common.exceptions.almanach_exception import AlmanachException @@ -161,14 +160,11 @@ class DatabaseAdapter(object): def delete_active_entity(self, entity_id): self.db.entity.remove({"entity_id": entity_id, "end": None}) - @encode_input def _insert_entity(self, entity): self.db.entity.insert(entity) - @decode_output def _get_entities_from_db(self, args): return list(self.db.entity.find(args, {"_id": 0})) - @decode_output def _get_one_entity_from_db(self, args): return self.db.entity.find_one(args, {"_id": 0}) diff --git a/almanach/adapters/retry_adapter.py b/almanach/adapters/retry_adapter.py index e57e5aa..23ce81a 100644 --- a/almanach/adapters/retry_adapter.py +++ b/almanach/adapters/retry_adapter.py @@ -12,10 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json import logging from kombu import Exchange, Queue, Producer +from oslo_serialization import jsonutils + from almanach import config @@ -38,7 +39,7 @@ class RetryAdapter: else: logging.info("Publishing to dead letter queue") self._publish_message(self._dead_producer, message) - logging.info("Publishing notification to dead letter queue: {0}".format(json.dumps(message.body))) + logging.info("Publishing notification to dead letter queue: {0}".format(jsonutils.dumps(message.body))) def _configure_retry_exchanges(self, connection): def declare_queues(): diff --git a/almanach/auth/keystone_auth.py b/almanach/auth/keystone_auth.py index 5d24a5a..1acb381 100644 --- a/almanach/auth/keystone_auth.py +++ b/almanach/auth/keystone_auth.py @@ -46,6 +46,6 @@ class KeystoneAuthentication(BaseAuth): try: self.token_manager_factory.get_manager().validate(token) except Exception as e: - raise AuthenticationFailureException(e.message) + raise AuthenticationFailureException(e) return True diff --git a/almanach/common/exceptions/almanach_exception.py b/almanach/common/exceptions/almanach_exception.py index ac3ec3d..c54ed16 100644 --- a/almanach/common/exceptions/almanach_exception.py +++ b/almanach/common/exceptions/almanach_exception.py @@ -14,4 +14,5 @@ class AlmanachException(Exception): - pass + def __init__(self, message=None): + self.message = message diff --git a/almanach/config.py b/almanach/config.py index 97307f5..5643c64 100644 --- a/almanach/config.py +++ b/almanach/config.py @@ -12,13 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -import ConfigParser import os import os.path as os_path +import six from almanach.common.exceptions.almanach_exception import AlmanachException -configuration = ConfigParser.RawConfigParser() +if six.PY2: + from ConfigParser import RawConfigParser +else: + from configparser import RawConfigParser + +configuration = RawConfigParser() def read(filename): diff --git a/almanach/core/model.py b/almanach/core/model.py index f199393..99df8b5 100644 --- a/almanach/core/model.py +++ b/almanach/core/model.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import six class Entity(object): @@ -45,6 +46,10 @@ class Instance(Entity): self.metadata = metadata self.os = OS(**os) + def as_dict(self): + _replace_metadata_name_with_dot_instead_of_circumflex(self) + return todict(self) + def __eq__(self, other): return (super(Instance, self).__eq__(other) and other.flavor == self.flavor and @@ -95,6 +100,7 @@ class VolumeType(object): def build_entity_from_dict(entity_dict): if entity_dict.get("entity_type") == Instance.TYPE: + _replace_metadata_name_with_circumflex_instead_of_dot(entity_dict) return Instance(**entity_dict) elif entity_dict.get("entity_type") == Volume.TYPE: return Volume(**entity_dict) @@ -102,13 +108,34 @@ def build_entity_from_dict(entity_dict): def todict(obj): - if isinstance(obj, dict): + if isinstance(obj, dict) or isinstance(obj, six.text_type): return obj elif hasattr(obj, "__iter__"): return [todict(v) for v in obj] elif hasattr(obj, "__dict__"): return dict([(key, todict(value)) - for key, value in obj.__dict__.iteritems() + for key, value in obj.__dict__.items() if not callable(value) and not key.startswith('_')]) else: return obj + + +def _replace_metadata_name_with_dot_instead_of_circumflex(instance): + if instance.metadata: + cleaned_metadata = dict() + for key, value in instance.metadata.items(): + if '.' in key: + key = key.replace(".", "^") + cleaned_metadata[key] = value + instance.metadata = cleaned_metadata + + +def _replace_metadata_name_with_circumflex_instead_of_dot(entity_dict): + metadata = entity_dict.get("metadata") + if metadata: + dirty_metadata = dict() + for key, value in metadata.items(): + if '^' in key: + key = key.replace("^", ".") + dirty_metadata[key] = value + entity_dict["metadata"] = dirty_metadata diff --git a/almanach/validators/instance_validator.py b/almanach/validators/instance_validator.py index c39c82f..fc1500a 100644 --- a/almanach/validators/instance_validator.py +++ b/almanach/validators/instance_validator.py @@ -1,3 +1,4 @@ +import six from voluptuous import Schema, MultipleInvalid, Datetime, Required from almanach.common.exceptions.validation_exception import InvalidAttributeException @@ -6,12 +7,12 @@ from almanach.common.exceptions.validation_exception import InvalidAttributeExce class InstanceValidator(object): def __init__(self): self.schema = Schema({ - 'name': unicode, - 'flavor': unicode, + 'name': six.text_type, + 'flavor': six.text_type, 'os': { - Required('distro'): unicode, - Required('version'): unicode, - Required('os_type'): unicode, + Required('distro'): six.text_type, + Required('version'): six.text_type, + Required('os_type'): six.text_type, }, 'metadata': dict, 'start_date': Datetime(), diff --git a/integration_tests/builders/messages.py b/integration_tests/builders/messages.py index 870ea20..0155f09 100644 --- a/integration_tests/builders/messages.py +++ b/integration_tests/builders/messages.py @@ -33,7 +33,7 @@ def get_instance_create_end_sample(instance_id=None, tenant_id=None, flavor_name "os_version": os_version or "6.4", "created_at": creation_timestamp if creation_timestamp else datetime(2014, 2, 14, 16, 29, 58, tzinfo=pytz.utc), "launched_at": creation_timestamp + timedelta(seconds=1) if creation_timestamp else datetime(2014, 2, 14, 16, - 30, 02, + 30, 2, tzinfo=pytz.utc), "terminated_at": None, "deleted_at": None, @@ -56,7 +56,7 @@ def get_instance_delete_end_sample(instance_id=None, tenant_id=None, flavor_name "os_version": os_version or "6.4", "created_at": creation_timestamp if creation_timestamp else datetime(2014, 2, 14, 16, 29, 58, tzinfo=pytz.utc), "launched_at": creation_timestamp + timedelta(seconds=1) if creation_timestamp else datetime(2014, 2, 14, 16, - 30, 02, + 30, 2, tzinfo=pytz.utc), "terminated_at": deletion_timestamp if deletion_timestamp else datetime(2014, 2, 18, 12, 5, 23, tzinfo=pytz.utc), diff --git a/integration_tests/test_api_instance_entity.py b/integration_tests/test_api_instance_entity.py index a8b8498..c4457d9 100644 --- a/integration_tests/test_api_instance_entity.py +++ b/integration_tests/test_api_instance_entity.py @@ -41,7 +41,9 @@ class ApiInstanceEntityTest(BaseApiTestCase): ) assert_that(response.status_code, equal_to(400)) - assert_that(response.json(), equal_to({"error": {"flavor": "expected unicode", "os": "expected a dictionary"}})) + error_dict = response.json()['error'] + assert_that(len(error_dict), equal_to(2)) + assert_that(sorted(error_dict.keys()), equal_to(["flavor", "os"])) def test_update_entity_instance_with_one_attribute(self): instance_id = self._create_instance_entity() diff --git a/integration_tests/test_metadata_instance_create.py b/integration_tests/test_metadata_instance_create.py new file mode 100644 index 0000000..2ebca45 --- /dev/null +++ b/integration_tests/test_metadata_instance_create.py @@ -0,0 +1,29 @@ +from uuid import uuid4 +from datetime import datetime +from hamcrest import assert_that, has_entry +from hamcrest import equal_to +from integration_tests.base_api_testcase import BaseApiTestCase +from integration_tests.builders.messages import get_instance_create_end_sample +import pytz + + +class MetadataInstanceCreateTest(BaseApiTestCase): + def test_instance_create_with_metadata(self): + instance_id = str(uuid4()) + tenant_id = str(uuid4()) + + self.rabbitMqHelper.push( + get_instance_create_end_sample( + instance_id=instance_id, + tenant_id=tenant_id, + creation_timestamp=datetime(2016, 2, 1, 9, 0, 0, tzinfo=pytz.utc), + metadata={"metering.billing_mode": "42"} + )) + + self.assert_that_instance_entity_is_created_and_have_proper_metadata(instance_id, tenant_id) + + def assert_that_instance_entity_is_created_and_have_proper_metadata(self, instance_id, tenant_id): + entities = self.almanachHelper.get_entities(tenant_id, "2016-01-01 00:00:00.000") + assert_that(len(entities), equal_to(1)) + assert_that(entities[0], has_entry("entity_id", instance_id)) + assert_that(entities[0], has_entry("metadata", {'metering.billing_mode': '42'})) diff --git a/requirements.txt b/requirements.txt index fc12f62..b98554f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,8 @@ jsonpickle==0.7.1 pymongo==2.7.2 kombu>=3.0.30 python-dateutil==2.2 -python-pymongomodem==0.0.3 pytz>=2014.10 voluptuous==0.8.11 python-keystoneclient>=1.6.0 +six>=1.9.0 # MIT +oslo.serialization>=1.10.0 # Apache-2.0 \ No newline at end of file diff --git a/tests/adapters/test_bus_adapter.py b/tests/adapters/test_bus_adapter.py index 2a36e4c..19f0997 100644 --- a/tests/adapters/test_bus_adapter.py +++ b/tests/adapters/test_bus_adapter.py @@ -36,7 +36,7 @@ class BusAdapterTest(unittest.TestCase): instance_id = "e7d44dea-21c1-452c-b50c-cbab0d07d7d3" tenant_id = "0be9215b503b43279ae585d50a33aed8" instance_type = "myflavor" - timestamp = datetime(2014, 02, 14, 16, 30, 10, tzinfo=pytz.utc) + timestamp = datetime(2014, 2, 14, 16, 30, 10, tzinfo=pytz.utc) hostname = "some hostname" metadata = {"a_metadata.to_filter": "filtered_value", } @@ -72,7 +72,7 @@ class BusAdapterTest(unittest.TestCase): instance_id = "e7d44dea-21c1-452c-b50c-cbab0d07d7d3" tenant_id = "0be9215b503b43279ae585d50a33aed8" instance_type = "myflavor" - timestamp = datetime(2014, 02, 14, 16, 30, 10, tzinfo=pytz.utc) + timestamp = datetime(2014, 2, 14, 16, 30, 10, tzinfo=pytz.utc) hostname = "some hostname" notification = messages.get_instance_create_end_sample(instance_id=instance_id, tenant_id=tenant_id, @@ -173,7 +173,7 @@ class BusAdapterTest(unittest.TestCase): def test_on_message_with_volume(self): volume_id = "vol_id" tenant_id = "tenant_id" - timestamp_datetime = datetime(2014, 02, 14, 16, 30, 10, tzinfo=pytz.utc) + timestamp_datetime = datetime(2014, 2, 14, 16, 30, 10, tzinfo=pytz.utc) volume_type = "SF400" volume_size = 100000 some_volume = "volume_name" diff --git a/tests/adapters/test_retry_adapter.py b/tests/adapters/test_retry_adapter.py index 8b40621..08c2f6d 100644 --- a/tests/adapters/test_retry_adapter.py +++ b/tests/adapters/test_retry_adapter.py @@ -61,12 +61,7 @@ class BusAdapterTest(unittest.TestCase): self.retry_adapter = RetryAdapter(connection) def test_publish_to_retry_queue_happy_path(self): - message = MyObject - message.headers = [] - message.body = 'omnomnom' - message.delivery_info = {'routing_key': 42} - message.content_type = 'xml/rapture' - message.content_encoding = 'iso8859-1' + message = self.build_message() self.config_mock.should_receive('rabbitmq_retry').and_return(1) self.expect_publish_with(message, 'almanach.retry').once() @@ -74,12 +69,7 @@ class BusAdapterTest(unittest.TestCase): self.retry_adapter.publish_to_dead_letter(message) def test_publish_to_retry_queue_retries_if_it_fails(self): - message = MyObject - message.headers = {} - message.body = 'omnomnom' - message.delivery_info = {'routing_key': 42} - message.content_type = 'xml/rapture' - message.content_encoding = 'iso8859-1' + message = self.build_message() self.config_mock.should_receive('rabbitmq_retry').and_return(2) self.expect_publish_with(message, 'almanach.retry').times(4)\ @@ -90,13 +80,17 @@ class BusAdapterTest(unittest.TestCase): self.retry_adapter.publish_to_dead_letter(message) - def test_publish_to_dead_letter_messages_retried_more_than_twice(self): - message = MyObject - message.headers = {'x-death': [0, 1, 2, 3]} - message.body = 'omnomnom' - message.delivery_info = {'routing_key': ''} + def build_message(self, headers=dict()): + message = MyObject() + message.headers = headers + message.body = b'Now that the worst is behind you, it\'s time we get you back. - Mr. Robot' + message.delivery_info = {'routing_key': 42} message.content_type = 'xml/rapture' message.content_encoding = 'iso8859-1' + return message + + def test_publish_to_dead_letter_messages_retried_more_than_twice(self): + message = self.build_message(headers={'x-death': [0, 1, 2, 3]}) self.config_mock.should_receive('rabbitmq_retry').and_return(2) self.expect_publish_with(message, 'almanach.dead').once() @@ -117,4 +111,8 @@ class BusAdapterTest(unittest.TestCase): class MyObject(object): - pass + headers = None + body = None + delivery_info = None + content_type = None + content_encoding = None diff --git a/tests/api/base_api.py b/tests/api/base_api.py index 590da07..736368b 100644 --- a/tests/api/base_api.py +++ b/tests/api/base_api.py @@ -18,6 +18,7 @@ import flask from unittest import TestCase from datetime import datetime from flexmock import flexmock, flexmock_teardown +import oslo_serialization from almanach import config from almanach.adapters import api_route_v1 as api_route @@ -76,7 +77,7 @@ class BaseApi(TestCase): headers = {} headers['Accept'] = accept result = getattr(http_client, method)(url, data=json.dumps(data), query_string=query_string, headers=headers) - return_data = json.loads(result.data) \ + return_data = oslo_serialization.jsonutils.loads(result.data) \ if result.headers.get('Content-Type') == 'application/json' \ else result.data return result.status_code, return_data diff --git a/tests/api/test_api_entity.py b/tests/api/test_api_entity.py index 96809ee..cf87369 100644 --- a/tests/api/test_api_entity.py +++ b/tests/api/test_api_entity.py @@ -31,7 +31,7 @@ class ApiEntityTest(BaseApi): .with_args( instance_id="INSTANCE_ID", start_date=data["start_date"], - ).and_return(a(instance().with_id('INSTANCE_ID').with_start(2014, 01, 01, 00, 0, 00))) + ).and_return(a(instance().with_id('INSTANCE_ID').with_start(2014, 1, 1, 0, 0, 0))) code, result = self.api_put( '/entity/instance/INSTANCE_ID', diff --git a/tests/api/test_api_instance.py b/tests/api/test_api_instance.py index 4ad42ed..6b8d44c 100644 --- a/tests/api/test_api_instance.py +++ b/tests/api/test_api_instance.py @@ -54,8 +54,8 @@ class ApiInstanceTest(BaseApi): ).and_return(a( instance(). with_id('INSTANCE_ID'). - with_start(2016, 03, 01, 00, 0, 00). - with_end(2016, 03, 03, 00, 0, 00). + with_start(2016, 3, 1, 0, 0, 0). + with_end(2016, 3, 3, 0, 0, 0). with_flavor(some_new_flavor)) ) diff --git a/tox.ini b/tox.ini index 218d8e7..2bfd063 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,pep8 +envlist = py27,py34,pep8 [testenv] deps =