Repository: MediaMath/t1-python

Issue:
Impression Pacing New Fields
**Handling of Posts With ONLY Old Paradigm Fields**
We have introduced logic in our API such that new posts to Old Paradigm fields are correctly translated into their New Paradigm counterparts, provided that the posts do not also include New Paradigm fields.  The table below outlines the translation.

**Handling of Posts With ONLY New Paradigm Fields**
Posts to the New Paradigm fields that do not include any Old Paradigm fields will be accepted normally.

**Handling of Posts With Both New AND Old Paradigm Fields**
We strongly advise against submitting posts that include both New and Old Paradigm fields because it will be possible to create a conflict between the two Paradigms that will be resolved in a way that may or may not align with the user’s intent.

Currently, new impression pacing fields are not in POST data because they are not in the Strategy model's _pull list.

<img width="637" alt="screen shot 2016-09-21 at 4 55 24 pm" src="https://cloud.githubusercontent.com/assets/4910009/18729102/544200b0-801e-11e6-950f-f5a4eb7584c0.png">


Discussion:

Patch:
diff --git a/terminalone/models/campaign.py b/terminalone/models/campaign.py
index a38d866..41e950c 100644
--- a/terminalone/models/campaign.py
+++ b/terminalone/models/campaign.py
@@ -18,9 +18,9 @@ class Campaign(Entity):
         'advertiser', 'ad_server', 'currency', 'merit_pixel', 'time_zone',
     }
     _conv = t1types.enum({'every', 'one', 'variable'}, 'variable')
-    _freq_ints = t1types.enum({'hour', 'day', 'week', 'month',
-                               'not-applicable'}, 'not-applicable')
-    _freq_types = t1types.enum({'even', 'asap', 'no-limit'}, 'no-limit')
+    _cap_ints = t1types.enum({'hour', 'day', 'week', 'month',
+                              'not-applicable'}, 'not-applicable')
+    _cap_types = t1types.enum({'even', 'asap', 'no-limit'}, 'no-limit')
     _goal_cats = t1types.enum({'audience', 'engagement', 'response'}, None)
     _goal_types = t1types.enum({'spend', 'reach', 'cpc', 'cpe', 'cpa', 'roi'},
                                None)
@@ -47,6 +47,9 @@ class Campaign(Entity):
         'goal_value': float,
         'has_custom_attribution': t1types.int_to_bool,
         'id': int,
+        'impression_cap_amount': int,
+        'impression_cap_automatic': t1types.int_to_bool,
+        'impression_cap_type': None,
         'io_name': None,
         'io_reference_num': None,
         'initial_start_date': t1types.strpt,
@@ -58,12 +61,15 @@ class Campaign(Entity):
         'pv_pct': float,
         'pv_window_minutes': int,
         'service_type': None,
+        'source_campaign_id': int,
         'spend_cap_amount': float,
         'spend_cap_automatic': t1types.int_to_bool,
         'spend_cap_enabled': t1types.int_to_bool,
+        'spend_cap_type': None,
         'start_date': t1types.strpt,
         'status': t1types.int_to_bool,
         'total_budget': float,
+        'total_impression_budget': int,
         'updated_on': t1types.strpt,
         'use_default_ad_server': t1types.int_to_bool,
         'use_mm_freq': t1types.int_to_bool,
@@ -75,15 +81,18 @@ class Campaign(Entity):
         'conversion_type': _conv,
         'dcs_data_is_campaign_level': int,
         'end_date': t1types.strft,
-        'frequency_interval': _freq_ints,
-        'frequency_type': _freq_types,
+        'frequency_interval': _cap_ints,
+        'frequency_type': _cap_types,
         'goal_category': _goal_cats,
         'goal_type': _goal_types,
         'has_custom_attribution': int,
+        'impression_cap_automatic': int,
+        'impression_cap_type': _cap_types,
         'initial_start_date': t1types.strft,
         'service_type': _serv_types,
         'spend_cap_automatic': int,
         'spend_cap_enabled': int,
+        'spend_cap_type': _cap_types,
         'start_date': t1types.strft,
         'status': int,
         'use_default_ad_server': int,
@@ -91,4 +100,51 @@ class Campaign(Entity):
     })
 
     def __init__(self, session, properties=None, **kwargs):
+        if properties is None:
+            # super(Entity) supers to grandparent
+            super(Entity, self).__setattr__('_init_sce', None)
+            super(Entity, self).__setattr__('_init_sct', None)
+        else:
+            super(Entity, self).__setattr__('_init_sce',
+                                            properties.get('spend_cap_enabled'))
+            super(Entity, self).__setattr__('_init_sct',
+                                            properties.get('spend_cap_type'))
         super(Campaign, self).__init__(session, properties, **kwargs)
+
+    def _migration_asst(self):
+        """Helps migrate users to the new impression pacing features.
+
+        spend_cap_enabled is the old field. spend_cap_type is the new field.
+        If the user has changed:
+            - Nothing (final vals all equal): remove both fields
+            - Old (new vals equal): remove new fields, post old
+            - New (old vals equal): remove old fields, post new
+            - Both (no vals equal): UNDEFINED. remove old fields to prep.
+        """
+        i_sce, i_sct = self._init_sce, self._init_sct
+        f_sce, f_sct = (self.properties.get('spend_cap_enabled'),
+                        self.properties.get('spend_cap_type'))
+
+        fields_to_remove = None
+        if i_sce == f_sce and i_sct == f_sct:
+            fields_to_remove = ['spend_cap_enabled', 'spend_cap_type']
+        elif i_sct == f_sct:
+            fields_to_remove = ['spend_cap_type']
+        else:  # we don't need a second elif here because it's the same result
+            fields_to_remove = ['spend_cap_enabled']
+        return fields_to_remove
+
+    def save(self, data=None, url=None):
+        """Save object to T1 while accounting for old fields"""
+        if data is None:
+            data = self.properties.copy()
+
+        fields_to_remove = self._migration_asst()
+        for field in fields_to_remove:
+            data.pop(field, None)
+
+        super(Campaign, self).save(data=data, url=url)
+        # Re-set the fields so that if the same object get saved, we
+        # compare agains the re-initialized values
+        super(Entity, self).__setattr__('_init_sce', self.spend_cap_enabled)
+        super(Entity, self).__setattr__('_init_sct', self.spend_cap_type)
diff --git a/terminalone/models/strategy.py b/terminalone/models/strategy.py
index a9f8edf..0c356ed 100644
--- a/terminalone/models/strategy.py
+++ b/terminalone/models/strategy.py
@@ -20,10 +20,10 @@ class Strategy(Entity):
     _relations = {
         'campaign', 'currency', 'time_zone',
     }
-    _aud_seg_ops = t1types.enum({'AND', 'OR'}, 'OR')
-    _freq_int = t1types.enum({'hour', 'day', 'week', 'month', 'campaign',
-                              'not-applicable'}, 'not-applicable')
-    _freq_type = t1types.enum({'even', 'asap', 'no-limit'}, 'no-limit')
+    _seg_incexc_ops = t1types.enum({'AND', 'OR'}, 'OR')
+    _pacing_ints = t1types.enum({'hour', 'day', 'week', 'month', 'campaign',
+                                'not-applicable'}, 'not-applicable')
+    _pacing_types = t1types.enum({'even', 'asap', 'no-limit'}, 'no-limit')
     _goal_type = t1types.enum({'spend', 'reach', 'cpc', 'cpe', 'cpa', 'roi'},
                               'cpc')
     _media_type = t1types.enum({'DISPLAY', 'VIDEO'}, 'DISPLAY')
@@ -42,18 +42,24 @@ class Strategy(Entity):
         'budget': float,
         'campaign_id': int,
         'created_on': t1types.strpt,
+        'currency_code': None,
         'description': None,
         'effective_goal_value': float,
         'end_date': t1types.strpt,
         'feature_compatibility': None,
         'frequency_amount': int,
         'frequency_interval': None,
+        'frequency_optimization': t1types.int_to_bool,
         'frequency_type': None,
         'goal_type': None,
         'goal_value': float,
         'id': int,
         'impression_cap': int,
+        'impression_pacing_amount': int,
+        'impression_pacing_interval': None,
+        'impression_pacing_type': None,
         'max_bid': float,
+        'max_bid_wm': float,
         'media_type': None,
         'name': None,
         'pacing_amount': float,
@@ -71,6 +77,8 @@ class Strategy(Entity):
         'start_date': t1types.strpt,
         'status': t1types.int_to_bool,
         'supply_type': None,
+        'targeting_segment_exclude_op': None,
+        'targeting_segment_include_op': None,
         'type': None,
         'updated_on': t1types.strpt,
         'use_campaign_end': t1types.int_to_bool,
@@ -82,13 +90,16 @@ class Strategy(Entity):
     }
     _push = _pull.copy()
     _push.update({
-        'audience_segment_exclude_op': _aud_seg_ops,
-        'audience_segment_include_op': _aud_seg_ops,
+        'audience_segment_exclude_op': _seg_incexc_ops,
+        'audience_segment_include_op': _seg_incexc_ops,
         'bid_price_is_media_only': int,
         'end_date': partial(t1types.strft, null_on_none=True),
-        'frequency_interval': _freq_int,
-        'frequency_type': _freq_type,
+        'frequency_interval': _pacing_ints,
+        'frequency_optimization': int,
+        'frequency_type': _pacing_types,
         'goal_type': _goal_type,
+        'impression_pacing_interval': _pacing_ints,
+        'impression_pacing_type': _pacing_types,
         'media_type': _media_type,
         'pacing_interval': _pac_int,
         'pacing_type': _pac_type,
@@ -102,6 +113,8 @@ class Strategy(Entity):
         'start_date': partial(t1types.strft, null_on_none=True),
         'status': int,
         'supply_type': _supply_type,
+        'targeting_segment_exclude_op': _seg_incexc_ops,
+        'targeting_segment_include_op': _seg_incexc_ops,
         'type': _type,
         'use_campaign_end': int,
         'use_campaign_start': int,
@@ -112,6 +125,17 @@ class Strategy(Entity):
     _readonly = Entity._readonly | {'effective_goal_value', 'zone_name'}
 
     def __init__(self, session, properties=None, **kwargs):
+        if properties is None:
+            # super(Entity) supers to grandparent
+            super(Entity, self).__setattr__('_init_impcap', None)
+            super(Entity, self).__setattr__('_init_imppac', None)
+        else:
+            super(Entity, self).__setattr__('_init_impcap',
+                                            properties.get('impression_cap'))
+            super(Entity, self).__setattr__('_init_imppac',
+                                            (properties.get('impression_pacing_type'),
+                                             properties.get('impression_pacing_amount'),
+                                             properties.get('impression_pacing_interval')))
         super(Strategy, self).__init__(session, properties, **kwargs)
         try:
             self.pixel_target_expr
@@ -148,6 +172,35 @@ class Strategy(Entity):
             },
         }
 
+    def _migration_asst(self):
+        """Helps migrate users to the new impression pacing features.
+
+        impression_cap is the old field. impression pacing comprise the new.
+        If the user has changed:
+            - Nothing (final vals all equal): remove both fields
+            - Old (new vals equal): remove new fields, post old
+            - New (old vals equal): remove old fields, post new
+            - Both (no vals equal): UNDEFINED. remove old fields to prep.
+        """
+        new_fields = ['impression_pacing_type',
+                      'impression_pacing_amount',
+                      'impression_pacing_interval']
+        i_cap, i_pac = self._init_impcap, self._init_imppac
+        f_cap, f_pac = (self.properties.get('impression_cap'),
+                        (self.properties.get('impression_pacing_type'),
+                         self.properties.get('impression_pacing_amount'),
+                         self.properties.get('impression_pacing_interval')))
+
+        fields_to_remove = None
+        if i_cap == f_cap and i_pac == f_pac:
+            fields_to_remove = ['impression_cap']
+            fields_to_remove.extend(new_fields)
+        elif i_pac == f_pac:
+            fields_to_remove = new_fields
+        else:  # we don't need a second elif here because it's the same result
+            fields_to_remove = ['impression_cap']
+        return fields_to_remove
+
     def save_supplies(self, data):
         url = self._construct_url(addl=['supplies', ])
         entity, _ = super(Strategy, self)._post(PATHS['mgmt'], url, data)
@@ -189,12 +242,16 @@ class Strategy(Entity):
             return include_string + exclude_string
 
     def save(self, data=None, url=None):
-
+        """Save object to T1 accounting for fields an pixel target expr"""
         if data is None:
             data = self.properties.copy()
 
         data['pixel_target_expr'] = self._serialize_target_expr()
 
+        fields_to_remove = self._migration_asst()
+        for field in fields_to_remove:
+            data.pop(field, None)
+
         if getattr(self, 'use_campaign_start', False) and 'start_date' in data:
             self.properties.pop('start_date', None)
             data['start_date'] = None
@@ -204,7 +261,14 @@ class Strategy(Entity):
 
         super(Strategy, self).save(data=data, url=url)
 
+        # Re-set the fields so that if the same object get saved, we
+        # compare agains the re-initialized values
         self._deserialize_target_expr()
+        super(Entity, self).__setattr__('_init_impcap', self.impression_cap)
+        super(Entity, self).__setattr__('_init_imppac',
+                                        (self.impression_pacing_type,
+                                         self.impression_pacing_amount,
+                                         self.impression_pacing_interval))
 
     @property
     def pixel_target_expr_string(self):
diff --git a/terminalone/vendor/six.py b/terminalone/vendor/six.py
index 21b0e80..190c023 100644
--- a/terminalone/vendor/six.py
+++ b/terminalone/vendor/six.py
@@ -1,6 +1,6 @@
 """Utilities for writing code that runs on Python 2 and 3"""
 
-# Copyright (c) 2010-2014 Benjamin Peterson
+# Copyright (c) 2010-2015 Benjamin Peterson
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the "Software"), to deal
@@ -23,17 +23,19 @@
 from __future__ import absolute_import
 
 import functools
+import itertools
 import operator
 import sys
 import types
 
 __author__ = "Benjamin Peterson <benjamin@python.org>"
-__version__ = "1.8.0"
+__version__ = "1.10.0"
 
 
 # Useful for very coarse version differentiation.
 PY2 = sys.version_info[0] == 2
 PY3 = sys.version_info[0] == 3
+PY34 = sys.version_info[0:2] >= (3, 4)
 
 if PY3:
     string_types = str,
@@ -56,6 +58,7 @@ else:
     else:
         # It's possible to have sizeof(long) != sizeof(Py_ssize_t).
         class X(object):
+
             def __len__(self):
                 return 1 << 31
         try:
@@ -87,9 +90,13 @@ class _LazyDescr(object):
 
     def __get__(self, obj, tp):
         result = self._resolve()
-        setattr(obj, self.name, result) # Invokes __set__.
-        # This is a bit ugly, but it avoids running this again.
-        delattr(obj.__class__, self.name)
+        setattr(obj, self.name, result)  # Invokes __set__.
+        try:
+            # This is a bit ugly, but it avoids running this again by
+            # removing this descriptor.
+            delattr(obj.__class__, self.name)
+        except AttributeError:
+            pass
         return result
 
 
@@ -155,12 +162,14 @@ class MovedAttribute(_LazyDescr):
 
 
 class _SixMetaPathImporter(object):
+
     """
     A meta path importer to import six.moves and its submodules.
 
     This class implements a PEP302 finder and loader. It should be compatible
     with Python 2.5 and all existing versions of Python3
     """
+
     def __init__(self, six_module_name):
         self.name = six_module_name
         self.known_modules = {}
@@ -218,6 +227,7 @@ _importer = _SixMetaPathImporter(__name__)
 
 
 class _MovedItems(_LazyModule):
+
     """Lazy loading of moved objects"""
     __path__ = []  # mark as package
 
@@ -229,8 +239,10 @@ _moved_attributes = [
     MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
     MovedAttribute("intern", "__builtin__", "sys"),
     MovedAttribute("map", "itertools", "builtins", "imap", "map"),
+    MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"),
+    MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"),
     MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
-    MovedAttribute("reload_module", "__builtin__", "imp", "reload"),
+    MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"),
     MovedAttribute("reduce", "__builtin__", "functools"),
     MovedAttribute("shlex_quote", "pipes", "shlex", "quote"),
     MovedAttribute("StringIO", "StringIO", "io"),
@@ -240,7 +252,6 @@ _moved_attributes = [
     MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
     MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
     MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
-
     MovedModule("builtins", "__builtin__"),
     MovedModule("configparser", "ConfigParser"),
     MovedModule("copyreg", "copy_reg"),
@@ -287,8 +298,13 @@ _moved_attributes = [
     MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
     MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"),
     MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"),
-    MovedModule("winreg", "_winreg"),
 ]
+# Add windows specific modules.
+if sys.platform == "win32":
+    _moved_attributes += [
+        MovedModule("winreg", "_winreg"),
+    ]
+
 for attr in _moved_attributes:
     setattr(_MovedItems, attr.name, attr)
     if isinstance(attr, MovedModule):
@@ -302,6 +318,7 @@ _importer._add_module(moves, "moves")
 
 
 class Module_six_moves_urllib_parse(_LazyModule):
+
     """Lazy loading of moved objects in six.moves.urllib_parse"""
 
 
@@ -341,6 +358,7 @@ _importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_pa
 
 
 class Module_six_moves_urllib_error(_LazyModule):
+
     """Lazy loading of moved objects in six.moves.urllib_error"""
 
 
@@ -360,6 +378,7 @@ _importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.er
 
 
 class Module_six_moves_urllib_request(_LazyModule):
+
     """Lazy loading of moved objects in six.moves.urllib_request"""
 
 
@@ -409,6 +428,7 @@ _importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.
 
 
 class Module_six_moves_urllib_response(_LazyModule):
+
     """Lazy loading of moved objects in six.moves.urllib_response"""
 
 
@@ -429,6 +449,7 @@ _importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib
 
 
 class Module_six_moves_urllib_robotparser(_LazyModule):
+
     """Lazy loading of moved objects in six.moves.urllib_robotparser"""
 
 
@@ -446,6 +467,7 @@ _importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.url
 
 
 class Module_six_moves_urllib(types.ModuleType):
+
     """Create a six.moves.urllib namespace that resembles the Python 3 namespace"""
     __path__ = []  # mark as package
     parse = _importer._get_module("moves.urllib_parse")
@@ -516,6 +538,9 @@ if PY3:
 
     create_bound_method = types.MethodType
 
+    def create_unbound_method(func, cls):
+        return func
+
     Iterator = object
 else:
     def get_unbound_function(unbound):
@@ -524,6 +549,9 @@ else:
     def create_bound_method(func, obj):
         return types.MethodType(func, obj, obj.__class__)
 
+    def create_unbound_method(func, cls):
+        return types.MethodType(func, None, cls)
+
     class Iterator(object):
 
         def next(self):
@@ -554,18 +582,30 @@ if PY3:
 
     def iterlists(d, **kw):
         return iter(d.lists(**kw))
+
+    viewkeys = operator.methodcaller("keys")
+
+    viewvalues = operator.methodcaller("values")
+
+    viewitems = operator.methodcaller("items")
 else:
     def iterkeys(d, **kw):
-        return iter(d.iterkeys(**kw))
+        return d.iterkeys(**kw)
 
     def itervalues(d, **kw):
-        return iter(d.itervalues(**kw))
+        return d.itervalues(**kw)
 
     def iteritems(d, **kw):
-        return iter(d.iteritems(**kw))
+        return d.iteritems(**kw)
 
     def iterlists(d, **kw):
-        return iter(d.iterlists(**kw))
+        return d.iterlists(**kw)
+
+    viewkeys = operator.methodcaller("viewkeys")
+
+    viewvalues = operator.methodcaller("viewvalues")
+
+    viewitems = operator.methodcaller("viewitems")
 
 _add_doc(iterkeys, "Return an iterator over the keys of a dictionary.")
 _add_doc(itervalues, "Return an iterator over the values of a dictionary.")
@@ -578,45 +618,66 @@ _add_doc(iterlists,
 if PY3:
     def b(s):
         return s.encode("latin-1")
+
     def u(s):
         return s
     unichr = chr
-    if sys.version_info[1] <= 1:
-        def int2byte(i):
-            return bytes((i,))
-    else:
-        # This is about 2x faster than the implementation above on 3.2+
-        int2byte = operator.methodcaller("to_bytes", 1, "big")
+    import struct
+    int2byte = struct.Struct(">B").pack
+    del struct
     byte2int = operator.itemgetter(0)
     indexbytes = operator.getitem
     iterbytes = iter
     import io
     StringIO = io.StringIO
     BytesIO = io.BytesIO
+    _assertCountEqual = "assertCountEqual"
+    if sys.version_info[1] <= 1:
+        _assertRaisesRegex = "assertRaisesRegexp"
+        _assertRegex = "assertRegexpMatches"
+    else:
+        _assertRaisesRegex = "assertRaisesRegex"
+        _assertRegex = "assertRegex"
 else:
     def b(s):
         return s
     # Workaround for standalone backslash
+
     def u(s):
         return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape")
     unichr = unichr
     int2byte = chr
+
     def byte2int(bs):
         return ord(bs[0])
+
     def indexbytes(buf, i):
         return ord(buf[i])
-    def iterbytes(buf):
-        return (ord(byte) for byte in buf)
+    iterbytes = functools.partial(itertools.imap, ord)
     import StringIO
     StringIO = BytesIO = StringIO.StringIO
+    _assertCountEqual = "assertItemsEqual"
+    _assertRaisesRegex = "assertRaisesRegexp"
+    _assertRegex = "assertRegexpMatches"
 _add_doc(b, """Byte literal""")
 _add_doc(u, """Text literal""")
 
 
+def assertCountEqual(self, *args, **kwargs):
+    return getattr(self, _assertCountEqual)(*args, **kwargs)
+
+
+def assertRaisesRegex(self, *args, **kwargs):
+    return getattr(self, _assertRaisesRegex)(*args, **kwargs)
+
+
+def assertRegex(self, *args, **kwargs):
+    return getattr(self, _assertRegex)(*args, **kwargs)
+
+
 if PY3:
     exec_ = getattr(moves.builtins, "exec")
 
-
     def reraise(tp, value, tb=None):
         if value is None:
             value = tp()
@@ -637,12 +698,26 @@ else:
             _locs_ = _globs_
         exec("""exec _code_ in _globs_, _locs_""")
 
-
     exec_("""def reraise(tp, value, tb=None):
     raise tp, value, tb
 """)
 
 
+if sys.version_info[:2] == (3, 2):
+    exec_("""def raise_from(value, from_value):
+    if from_value is None:
+        raise value
+    raise value from from_value
+""")
+elif sys.version_info[:2] > (3, 2):
+    exec_("""def raise_from(value, from_value):
+    raise value from from_value
+""")
+else:
+    def raise_from(value, from_value):
+        raise value
+
+
 print_ = getattr(moves.builtins, "print", None)
 if print_ is None:
     def print_(*args, **kwargs):
@@ -650,13 +725,14 @@ if print_ is None:
         fp = kwargs.pop("file", sys.stdout)
         if fp is None:
             return
+
         def write(data):
             if not isinstance(data, basestring):
                 data = str(data)
             # If the file has an encoding, encode unicode with it.
             if (isinstance(fp, file) and
-                isinstance(data, unicode) and
-                fp.encoding is not None):
+                    isinstance(data, unicode) and
+                    fp.encoding is not None):
                 errors = getattr(fp, "errors", None)
                 if errors is None:
                     errors = "strict"
@@ -697,6 +773,15 @@ if print_ is None:
                 write(sep)
             write(arg)
         write(end)
+if sys.version_info[:2] < (3, 3):
+    _print = print_
+
+    def print_(*args, **kwargs):
+        fp = kwargs.get("file", sys.stdout)
+        flush = kwargs.pop("flush", False)
+        _print(*args, **kwargs)
+        if flush and fp is not None:
+            fp.flush()
 
 _add_doc(reraise, """Reraise an exception.""")
 
@@ -704,19 +789,21 @@ if sys.version_info[0:2] < (3, 4):
     def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
               updated=functools.WRAPPER_UPDATES):
         def wrapper(f):
-            f = functools.wraps(wrapped)(f)
+            f = functools.wraps(wrapped, assigned, updated)(f)
             f.__wrapped__ = wrapped
             return f
         return wrapper
 else:
     wraps = functools.wraps
 
+
 def with_metaclass(meta, *bases):
     """Create a base class with a metaclass."""
     # This requires a bit of explanation: the basic idea is to make a dummy
     # metaclass for one level of class instantiation that replaces itself with
     # the actual metaclass.
     class metaclass(meta):
+
         def __new__(cls, name, this_bases, d):
             return meta(name, bases, d)
     return type.__new__(metaclass, 'temporary_class', (), {})
@@ -737,6 +824,25 @@ def add_metaclass(metaclass):
         return metaclass(cls.__name__, cls.__bases__, orig_vars)
     return wrapper
 
+
+def python_2_unicode_compatible(klass):
+    """
+    A decorator that defines __unicode__ and __str__ methods under Python 2.
+    Under Python 3 it does nothing.
+
+    To support Python 2 and 3 with a single code base, define a __str__ method
+    returning text and apply this decorator to the class.
+    """
+    if PY2:
+        if '__str__' not in klass.__dict__:
+            raise ValueError("@python_2_unicode_compatible cannot be applied "
+                             "to %s because it doesn't define __str__()." %
+                             klass.__name__)
+        klass.__unicode__ = klass.__str__
+        klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
+    return klass
+
+
 # Complete the moves implementation.
 # This code is at the end of this module to speed up module loading.
 # Turn this module into a package.
@@ -754,7 +860,7 @@ if sys.meta_path:
         # the six meta path importer, since the other six instance will have
         # inserted an importer with different class.
         if (type(importer).__name__ == "_SixMetaPathImporter" and
-            importer.name == __name__):
+                importer.name == __name__):
             del sys.meta_path[i]
             break
     del i, importer


Test patch:
diff --git a/tests/test_deprecated_fields.py b/tests/test_deprecated_fields.py
new file mode 100644
index 0000000..1f5123d
--- /dev/null
+++ b/tests/test_deprecated_fields.py
@@ -0,0 +1,85 @@
+from __future__ import absolute_import
+import unittest
+from terminalone.models import campaign
+from terminalone.models import strategy
+from terminalone.vendor import six
+
+
+class TestRemoveDeprecatedFields(unittest.TestCase):
+    """Tests for removing deprecated fields on save from strategy and campaign entities"""
+
+    def setUp(self):
+        mock_campaign_properties = {
+            "spend_cap_enabled": False,
+            "spend_cap_type": "no-limit",
+            "spend_cap_automatic": True
+
+        }
+        mock_strategy_properties = {
+            "impression_pacing_interval": "day",
+            "impression_pacing_type": "no-limit",
+            "impression_pacing_amount": 10,
+            "impression_cap": 10,
+
+        }
+        self.campaign = campaign.Campaign(None, mock_campaign_properties)
+        self.strategy = strategy.Strategy(None, mock_strategy_properties)
+
+    def test_campaign_remove_both_fields_when_no_changes(self):
+        fields_to_remove = self.campaign._migration_asst()
+        expected = ['spend_cap_enabled', 'spend_cap_type']
+        self.assertEqual(expected, fields_to_remove)
+
+    def test_campaign_remove_new_field_when_old_changed(self):
+        self.campaign.spend_cap_enabled = True
+
+        fields_to_remove = self.campaign._migration_asst()
+        expected = ['spend_cap_type']
+        self.assertEqual(expected, fields_to_remove)
+
+    def test_campaign_remove_old_field_when_new_changed(self):
+        self.campaign.spend_cap_type = 'derp'
+
+        fields_to_remove = self.campaign._migration_asst()
+        expected = ['spend_cap_enabled']
+        self.assertEqual(expected, fields_to_remove)
+
+    def test_campaign_remove_old_field_when_both_changed(self):
+        self.campaign.spend_cap_type = 'derp'
+        self.campaign.spend_cap_enabled = True
+
+        fields_to_remove = self.campaign._migration_asst()
+        expected = ['spend_cap_enabled']
+        self.assertEqual(expected, fields_to_remove)
+
+    def test_strategy_remove_all_fields_when_no_changes(self):
+        fields_to_remove = self.strategy._migration_asst()
+        expected = ['impression_pacing_interval', 'impression_pacing_type', 'impression_pacing_amount',
+                    'impression_cap']
+        six.assertCountEqual(self, expected, fields_to_remove)
+
+    def test_strategy_remove_new_fields_when_old_changed(self):
+        self.strategy.impression_cap = 1
+
+        fields_to_remove = self.strategy._migration_asst()
+        expected = ['impression_pacing_interval', 'impression_pacing_type', 'impression_pacing_amount']
+        six.assertCountEqual(self, expected, fields_to_remove)
+
+    def test_strategy_remove_old_fields_when_new_changed(self):
+        self.strategy.impression_pacing_interval = 'derp'
+        self.strategy.impression_pacing_type = 'derp'
+        self.strategy.impression_pacing_amount = 1
+
+        fields_to_remove = self.strategy._migration_asst()
+        expected = ['impression_cap']
+        six.assertCountEqual(self, expected, fields_to_remove)
+
+    def test_strategy_remove_old_fields_when_all_changed(self):
+        self.strategy.impression_pacing_interval = 'derp'
+        self.strategy.impression_pacing_type = 'derp'
+        self.strategy.impression_pacing_amount = 1
+        self.strategy.impression_cap = 1
+
+        fields_to_remove = self.strategy._migration_asst()
+        expected = ['impression_cap']
+        six.assertCountEqual(self, expected, fields_to_remove)