Repository: oasis-open/cti-python-stix2

Issue:
Passing a list to Bundle constructor creates list within a list
Currently if you pass a list to the Bundle constructor it creates a list within a list: 

```json
        {
          "objects":
             [
               [
                   { },
                   { }
               ]
             ]
        }
```

If the first argument is a list, the constructor should just assign it to `objects`.

Discussion:

Patch:
diff --git a/docs/overview.rst b/docs/overview.rst
index 643253c..b8c3809 100644
--- a/docs/overview.rst
+++ b/docs/overview.rst
@@ -1,9 +1,6 @@
 Overview
 ========
 
-Goals
------
-
 High level goals/principles of the python-stix2 library:
 
 1. It should be as easy as possible (but no easier!) to perform common tasks of
@@ -13,9 +10,6 @@ High level goals/principles of the python-stix2 library:
    the STIX 2.0 spec, as well as associated best practices. The library should
    make it hard to do "the wrong thing".
 
-Design Decisions
-----------------
-
 To accomplish these goals, and to incorporate lessons learned while developing
 python-stix (for STIX 1.x), several decisions influenced the design of
 python-stix2:
@@ -28,70 +22,3 @@ python-stix2:
 3. Core Python data types (including numeric types, ``datetime``) should be used
    when appropriate, and serialized to the correct format in JSON as specified
    in the STIX 2.0 spec.
-
-Architecture
-------------
-
-The `stix2` library APIs are divided into three logical layers, representing
-different levels of abstraction useful in different types of scripts and larger
-applications. It is possible to combine multiple layers in the same program,
-and the higher levels build on the layers below.
-
-
-Object Layer
-^^^^^^^^^^^^
-
-The lowest layer, **Object Layer**, is where Python objects representing STIX 2
-data types (such as SDOs, SROs, and Cyber Observable Objects, as well as
-non-top-level objects like External References, Kill Chain phases, and Cyber
-Observable extensions) are created, and can be serialized and deserialized
-to and from JSON representation.
-
-This layer is appropriate for stand-alone scripts that produce or consume STIX
-2 content, or can serve as a low-level data API for larger applications that
-need to represent STIX objects as Python classes.
-
-At this level, non-embedded reference properties (those ending in ``_ref``, such
-as the links from a Relationship object to its source and target objects) are
-not implemented as references between the Python objects themselves, but by
-simply having the same values in ``id`` and reference properties. There is no
-referential integrity maintained by the ``stix2`` library.
-
-*This layer is mostly complete.*
-
-Environment Layer
-^^^^^^^^^^^^^^^^^
-
-The **Environment Layer** adds several components that make it easier to handle
-STIX 2 data as part of a larger application and as part of a larger cyber threat
-intelligence ecosystem.
-
-- ``Data Source``s represent locations from which STIX data can be retrieved,
-  such as a TAXII server, database, or local filesystem. The Data Source API
-  abstracts differences between these storage location, giving a common API to
-  get objects by ID or query by various properties, as well as allowing
-  federated operations over multiple data sources.
-- Similarly, ``Data Sink`` objects represent destinations for sending STIX data.
-- An ``Object Factory`` provides a way to add common properties to all created
-  objects (such as the same ``created_by_ref``, or a ``StatementMarking`` with
-  copyright information or terms of use for the STIX data).
-
-Each of these components can be used individually, or combined as part of an
-``Environment``. These ``Environment`` objects allow different settings to be
-used by different users of a multi-user application (such as a web application).
-
-*This layer is currently being developed.*
-
-Workbench Layer
-^^^^^^^^^^^^^^^
-
-The highest layer of the ``stix2`` APIs is the **Workbench Layer**, designed for
-a single user in a highly-interactive analytical environment (such as a `Jupyter
-Notebook <https://jupyter.org/>`_). It builds on the lower layers of the API,
-while hiding most of their complexity. Unlike the other layers, this layer is
-designed to be used directly by end users. For users who are comfortable with,
-Python, the Workbench Layer makes it easy to quickly interact with STIX data
-from a variety of sources without needing to write and run one-off Python
-scripts.
-
-*This layer has not yet been started.*
diff --git a/stix2/base.py b/stix2/base.py
index 7de193b..cb17f11 100644
--- a/stix2/base.py
+++ b/stix2/base.py
@@ -176,7 +176,7 @@ class _STIXBase(collections.Mapping):
         if 'modified' not in kwargs:
             kwargs['modified'] = get_timestamp()
         else:
-            new_modified_property = parse_into_datetime(kwargs['modified'], precision='millisecond')
+            new_modified_property = parse_into_datetime(kwargs['modified'])
             if new_modified_property < self.modified:
                 raise InvalidValueError(cls, 'modified', "The new modified datetime cannot be before the current modified datatime.")
         new_obj_inner.update(kwargs)
diff --git a/stix2/bundle.py b/stix2/bundle.py
index 85be3e1..b598ceb 100644
--- a/stix2/bundle.py
+++ b/stix2/bundle.py
@@ -17,6 +17,9 @@ class Bundle(_STIXBase):
     def __init__(self, *args, **kwargs):
         # Add any positional arguments to the 'objects' kwarg.
         if args:
-            kwargs['objects'] = kwargs.get('objects', []) + list(args)
+            if isinstance(args[0], list):
+                kwargs['objects'] = args[0] + list(args[1:]) + kwargs.get('objects', [])
+            else:
+                kwargs['objects'] = list(args) + kwargs.get('objects', [])
 
         super(Bundle, self).__init__(**kwargs)
diff --git a/stix2/common.py b/stix2/common.py
index 7c6e747..c8c243d 100644
--- a/stix2/common.py
+++ b/stix2/common.py
@@ -7,8 +7,8 @@ from .utils import NOW
 
 COMMON_PROPERTIES = {
     # 'type' and 'id' should be defined on each individual type
-    'created': TimestampProperty(default=lambda: NOW, precision='millisecond'),
-    'modified': TimestampProperty(default=lambda: NOW, precision='millisecond'),
+    'created': TimestampProperty(default=lambda: NOW),
+    'modified': TimestampProperty(default=lambda: NOW),
     'external_references': ListProperty(ExternalReference),
     'revoked': BooleanProperty(),
     'labels': ListProperty(StringProperty),
diff --git a/stix2/observables.py b/stix2/observables.py
index a8f3b67..086dc45 100644
--- a/stix2/observables.py
+++ b/stix2/observables.py
@@ -215,7 +215,7 @@ class WindowsPEBinaryExt(_Extension):
         'imphash': StringProperty(),
         'machine_hex': HexProperty(),
         'number_of_sections': IntegerProperty(),
-        'time_date_stamp': TimestampProperty(precision='second'),
+        'time_date_stamp': TimestampProperty(),
         'pointer_to_symbol_table_hex': HexProperty(),
         'number_of_symbols': IntegerProperty(),
         'size_of_optional_header': IntegerProperty(),
diff --git a/stix2/properties.py b/stix2/properties.py
index 80e5345..9d54cf0 100644
--- a/stix2/properties.py
+++ b/stix2/properties.py
@@ -1,15 +1,18 @@
 import base64
 import binascii
 import collections
+import datetime as dt
 import inspect
 import re
 import uuid
 
+from dateutil import parser
+import pytz
 from six import text_type
 
 from .base import _Observable, _STIXBase
 from .exceptions import DictionaryKeyError
-from .utils import get_dict, parse_into_datetime
+from .utils import get_dict
 
 
 class Property(object):
@@ -212,12 +215,26 @@ class BooleanProperty(Property):
 
 class TimestampProperty(Property):
 
-    def __init__(self, precision=None, **kwargs):
-        self.precision = precision
-        super(TimestampProperty, self).__init__(**kwargs)
-
     def clean(self, value):
-        return parse_into_datetime(value, self.precision)
+        if isinstance(value, dt.date):
+            if hasattr(value, 'hour'):
+                return value
+            else:
+                # Add a time component
+                return dt.datetime.combine(value, dt.time(), tzinfo=pytz.utc)
+
+        # value isn't a date or datetime object so assume it's a string
+        try:
+            parsed = parser.parse(value)
+        except TypeError:
+            # Unknown format
+            raise ValueError("must be a datetime object, date object, or "
+                             "timestamp string in a recognizable format.")
+        if parsed.tzinfo:
+            return parsed.astimezone(pytz.utc)
+        else:
+            # Doesn't have timezone info in the string; assume UTC
+            return pytz.utc.localize(parsed)
 
 
 class ObservableProperty(Property):
diff --git a/stix2/utils.py b/stix2/utils.py
index 12b889c..bb41937 100644
--- a/stix2/utils.py
+++ b/stix2/utils.py
@@ -12,29 +12,15 @@ import pytz
 NOW = object()
 
 
-class STIXdatetime(dt.datetime):
-    def __new__(cls, *args, **kwargs):
-        precision = kwargs.pop('precision', None)
-        if isinstance(args[0], dt.datetime):  # Allow passing in a datetime object
-            dttm = args[0]
-            args = (dttm.year, dttm.month, dttm.day, dttm.hour, dttm.minute,
-                    dttm.second, dttm.microsecond, dttm.tzinfo)
-        # self will be an instance of STIXdatetime, not dt.datetime
-        self = dt.datetime.__new__(cls, *args, **kwargs)
-        self.precision = precision
-        return self
-
-
 def get_timestamp():
-    return STIXdatetime.now(tz=pytz.UTC)
+    return dt.datetime.now(tz=pytz.UTC)
 
 
 def format_datetime(dttm):
     # 1. Convert to timezone-aware
     # 2. Convert to UTC
     # 3. Format in ISO format
-    # 4. Ensure correct precision
-    # 4a. Add subsecond value if non-zero and precision not defined
+    # 4. Add subsecond value if non-zero
     # 5. Add "Z"
 
     if dttm.tzinfo is None or dttm.tzinfo.utcoffset(dttm) is None:
@@ -43,53 +29,32 @@ def format_datetime(dttm):
     else:
         zoned = dttm.astimezone(pytz.utc)
     ts = zoned.strftime("%Y-%m-%dT%H:%M:%S")
-    ms = zoned.strftime("%f")
-    precision = getattr(dttm, "precision", None)
-    if precision == 'second':
-        pass  # Alredy precise to the second
-    elif precision == "millisecond":
-        ts = ts + '.' + ms[:3]
-    elif zoned.microsecond > 0:
+    if zoned.microsecond > 0:
+        ms = zoned.strftime("%f")
         ts = ts + '.' + ms.rstrip("0")
     return ts + "Z"
 
 
-def parse_into_datetime(value, precision=None):
+def parse_into_datetime(value):
     if isinstance(value, dt.date):
         if hasattr(value, 'hour'):
-            ts = value
+            return value
         else:
             # Add a time component
-            ts = dt.datetime.combine(value, dt.time(0, 0, tzinfo=pytz.utc))
+            return dt.datetime.combine(value, dt.time(0, 0, tzinfo=pytz.utc))
+
+    # value isn't a date or datetime object so assume it's a string
+    try:
+        parsed = parser.parse(value)
+    except (TypeError, ValueError):
+        # Unknown format
+        raise ValueError("must be a datetime object, date object, or "
+                         "timestamp string in a recognizable format.")
+    if parsed.tzinfo:
+        return parsed.astimezone(pytz.utc)
     else:
-        # value isn't a date or datetime object so assume it's a string
-        try:
-            parsed = parser.parse(value)
-        except (TypeError, ValueError):
-            # Unknown format
-            raise ValueError("must be a datetime object, date object, or "
-                             "timestamp string in a recognizable format.")
-        if parsed.tzinfo:
-            ts = parsed.astimezone(pytz.utc)
-        else:
-            # Doesn't have timezone info in the string; assume UTC
-            ts = pytz.utc.localize(parsed)
-
-    # Ensure correct precision
-    if not precision:
-        return ts
-    ms = ts.microsecond
-    if precision == 'second':
-        ts = ts.replace(microsecond=0)
-    elif precision == 'millisecond':
-        ms_len = len(str(ms))
-        if ms_len > 3:
-            # Truncate to millisecond precision
-            factor = 10 ** (ms_len - 3)
-            ts = ts.replace(microsecond=(ts.microsecond // factor) * factor)
-        else:
-            ts = ts.replace(microsecond=0)
-    return STIXdatetime(ts, precision=precision)
+        # Doesn't have timezone info in the string; assume UTC
+        return pytz.utc.localize(parsed)
 
 
 def get_dict(data):


Test patch:
diff --git a/stix2/test/conftest.py b/stix2/test/conftest.py
index 9f61bc2..d1f3330 100644
--- a/stix2/test/conftest.py
+++ b/stix2/test/conftest.py
@@ -1,3 +1,4 @@
+import datetime as dt
 import uuid
 
 import pytest
@@ -12,12 +13,12 @@ from .constants import (FAKE_TIME, INDICATOR_KWARGS, MALWARE_KWARGS,
 @pytest.fixture
 def clock(monkeypatch):
 
-    class mydatetime(stix2.utils.STIXdatetime):
+    class mydatetime(dt.datetime):
         @classmethod
         def now(cls, tz=None):
             return FAKE_TIME
 
-    monkeypatch.setattr(stix2.utils, 'STIXdatetime', mydatetime)
+    monkeypatch.setattr(dt, 'datetime', mydatetime)
 
 
 @pytest.fixture
diff --git a/stix2/test/test_attack_pattern.py b/stix2/test/test_attack_pattern.py
index 5bd5af2..7510888 100644
--- a/stix2/test/test_attack_pattern.py
+++ b/stix2/test/test_attack_pattern.py
@@ -9,7 +9,7 @@ from .constants import ATTACK_PATTERN_ID
 
 
 EXPECTED = """{
-    "created": "2016-05-12T08:17:27.000Z",
+    "created": "2016-05-12T08:17:27Z",
     "description": "...",
     "external_references": [
         {
@@ -18,7 +18,7 @@ EXPECTED = """{
         }
     ],
     "id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
-    "modified": "2016-05-12T08:17:27.000Z",
+    "modified": "2016-05-12T08:17:27Z",
     "name": "Spear Phishing",
     "type": "attack-pattern"
 }"""
@@ -27,8 +27,8 @@ EXPECTED = """{
 def test_attack_pattern_example():
     ap = stix2.AttackPattern(
         id="attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
-        created="2016-05-12T08:17:27.000Z",
-        modified="2016-05-12T08:17:27.000Z",
+        created="2016-05-12T08:17:27Z",
+        modified="2016-05-12T08:17:27Z",
         name="Spear Phishing",
         external_references=[{
             "source_name": "capec",
@@ -45,8 +45,8 @@ def test_attack_pattern_example():
     {
         "type": "attack-pattern",
         "id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
-        "created": "2016-05-12T08:17:27.000Z",
-        "modified": "2016-05-12T08:17:27.000Z",
+        "created": "2016-05-12T08:17:27Z",
+        "modified": "2016-05-12T08:17:27Z",
         "description": "...",
         "external_references": [
             {
diff --git a/stix2/test/test_bundle.py b/stix2/test/test_bundle.py
index fc3e350..9c2dd19 100644
--- a/stix2/test/test_bundle.py
+++ b/stix2/test/test_bundle.py
@@ -7,30 +7,30 @@ EXPECTED_BUNDLE = """{
     "id": "bundle--00000000-0000-0000-0000-000000000004",
     "objects": [
         {
-            "created": "2017-01-01T12:34:56.000Z",
+            "created": "2017-01-01T12:34:56Z",
             "id": "indicator--00000000-0000-0000-0000-000000000001",
             "labels": [
                 "malicious-activity"
             ],
-            "modified": "2017-01-01T12:34:56.000Z",
+            "modified": "2017-01-01T12:34:56Z",
             "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
             "type": "indicator",
             "valid_from": "2017-01-01T12:34:56Z"
         },
         {
-            "created": "2017-01-01T12:34:56.000Z",
+            "created": "2017-01-01T12:34:56Z",
             "id": "malware--00000000-0000-0000-0000-000000000002",
             "labels": [
                 "ransomware"
             ],
-            "modified": "2017-01-01T12:34:56.000Z",
+            "modified": "2017-01-01T12:34:56Z",
             "name": "Cryptolocker",
             "type": "malware"
         },
         {
-            "created": "2017-01-01T12:34:56.000Z",
+            "created": "2017-01-01T12:34:56Z",
             "id": "relationship--00000000-0000-0000-0000-000000000003",
-            "modified": "2017-01-01T12:34:56.000Z",
+            "modified": "2017-01-01T12:34:56Z",
             "relationship_type": "indicates",
             "source_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef",
             "target_ref": "malware--fedcba98-7654-3210-fedc-ba9876543210",
@@ -92,3 +92,27 @@ def test_create_bundle_with_positional_args(indicator, malware, relationship):
     bundle = stix2.Bundle(indicator, malware, relationship)
 
     assert str(bundle) == EXPECTED_BUNDLE
+
+
+def test_create_bundle_with_positional_listarg(indicator, malware, relationship):
+    bundle = stix2.Bundle([indicator, malware, relationship])
+
+    assert str(bundle) == EXPECTED_BUNDLE
+
+
+def test_create_bundle_with_listarg_and_positional_arg(indicator, malware, relationship):
+    bundle = stix2.Bundle([indicator, malware], relationship)
+
+    assert str(bundle) == EXPECTED_BUNDLE
+
+
+def test_create_bundle_with_listarg_and_kwarg(indicator, malware, relationship):
+    bundle = stix2.Bundle([indicator, malware], objects=[relationship])
+
+    assert str(bundle) == EXPECTED_BUNDLE
+
+
+def test_create_bundle_with_arg_listarg_and_kwarg(indicator, malware, relationship):
+    bundle = stix2.Bundle([indicator], malware, objects=[relationship])
+
+    assert str(bundle) == EXPECTED_BUNDLE
diff --git a/stix2/test/test_campaign.py b/stix2/test/test_campaign.py
index 30b9444..9920019 100644
--- a/stix2/test/test_campaign.py
+++ b/stix2/test/test_campaign.py
@@ -9,11 +9,11 @@ from .constants import CAMPAIGN_ID
 
 
 EXPECTED = """{
-    "created": "2016-04-06T20:03:00.000Z",
+    "created": "2016-04-06T20:03:00Z",
     "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
     "description": "Campaign by Green Group against a series of targets in the financial services sector.",
     "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
-    "modified": "2016-04-06T20:03:00.000Z",
+    "modified": "2016-04-06T20:03:00Z",
     "name": "Green Group Attacks Against Finance",
     "type": "campaign"
 }"""
diff --git a/stix2/test/test_course_of_action.py b/stix2/test/test_course_of_action.py
index e7a1b22..263eae2 100644
--- a/stix2/test/test_course_of_action.py
+++ b/stix2/test/test_course_of_action.py
@@ -9,11 +9,11 @@ from .constants import COURSE_OF_ACTION_ID
 
 
 EXPECTED = """{
-    "created": "2016-04-06T20:03:48.000Z",
+    "created": "2016-04-06T20:03:48Z",
     "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
     "description": "This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ...",
     "id": "course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
-    "modified": "2016-04-06T20:03:48.000Z",
+    "modified": "2016-04-06T20:03:48Z",
     "name": "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter",
     "type": "course-of-action"
 }"""
@@ -23,8 +23,8 @@ def test_course_of_action_example():
     coa = stix2.CourseOfAction(
         id="course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
         created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
-        created="2016-04-06T20:03:48.000Z",
-        modified="2016-04-06T20:03:48.000Z",
+        created="2016-04-06T20:03:48Z",
+        modified="2016-04-06T20:03:48Z",
         name="Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter",
         description="This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ..."
     )
@@ -35,11 +35,11 @@ def test_course_of_action_example():
 @pytest.mark.parametrize("data", [
     EXPECTED,
     {
-        "created": "2016-04-06T20:03:48.000Z",
+        "created": "2016-04-06T20:03:48Z",
         "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
         "description": "This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ...",
         "id": "course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
-        "modified": "2016-04-06T20:03:48.000Z",
+        "modified": "2016-04-06T20:03:48Z",
         "name": "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter",
         "type": "course-of-action"
     },
diff --git a/stix2/test/test_fixtures.py b/stix2/test/test_fixtures.py
index 83d5f85..9078972 100644
--- a/stix2/test/test_fixtures.py
+++ b/stix2/test/test_fixtures.py
@@ -1,12 +1,11 @@
+import datetime as dt
 import uuid
 
-from stix2 import utils
-
 from .constants import FAKE_TIME
 
 
 def test_clock(clock):
-    assert utils.STIXdatetime.now() == FAKE_TIME
+    assert dt.datetime.now() == FAKE_TIME
 
 
 def test_my_uuid4_fixture(uuid4):
diff --git a/stix2/test/test_identity.py b/stix2/test/test_identity.py
index ed51958..b2c166c 100644
--- a/stix2/test/test_identity.py
+++ b/stix2/test/test_identity.py
@@ -9,10 +9,10 @@ from .constants import IDENTITY_ID
 
 
 EXPECTED = """{
-    "created": "2015-12-21T19:59:11.000Z",
+    "created": "2015-12-21T19:59:11Z",
     "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c",
     "identity_class": "individual",
-    "modified": "2015-12-21T19:59:11.000Z",
+    "modified": "2015-12-21T19:59:11Z",
     "name": "John Smith",
     "type": "identity"
 }"""
@@ -21,8 +21,8 @@ EXPECTED = """{
 def test_identity_example():
     identity = stix2.Identity(
         id="identity--311b2d2d-f010-5473-83ec-1edf84858f4c",
-        created="2015-12-21T19:59:11.000Z",
-        modified="2015-12-21T19:59:11.000Z",
+        created="2015-12-21T19:59:11Z",
+        modified="2015-12-21T19:59:11Z",
         name="John Smith",
         identity_class="individual",
     )
@@ -33,10 +33,10 @@ def test_identity_example():
 @pytest.mark.parametrize("data", [
     EXPECTED,
     {
-        "created": "2015-12-21T19:59:11.000Z",
+        "created": "2015-12-21T19:59:11Z",
         "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c",
         "identity_class": "individual",
-        "modified": "2015-12-21T19:59:11.000Z",
+        "modified": "2015-12-21T19:59:11Z",
         "name": "John Smith",
         "type": "identity"
     },
@@ -56,8 +56,8 @@ def test_parse_no_type():
         stix2.parse("""
         {
             "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c",
-            "created": "2015-12-21T19:59:11.000Z",
-            "modified": "2015-12-21T19:59:11.000Z",
+            "created": "2015-12-21T19:59:11Z",
+            "modified": "2015-12-21T19:59:11Z",
             "name": "John Smith",
             "identity_class": "individual"
         }""")
diff --git a/stix2/test/test_indicator.py b/stix2/test/test_indicator.py
index 5daa0f6..5db50e6 100644
--- a/stix2/test/test_indicator.py
+++ b/stix2/test/test_indicator.py
@@ -10,22 +10,22 @@ from .constants import FAKE_TIME, INDICATOR_ID, INDICATOR_KWARGS
 
 
 EXPECTED_INDICATOR = """{
-    "created": "2017-01-01T00:00:01.000Z",
+    "created": "2017-01-01T00:00:01Z",
     "id": "indicator--01234567-89ab-cdef-0123-456789abcdef",
     "labels": [
         "malicious-activity"
     ],
-    "modified": "2017-01-01T00:00:01.000Z",
+    "modified": "2017-01-01T00:00:01Z",
     "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
     "type": "indicator",
     "valid_from": "1970-01-01T00:00:01Z"
 }"""
 
 EXPECTED_INDICATOR_REPR = "Indicator(" + " ".join("""
-    created=STIXdatetime(2017, 1, 1, 0, 0, 1, tzinfo=<UTC>),
+    created=datetime.datetime(2017, 1, 1, 0, 0, 1, tzinfo=<UTC>),
     id='indicator--01234567-89ab-cdef-0123-456789abcdef',
     labels=['malicious-activity'],
-    modified=STIXdatetime(2017, 1, 1, 0, 0, 1, tzinfo=<UTC>),
+    modified=datetime.datetime(2017, 1, 1, 0, 0, 1, tzinfo=<UTC>),
     pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
     type='indicator',
     valid_from=datetime.datetime(1970, 1, 1, 0, 0, 1, tzinfo=<UTC>)
@@ -48,8 +48,6 @@ def test_indicator_with_all_required_properties():
 
     assert str(ind) == EXPECTED_INDICATOR
     rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(ind))
-    print(rep)
-    print(EXPECTED_INDICATOR_REPR)
     assert rep == EXPECTED_INDICATOR_REPR
 
 
diff --git a/stix2/test/test_intrusion_set.py b/stix2/test/test_intrusion_set.py
index a6eee7f..3241ced 100644
--- a/stix2/test/test_intrusion_set.py
+++ b/stix2/test/test_intrusion_set.py
@@ -12,7 +12,7 @@ EXPECTED = """{
     "aliases": [
         "Zookeeper"
     ],
-    "created": "2016-04-06T20:03:48.000Z",
+    "created": "2016-04-06T20:03:48Z",
     "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
     "description": "Incidents usually feature a shared TTP of a bobcat being released...",
     "goals": [
@@ -21,7 +21,7 @@ EXPECTED = """{
         "damage"
     ],
     "id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29",
-    "modified": "2016-04-06T20:03:48.000Z",
+    "modified": "2016-04-06T20:03:48Z",
     "name": "Bobcat Breakin",
     "type": "intrusion-set"
 }"""
@@ -31,8 +31,8 @@ def test_intrusion_set_example():
     intrusion_set = stix2.IntrusionSet(
         id="intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29",
         created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
-        created="2016-04-06T20:03:48.000Z",
-        modified="2016-04-06T20:03:48.000Z",
+        created="2016-04-06T20:03:48Z",
+        modified="2016-04-06T20:03:48Z",
         name="Bobcat Breakin",
         description="Incidents usually feature a shared TTP of a bobcat being released...",
         aliases=["Zookeeper"],
@@ -48,7 +48,7 @@ def test_intrusion_set_example():
         "aliases": [
             "Zookeeper"
         ],
-        "created": "2016-04-06T20:03:48.000Z",
+        "created": "2016-04-06T20:03:48Z",
         "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
         "description": "Incidents usually feature a shared TTP of a bobcat being released...",
         "goals": [
@@ -57,7 +57,7 @@ def test_intrusion_set_example():
             "damage"
         ],
         "id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29",
-        "modified": "2016-04-06T20:03:48.000Z",
+        "modified": "2016-04-06T20:03:48Z",
         "name": "Bobcat Breakin",
         "type": "intrusion-set"
     },
diff --git a/stix2/test/test_malware.py b/stix2/test/test_malware.py
index ff0b394..266d012 100644
--- a/stix2/test/test_malware.py
+++ b/stix2/test/test_malware.py
@@ -10,12 +10,12 @@ from .constants import FAKE_TIME, MALWARE_ID, MALWARE_KWARGS
 
 
 EXPECTED_MALWARE = """{
-    "created": "2016-05-12T08:17:27.000Z",
+    "created": "2016-05-12T08:17:27Z",
     "id": "malware--fedcba98-7654-3210-fedc-ba9876543210",
     "labels": [
         "ransomware"
     ],
-    "modified": "2016-05-12T08:17:27.000Z",
+    "modified": "2016-05-12T08:17:27Z",
     "name": "Cryptolocker",
     "type": "malware"
 }"""
@@ -109,8 +109,8 @@ def test_invalid_kwarg_to_malware():
     {
         "type": "malware",
         "id": "malware--fedcba98-7654-3210-fedc-ba9876543210",
-        "created": "2016-05-12T08:17:27.000Z",
-        "modified": "2016-05-12T08:17:27.000Z",
+        "created": "2016-05-12T08:17:27Z",
+        "modified": "2016-05-12T08:17:27Z",
         "labels": ["ransomware"],
         "name": "Cryptolocker",
     },
diff --git a/stix2/test/test_markings.py b/stix2/test/test_markings.py
index c2e0276..f1f07db 100644
--- a/stix2/test/test_markings.py
+++ b/stix2/test/test_markings.py
@@ -40,7 +40,7 @@ EXPECTED_GRANULAR_MARKING = """{
 }"""
 
 EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS = """{
-    "created": "2016-04-06T20:03:00.000Z",
+    "created": "2016-04-06T20:03:00Z",
     "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
     "description": "Campaign by Green Group against a series of targets in the financial services sector.",
     "granular_markings": [
@@ -52,7 +52,7 @@ EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS = """{
         }
     ],
     "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
-    "modified": "2016-04-06T20:03:00.000Z",
+    "modified": "2016-04-06T20:03:00Z",
     "name": "Green Group Attacks Against Finance",
     "type": "campaign"
 }"""
diff --git a/stix2/test/test_observed_data.py b/stix2/test/test_observed_data.py
index 75f3070..0ed9954 100644
--- a/stix2/test/test_observed_data.py
+++ b/stix2/test/test_observed_data.py
@@ -10,12 +10,12 @@ from .constants import OBSERVED_DATA_ID
 
 
 EXPECTED = """{
-    "created": "2016-04-06T19:58:16.000Z",
+    "created": "2016-04-06T19:58:16Z",
     "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
     "first_observed": "2015-12-21T19:00:00Z",
     "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
     "last_observed": "2015-12-21T19:00:00Z",
-    "modified": "2016-04-06T19:58:16.000Z",
+    "modified": "2016-04-06T19:58:16Z",
     "number_observed": 50,
     "objects": {
         "0": {
@@ -31,8 +31,8 @@ def test_observed_data_example():
     observed_data = stix2.ObservedData(
         id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
         created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
-        created="2016-04-06T19:58:16.000Z",
-        modified="2016-04-06T19:58:16.000Z",
+        created="2016-04-06T19:58:16Z",
+        modified="2016-04-06T19:58:16Z",
         first_observed="2015-12-21T19:00:00Z",
         last_observed="2015-12-21T19:00:00Z",
         number_observed=50,
@@ -48,12 +48,12 @@ def test_observed_data_example():
 
 
 EXPECTED_WITH_REF = """{
-    "created": "2016-04-06T19:58:16.000Z",
+    "created": "2016-04-06T19:58:16Z",
     "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
     "first_observed": "2015-12-21T19:00:00Z",
     "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
     "last_observed": "2015-12-21T19:00:00Z",
-    "modified": "2016-04-06T19:58:16.000Z",
+    "modified": "2016-04-06T19:58:16Z",
     "number_observed": 50,
     "objects": {
         "0": {
@@ -76,8 +76,8 @@ def test_observed_data_example_with_refs():
     observed_data = stix2.ObservedData(
         id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
         created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
-        created="2016-04-06T19:58:16.000Z",
-        modified="2016-04-06T19:58:16.000Z",
+        created="2016-04-06T19:58:16Z",
+        modified="2016-04-06T19:58:16Z",
         first_observed="2015-12-21T19:00:00Z",
         last_observed="2015-12-21T19:00:00Z",
         number_observed=50,
@@ -102,8 +102,8 @@ def test_observed_data_example_with_bad_refs():
         stix2.ObservedData(
             id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
             created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
-            created="2016-04-06T19:58:16.000Z",
-            modified="2016-04-06T19:58:16.000Z",
+            created="2016-04-06T19:58:16Z",
+            modified="2016-04-06T19:58:16Z",
             first_observed="2015-12-21T19:00:00Z",
             last_observed="2015-12-21T19:00:00Z",
             number_observed=50,
@@ -130,11 +130,11 @@ def test_observed_data_example_with_bad_refs():
     {
         "type": "observed-data",
         "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
-        "created": "2016-04-06T19:58:16.000Z",
+        "created": "2016-04-06T19:58:16Z",
         "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
         "first_observed": "2015-12-21T19:00:00Z",
         "last_observed": "2015-12-21T19:00:00Z",
-        "modified": "2016-04-06T19:58:16.000Z",
+        "modified": "2016-04-06T19:58:16Z",
         "number_observed": 50,
         "objects": {
             "0": {
@@ -466,12 +466,12 @@ def test_parse_basic_tcp_traffic_with_error(data):
 
 
 EXPECTED_PROCESS_OD = """{
-    "created": "2016-04-06T19:58:16.000Z",
+    "created": "2016-04-06T19:58:16Z",
     "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
     "first_observed": "2015-12-21T19:00:00Z",
     "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
     "last_observed": "2015-12-21T19:00:00Z",
-    "modified": "2016-04-06T19:58:16.000Z",
+    "modified": "2016-04-06T19:58:16Z",
     "number_observed": 50,
     "objects": {
         "0": {
@@ -499,8 +499,8 @@ def test_observed_data_with_process_example():
     observed_data = stix2.ObservedData(
         id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
         created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
-        created="2016-04-06T19:58:16.000Z",
-        modified="2016-04-06T19:58:16.000Z",
+        created="2016-04-06T19:58:16Z",
+        modified="2016-04-06T19:58:16Z",
         first_observed="2015-12-21T19:00:00Z",
         last_observed="2015-12-21T19:00:00Z",
         number_observed=50,
diff --git a/stix2/test/test_relationship.py b/stix2/test/test_relationship.py
index 362348b..1ad792c 100644
--- a/stix2/test/test_relationship.py
+++ b/stix2/test/test_relationship.py
@@ -10,9 +10,9 @@ from .constants import (FAKE_TIME, INDICATOR_ID, MALWARE_ID, RELATIONSHIP_ID,
 
 
 EXPECTED_RELATIONSHIP = """{
-    "created": "2016-04-06T20:06:37.000Z",
+    "created": "2016-04-06T20:06:37Z",
     "id": "relationship--00000000-1111-2222-3333-444444444444",
-    "modified": "2016-04-06T20:06:37.000Z",
+    "modified": "2016-04-06T20:06:37Z",
     "relationship_type": "indicates",
     "source_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef",
     "target_ref": "malware--fedcba98-7654-3210-fedc-ba9876543210",
diff --git a/stix2/test/test_report.py b/stix2/test/test_report.py
index 4a1f905..cec217a 100644
--- a/stix2/test/test_report.py
+++ b/stix2/test/test_report.py
@@ -9,14 +9,14 @@ from .constants import INDICATOR_KWARGS, REPORT_ID
 
 
 EXPECTED = """{
-    "created": "2015-12-21T19:59:11.000Z",
+    "created": "2015-12-21T19:59:11Z",
     "created_by_ref": "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283",
     "description": "A simple report with an indicator and campaign",
     "id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3",
     "labels": [
         "campaign"
     ],
-    "modified": "2015-12-21T19:59:11.000Z",
+    "modified": "2015-12-21T19:59:11Z",
     "name": "The Black Vine Cyberespionage Group",
     "object_refs": [
         "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2",
@@ -32,8 +32,8 @@ def test_report_example():
     report = stix2.Report(
         id="report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3",
         created_by_ref="identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283",
-        created="2015-12-21T19:59:11.000Z",
-        modified="2015-12-21T19:59:11.000Z",
+        created="2015-12-21T19:59:11Z",
+        modified="2015-12-21T19:59:11Z",
         name="The Black Vine Cyberespionage Group",
         description="A simple report with an indicator and campaign",
         published="2016-01-20T17:00:00Z",
@@ -95,14 +95,14 @@ def test_report_example_objects_in_object_refs_with_bad_id():
 @pytest.mark.parametrize("data", [
     EXPECTED,
     {
-        "created": "2015-12-21T19:59:11.000Z",
+        "created": "2015-12-21T19:59:11Z",
         "created_by_ref": "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283",
         "description": "A simple report with an indicator and campaign",
         "id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3",
         "labels": [
             "campaign"
         ],
-        "modified": "2015-12-21T19:59:11.000Z",
+        "modified": "2015-12-21T19:59:11Z",
         "name": "The Black Vine Cyberespionage Group",
         "object_refs": [
             "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2",
diff --git a/stix2/test/test_sighting.py b/stix2/test/test_sighting.py
index 2036457..6c9b7d8 100644
--- a/stix2/test/test_sighting.py
+++ b/stix2/test/test_sighting.py
@@ -9,9 +9,9 @@ from .constants import INDICATOR_ID, SIGHTING_ID, SIGHTING_KWARGS
 
 
 EXPECTED_SIGHTING = """{
-    "created": "2016-04-06T20:06:37.000Z",
+    "created": "2016-04-06T20:06:37Z",
     "id": "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb",
-    "modified": "2016-04-06T20:06:37.000Z",
+    "modified": "2016-04-06T20:06:37Z",
     "sighting_of_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef",
     "type": "sighting",
     "where_sighted_refs": [
@@ -20,9 +20,9 @@ EXPECTED_SIGHTING = """{
 }"""
 
 BAD_SIGHTING = """{
-    "created": "2016-04-06T20:06:37.000Z",
+    "created": "2016-04-06T20:06:37Z",
     "id": "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb",
-    "modified": "2016-04-06T20:06:37.000Z",
+    "modified": "2016-04-06T20:06:37Z",
     "sighting_of_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef",
     "type": "sighting",
     "where_sighted_refs": [
diff --git a/stix2/test/test_threat_actor.py b/stix2/test/test_threat_actor.py
index 1bab744..93e8179 100644
--- a/stix2/test/test_threat_actor.py
+++ b/stix2/test/test_threat_actor.py
@@ -9,14 +9,14 @@ from .constants import THREAT_ACTOR_ID
 
 
 EXPECTED = """{
-    "created": "2016-04-06T20:03:48.000Z",
+    "created": "2016-04-06T20:03:48Z",
     "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
     "description": "The Evil Org threat actor group",
     "id": "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
     "labels": [
         "crime-syndicate"
     ],
-    "modified": "2016-04-06T20:03:48.000Z",
+    "modified": "2016-04-06T20:03:48Z",
     "name": "Evil Org",
     "type": "threat-actor"
 }"""
@@ -26,8 +26,8 @@ def test_threat_actor_example():
     threat_actor = stix2.ThreatActor(
         id="threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
         created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
-        created="2016-04-06T20:03:48.000Z",
-        modified="2016-04-06T20:03:48.000Z",
+        created="2016-04-06T20:03:48Z",
+        modified="2016-04-06T20:03:48Z",
         name="Evil Org",
         description="The Evil Org threat actor group",
         labels=["crime-syndicate"],
@@ -39,14 +39,14 @@ def test_threat_actor_example():
 @pytest.mark.parametrize("data", [
     EXPECTED,
     {
-        "created": "2016-04-06T20:03:48.000Z",
+        "created": "2016-04-06T20:03:48Z",
         "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
         "description": "The Evil Org threat actor group",
         "id": "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
         "labels": [
             "crime-syndicate"
         ],
-        "modified": "2016-04-06T20:03:48.000Z",
+        "modified": "2016-04-06T20:03:48Z",
         "name": "Evil Org",
         "type": "threat-actor"
     },
diff --git a/stix2/test/test_tool.py b/stix2/test/test_tool.py
index 04da7b3..d7d3bef 100644
--- a/stix2/test/test_tool.py
+++ b/stix2/test/test_tool.py
@@ -9,13 +9,13 @@ from .constants import TOOL_ID
 
 
 EXPECTED = """{
-    "created": "2016-04-06T20:03:48.000Z",
+    "created": "2016-04-06T20:03:48Z",
     "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
     "id": "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
     "labels": [
         "remote-access"
     ],
-    "modified": "2016-04-06T20:03:48.000Z",
+    "modified": "2016-04-06T20:03:48Z",
     "name": "VNC",
     "type": "tool"
 }"""
@@ -25,8 +25,8 @@ def test_tool_example():
     tool = stix2.Tool(
         id="tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
         created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
-        created="2016-04-06T20:03:48.000Z",
-        modified="2016-04-06T20:03:48.000Z",
+        created="2016-04-06T20:03:48Z",
+        modified="2016-04-06T20:03:48Z",
         name="VNC",
         labels=["remote-access"],
     )
diff --git a/stix2/test/test_utils.py b/stix2/test/test_utils.py
index c73bcd2..dbc0ed5 100644
--- a/stix2/test/test_utils.py
+++ b/stix2/test/test_utils.py
@@ -17,8 +17,6 @@ eastern = pytz.timezone('US/Eastern')
     (eastern.localize(dt.datetime(2017, 7, 1)), '2017-07-01T04:00:00Z'),
     (dt.datetime(2017, 7, 1), '2017-07-01T00:00:00Z'),
     (dt.datetime(2017, 7, 1, 0, 0, 0, 1), '2017-07-01T00:00:00.000001Z'),
-    (stix2.utils.STIXdatetime(2017, 7, 1, 0, 0, 0, 1, precision='millisecond'), '2017-07-01T00:00:00.000Z'),
-    (stix2.utils.STIXdatetime(2017, 7, 1, 0, 0, 0, 1, precision='second'), '2017-07-01T00:00:00Z'),
 ])
 def test_timestamp_formatting(dttm, timestamp):
     assert stix2.utils.format_datetime(dttm) == timestamp
@@ -35,17 +33,6 @@ def test_parse_datetime(timestamp, dttm):
     assert stix2.utils.parse_into_datetime(timestamp) == dttm
 
 
-@pytest.mark.parametrize('timestamp, dttm, precision', [
-    ('2017-01-01T01:02:03.000001', dt.datetime(2017, 1, 1, 1, 2, 3, 0, tzinfo=pytz.utc), 'millisecond'),
-    ('2017-01-01T01:02:03.001', dt.datetime(2017, 1, 1, 1, 2, 3, 1000, tzinfo=pytz.utc), 'millisecond'),
-    ('2017-01-01T01:02:03.1', dt.datetime(2017, 1, 1, 1, 2, 3, 100000, tzinfo=pytz.utc), 'millisecond'),
-    ('2017-01-01T01:02:03.45', dt.datetime(2017, 1, 1, 1, 2, 3, 450000, tzinfo=pytz.utc), 'millisecond'),
-    ('2017-01-01T01:02:03.45', dt.datetime(2017, 1, 1, 1, 2, 3, tzinfo=pytz.utc), 'second'),
-])
-def test_parse_datetime_precision(timestamp, dttm, precision):
-    assert stix2.utils.parse_into_datetime(timestamp, precision) == dttm
-
-
 @pytest.mark.parametrize('ts', [
     'foobar',
     1,
diff --git a/stix2/test/test_vulnerability.py b/stix2/test/test_vulnerability.py
index 27ab85f..751460c 100644
--- a/stix2/test/test_vulnerability.py
+++ b/stix2/test/test_vulnerability.py
@@ -9,7 +9,7 @@ from .constants import VULNERABILITY_ID
 
 
 EXPECTED = """{
-    "created": "2016-05-12T08:17:27.000Z",
+    "created": "2016-05-12T08:17:27Z",
     "external_references": [
         {
             "external_id": "CVE-2016-1234",
@@ -17,7 +17,7 @@ EXPECTED = """{
         }
     ],
     "id": "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
-    "modified": "2016-05-12T08:17:27.000Z",
+    "modified": "2016-05-12T08:17:27Z",
     "name": "CVE-2016-1234",
     "type": "vulnerability"
 }"""
@@ -26,8 +26,8 @@ EXPECTED = """{
 def test_vulnerability_example():
     vulnerability = stix2.Vulnerability(
         id="vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
-        created="2016-05-12T08:17:27.000Z",
-        modified="2016-05-12T08:17:27.000Z",
+        created="2016-05-12T08:17:27Z",
+        modified="2016-05-12T08:17:27Z",
         name="CVE-2016-1234",
         external_references=[
             stix2.ExternalReference(source_name='cve',