Repository: oasis-open/cti-python-stix2

Issue:
Add documentation for the `patterns` module
https://stix2.readthedocs.io/en/latest/api/stix2.patterns.html is pretty barebones, and we don't have anything in the User's Guide about it.

Discussion:

Patch:
diff --git a/README.rst b/README.rst
index 6770ec6..79b8e7e 100644
--- a/README.rst
+++ b/README.rst
@@ -170,6 +170,9 @@ repositories/maintainers-guide#additionalMaintainers>`__.
 
 **Current Maintainers of this TC Open Repository**
 
+-  `Greg Back <mailto:gback@mitre.org>`__; GitHub ID:
+   https://github.com/gtback/; WWW: `MITRE
+   Corporation <http://www.mitre.org/>`__
 -  `Chris Lenk <mailto:clenk@mitre.org>`__; GitHub ID:
    https://github.com/clenk/; WWW: `MITRE
    Corporation <http://www.mitre.org/>`__
diff --git a/docs/guide/creating.ipynb b/docs/guide/creating.ipynb
index 61bbe15..058aae3 100644
--- a/docs/guide/creating.ipynb
+++ b/docs/guide/creating.ipynb
@@ -881,7 +881,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.6.3"
+   "version": "3.6.5"
   }
  },
  "nbformat": 4,
diff --git a/docs/guide/patterns.ipynb b/docs/guide/patterns.ipynb
new file mode 100644
index 0000000..ee06675
--- /dev/null
+++ b/docs/guide/patterns.ipynb
@@ -0,0 +1,509 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# STIX2 Patterns\n",
+    "\n",
+    "The Python ``stix2`` library supports STIX 2 patterning insofar that patterns may be used for the pattern property of Indicators, identical to the STIX 2 specification. ``stix2`` does not evaluate patterns against STIX 2 content; for that functionality see [cti-pattern-matcher](https://github.com/oasis-open/cti-pattern-matcher).\n",
+    "\n",
+    "Patterns in the ``stix2`` library are built compositely from the bottom up, creating subcomponent expressions first before those at higher levels.\n",
+    "\n",
+    "## API Tips\n",
+    "\n",
+    "### ObservationExpression\n",
+    "\n",
+    "Within the STIX 2 Patterning specification, Observation Expressions denote a complete expression to be evaluated against a discrete observation.  In other words, an Observation Expression must be created to apply to a single Observation instance. This is further made clear by the visual brackets(```[]```) that encapsulate an Observation Expression. Thus, whatever sub expressions that are within the Observation Expression are meant to be matched against the same Observable instance.\n",
+    "\n",
+    "This requirement manifests itself within the ``stix2`` library via ```ObservationExpression```. When creating STIX 2 observation expressions, whenever the current expression is complete, wrap it with ```ObservationExpression()```. This allows the complete pattern expression - no matter its complexity - to be rendered as a proper specification-adhering string. __*Note: When pattern expressions are added to Indicator objects, the expression objects are implicitly converted to string representations*__. While the extra step may seem tedious in the construction of simple pattern expressions, this explicit marking of observation expressions becomes vital when converting the pattern expressions to strings. \n",
+    "\n",
+    "In all the examples, you can observe how in the process of building pattern expressions, when an Observation Expression is completed, it is wrapped with ```ObservationExpression()```.\n",
+    "\n",
+    "### ParentheticalExpression\n",
+    "\n",
+    "Do not be confused by the ```ParentheticalExpression``` object. It is not a distinct expression type but is also used to properly craft pattern expressions by denoting order priority and grouping of expression components. Use it in a similar manner as ```ObservationExpression```, wrapping completed subcomponent expressions with ```ParentheticalExpression()``` if explicit ordering is required. For usage examples with ```ParentheticalExpression```'s, see [here](#Compound-Observation-Expressions).\n",
+    "\n",
+    "### BooleanExpressions vs CompoundObservationExpressions\n",
+    "\n",
+    "Be careful to note the difference between these two very similar pattern components. \n",
+    "\n",
+    "__BooleanExpressions__\n",
+    "\n",
+    " - [AndBooleanExpression](../api/stix2.patterns.rst#stix2.patterns.AndBooleanExpression)\n",
+    " - [OrbooleanExpression](../api/stix2.patterns.rst#stix2.patterns.OrBooleanExpression)\n",
+    " \n",
+    "  __Usage__: When the boolean sub-expressions refer to the *same* root object \n",
+    "\n",
+    "  __Example__:\n",
+    "    ```[domain-name:value = \"www.5z8.info\" AND domain-name:resolvess_to_refs[*].value = \"'198.51.100.1/32'\"]```\n",
+    "    \n",
+    "  __Rendering__: when pattern is rendered, brackets or parenthesis will encapsulate boolean expression\n",
+    " \n",
+    "__CompoundObservationExpressions__\n",
+    "\n",
+    " - [AndObservationExpression](../api/stix2.patterns.rst#stix2.patterns.AndObservationExpression)\n",
+    " - [OrObservationExpression](../api/stix2.patterns.rst#stix2.patterns.OrObservationExpression)\n",
+    " \n",
+    "  __Usage__: When the boolean sub-expressions refer to *different* root objects\n",
+    "\n",
+    "  __Example__:\n",
+    "    ```[file:name=\"foo.dll\"] AND [process:name = \"procfoo\"]```\n",
+    "    \n",
+    "  __Rendering__: when pattern is rendered, brackets will encapsulate each boolean sub-expression\n",
+    "\n",
+    "\n",
+    "\n",
+    "## Examples\n",
+    "\n",
+    "### Comparison Expressions"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from stix2 import DomainName, File, IPv4Address\n",
+    "from stix2 import (ObjectPath, EqualityComparisonExpression, ObservationExpression,\n",
+    "                   GreaterThanComparisonExpression, IsSubsetComparisonExpression,\n",
+    "                   FloatConstant, StringConstant)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Equality Comparison expressions"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\t[domain-name:value = 'site.of.interest.zaz']\n",
+      "\n",
+      "\t[file:parent_directory_ref.path = 'C:\\\\Windows\\\\System32']\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "lhs = ObjectPath(\"domain-name\", [\"value\"])\n",
+    "ece_1 = ObservationExpression(EqualityComparisonExpression(lhs, \"site.of.interest.zaz\"))\n",
+    "print(\"\\t{}\\n\".format(ece_1))\n",
+    "\n",
+    "lhs = ObjectPath(\"file\", [\"parent_directory_ref\",\"path\"])\n",
+    "ece_2 = ObservationExpression(EqualityComparisonExpression(lhs, \"C:\\\\Windows\\\\System32\"))\n",
+    "print(\"\\t{}\\n\".format(ece_2))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Greater-than Comparison expressions"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\t[file:extensions.windows-pebinary-ext.sections[*].entropy > 7.0]\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "lhs = ObjectPath(\"file\", [\"extensions\", \"windows-pebinary-ext\", \"sections[*]\", \"entropy\"])\n",
+    "gte = ObservationExpression(GreaterThanComparisonExpression(lhs, FloatConstant(\"7.0\")))\n",
+    "print(\"\\t{}\\n\".format(gte))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### IsSubset Comparison expressions"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\t[network-traffic:dst_ref.value ISSUBSET '2001:0db8:dead:beef:0000:0000:0000:0000/64']\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "lhs = ObjectPath(\"network-traffic\", [\"dst_ref\", \"value\"])\n",
+    "iss = ObservationExpression(IsSubsetComparisonExpression(lhs, StringConstant(\"2001:0db8:dead:beef:0000:0000:0000:0000/64\")))\n",
+    "print(\"\\t{}\\n\".format(iss))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Compound Observation Expressions"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from stix2 import (IntegerConstant, HashConstant, ObjectPath,\n",
+    "                   EqualityComparisonExpression, AndBooleanExpression,\n",
+    "                   OrBooleanExpression, ParentheticalExpression,\n",
+    "                   AndObservationExpression, OrObservationExpression,\n",
+    "                   FollowedByObservationExpression, ObservationExpression)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### AND boolean"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "(AND)\n",
+      "[email-message:sender_ref.value = 'stark@example.com' AND email-message:subject = 'Conference Info']\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "ece3 = EqualityComparisonExpression(ObjectPath(\"email-message\", [\"sender_ref\", \"value\"]), \"stark@example.com\")\n",
+    "ece4 = EqualityComparisonExpression(ObjectPath(\"email-message\", [\"subject\"]), \"Conference Info\")\n",
+    "abe = ObservationExpression(AndBooleanExpression([ece3, ece4]))\n",
+    "print(\"(AND)\\n{}\\n\".format(abe))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### OR boolean"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "(OR)\n",
+      "[url:value = 'http://example.com/foo' OR url:value = 'http://example.com/bar']\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "ece5 = EqualityComparisonExpression(ObjectPath(\"url\", [\"value\"]), \"http://example.com/foo\")\n",
+    "ece6 = EqualityComparisonExpression(ObjectPath(\"url\", [\"value\"]), \"http://example.com/bar\")\n",
+    "obe = ObservationExpression(OrBooleanExpression([ece5, ece6]))\n",
+    "print(\"(OR)\\n{}\\n\".format(obe))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### ( OR ) AND   boolean"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "(OR,AND)\n",
+      "[(file:name = 'pdf.exe' OR file:size = 371712) AND file:created = 2014-01-13 07:03:17+00:00]\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "ece7 = EqualityComparisonExpression(ObjectPath(\"file\", [\"name\"]), \"pdf.exe\")\n",
+    "ece8 = EqualityComparisonExpression(ObjectPath(\"file\", [\"size\"]), IntegerConstant(\"371712\"))\n",
+    "ece9 = EqualityComparisonExpression(ObjectPath(\"file\", [\"created\"]), \"2014-01-13T07:03:17Z\")\n",
+    "obe1 = OrBooleanExpression([ece7, ece8])\n",
+    "pobe = ParentheticalExpression(obe1)\n",
+    "abe1 = ObservationExpression(AndBooleanExpression([pobe, ece9]))\n",
+    "print(\"(OR,AND)\\n{}\\n\".format(abe1))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### ( AND ) OR ( OR )  observation"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "(AND,OR,OR)\n",
+      "([file:name = 'foo.dll'] AND [win-registry-key:key = 'HKEY_LOCAL_MACHINE\\\\foo\\\\bar']) OR [process:name = 'fooproc' OR process:name = 'procfoo']\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "ece20 = ObservationExpression(EqualityComparisonExpression(ObjectPath(\"file\", [\"name\"]), \"foo.dll\"))\n",
+    "ece21 = ObservationExpression(EqualityComparisonExpression(ObjectPath(\"win-registry-key\", [\"key\"]), \"HKEY_LOCAL_MACHINE\\\\foo\\\\bar\"))\n",
+    "ece22 = EqualityComparisonExpression(ObjectPath(\"process\", [\"name\"]), \"fooproc\")\n",
+    "ece23 = EqualityComparisonExpression(ObjectPath(\"process\", [\"name\"]), \"procfoo\")\n",
+    "# NOTE: we need to use AND/OR observation expression instead of just boolean \n",
+    "# expressions as the operands are not on the same object-type\n",
+    "aoe = ParentheticalExpression(AndObservationExpression([ece20, ece21]))\n",
+    "obe2 = ObservationExpression(OrBooleanExpression([ece22, ece23]))\n",
+    "ooe = OrObservationExpression([aoe, obe2])\n",
+    "print(\"(AND,OR,OR)\\n{}\\n\".format(ooe))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### FOLLOWED-BY"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "(FollowedBy)\n",
+      "[file:hashes.MD5 = '79054025255fb1a26e4bc422aef54eb4'] FOLLOWEDBY [win-registry-key:key = 'HKEY_LOCAL_MACHINE\\\\foo\\\\bar']\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "ece10 = ObservationExpression(EqualityComparisonExpression(ObjectPath(\"file\", [\"hashes\", \"MD5\"]), HashConstant(\"79054025255fb1a26e4bc422aef54eb4\", \"MD5\")))\n",
+    "ece11 = ObservationExpression(EqualityComparisonExpression(ObjectPath(\"win-registry-key\", [\"key\"]), \"HKEY_LOCAL_MACHINE\\\\foo\\\\bar\"))\n",
+    "fbe = FollowedByObservationExpression([ece10, ece11])\n",
+    "print(\"(FollowedBy)\\n{}\\n\".format(fbe))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Qualified Observation Expressions"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from stix2 import (TimestampConstant, HashConstant, ObjectPath, EqualityComparisonExpression,\n",
+    "                   AndBooleanExpression, WithinQualifier, RepeatQualifier, StartStopQualifier,\n",
+    "                   QualifiedObservationExpression, FollowedByObservationExpression,\n",
+    "                   ParentheticalExpression, ObservationExpression)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### WITHIN"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "(WITHIN)\n",
+      "([file:hashes.MD5 = '79054025255fb1a26e4bc422aef54eb4'] FOLLOWEDBY [win-registry-key:key = 'HKEY_LOCAL_MACHINE\\\\foo\\\\bar']) WITHIN 300 SECONDS\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "ece10 = ObservationExpression(EqualityComparisonExpression(ObjectPath(\"file\", [\"hashes\", \"MD5\"]), HashConstant(\"79054025255fb1a26e4bc422aef54eb4\", \"MD5\")))\n",
+    "ece11 = ObservationExpression(EqualityComparisonExpression(ObjectPath(\"win-registry-key\", [\"key\"]), \"HKEY_LOCAL_MACHINE\\\\foo\\\\bar\"))\n",
+    "fbe = FollowedByObservationExpression([ece10, ece11])\n",
+    "par = ParentheticalExpression(fbe)\n",
+    "qoe = QualifiedObservationExpression(par, WithinQualifier(300))\n",
+    "print(\"(WITHIN)\\n{}\\n\".format(qoe))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### REPEATS, WITHIN"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "(REPEAT, WITHIN)\n",
+      "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] REPEATS 5 TIMES WITHIN 180 SECONDS\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "ece12 = EqualityComparisonExpression(ObjectPath(\"network-traffic\", [\"dst_ref\", \"type\"]), \"domain-name\")\n",
+    "ece13 = EqualityComparisonExpression(ObjectPath(\"network-traffic\", [\"dst_ref\", \"value\"]), \"example.com\")\n",
+    "abe2 = ObservationExpression(AndBooleanExpression([ece12, ece13]))\n",
+    "qoe1 = QualifiedObservationExpression(QualifiedObservationExpression(abe2, RepeatQualifier(5)), WithinQualifier(180))\n",
+    "print(\"(REPEAT, WITHIN)\\n{}\\n\".format(qoe1))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### START, STOP"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "(START-STOP)\n",
+      "[file:name = 'foo.dll'] START t'2016-06-01T00:00:00Z' STOP t'2016-07-01T00:00:00Z'\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "ece14 = ObservationExpression(EqualityComparisonExpression(ObjectPath(\"file\", [\"name\"]), \"foo.dll\"))\n",
+    "ssq = StartStopQualifier(TimestampConstant('2016-06-01T00:00:00Z'), TimestampConstant('2016-07-01T00:00:00Z'))\n",
+    "qoe2 = QualifiedObservationExpression(ece14, ssq)\n",
+    "print(\"(START-STOP)\\n{}\\n\".format(qoe2))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Attaching patterns to STIX2 Domain objects\n",
+    "\n",
+    "\n",
+    "### Example"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "{\n",
+      "    \"type\": \"indicator\",\n",
+      "    \"id\": \"indicator--219bc5fc-fdbf-4b54-a2fc-921be7ab3acb\",\n",
+      "    \"created\": \"2018-08-29T23:58:00.548Z\",\n",
+      "    \"modified\": \"2018-08-29T23:58:00.548Z\",\n",
+      "    \"name\": \"Cryptotorch\",\n",
+      "    \"pattern\": \"[file:name = '$$t00rzch$$.elf']\",\n",
+      "    \"valid_from\": \"2018-08-29T23:58:00.548391Z\",\n",
+      "    \"labels\": [\n",
+      "        \"malware\",\n",
+      "        \"ransomware\"\n",
+      "    ]\n",
+      "}\n"
+     ]
+    }
+   ],
+   "source": [
+    "from stix2 import Indicator, EqualityComparisonExpression, ObservationExpression\n",
+    "\n",
+    "ece14 = ObservationExpression(EqualityComparisonExpression(ObjectPath(\"file\", [\"name\"]), \"$$t00rzch$$.elf\"))\n",
+    "ind = Indicator(name=\"Cryptotorch\", labels=[\"malware\", \"ransomware\"], pattern=ece14)\n",
+    "print(ind)"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.6.3"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/stix2/patterns.py b/stix2/patterns.py
index 3f9cbd9..146ec04 100644
--- a/stix2/patterns.py
+++ b/stix2/patterns.py
@@ -18,6 +18,11 @@ class _Constant(object):
 
 
 class StringConstant(_Constant):
+    """Pattern string constant
+
+    Args:
+        value (str): string value
+    """
     def __init__(self, value):
         self.value = value
 
@@ -26,17 +31,27 @@ class StringConstant(_Constant):
 
 
 class TimestampConstant(_Constant):
+    """Pattern timestamp constant
+
+    Args:
+        value (datetime.datetime OR str): if string, must be a timestamp string
+    """
     def __init__(self, value):
         try:
             self.value = parse_into_datetime(value)
         except Exception:
-            raise ValueError("must be a datetime object or timestamp string.")
+            raise ValueError("Must be a datetime object or timestamp string.")
 
     def __str__(self):
         return "t%s" % repr(self.value)
 
 
 class IntegerConstant(_Constant):
+    """Pattern interger constant
+
+    Args:
+        value (int): integer value
+    """
     def __init__(self, value):
         try:
             self.value = int(value)
@@ -59,6 +74,13 @@ class FloatConstant(_Constant):
 
 
 class BooleanConstant(_Constant):
+    """Pattern boolean constant
+
+    Args:
+       value (str OR int):
+           (str) 'true', 't' for True; 'false', 'f' for False
+           (int) 1 for True; 0 for False
+    """
     def __init__(self, value):
         if isinstance(value, bool):
             self.value = value
@@ -106,6 +128,15 @@ _HASH_REGEX = {
 
 
 class HashConstant(StringConstant):
+    """Pattern hash constant
+
+    Args:
+        value (str): hash value
+        type (str): hash algorithm name. Supported hash algorithms:
+            "MD5", "MD6", RIPEMD160", "SHA1", "SHA224", "SHA256",
+            "SHA384", "SHA512", "SHA3224", "SHA3256", "SHA3384",
+            "SHA3512", "SSDEEP", "WHIRLPOOL"
+    """
     def __init__(self, value, type):
         key = type.upper().replace('-', '')
         if key in _HASH_REGEX:
@@ -116,7 +147,11 @@ class HashConstant(StringConstant):
 
 
 class BinaryConstant(_Constant):
+    """Pattern binary constant
 
+    Args:
+        value (str): base64 encoded string value
+    """
     def __init__(self, value):
         try:
             base64.b64decode(value)
@@ -129,6 +164,11 @@ class BinaryConstant(_Constant):
 
 
 class HexConstant(_Constant):
+    """Pattern hexadecimal constant
+
+    Args:
+        value (str): hexadecimal value
+    """
     def __init__(self, value):
         if not re.match('^([a-fA-F0-9]{2})+$', value):
             raise ValueError("must contain an even number of hexadecimal characters")
@@ -139,6 +179,11 @@ class HexConstant(_Constant):
 
 
 class ListConstant(_Constant):
+    """Pattern list constant
+
+    Args:
+        value (list): list of values
+    """
     def __init__(self, values):
         self.value = values
 
@@ -147,6 +192,12 @@ class ListConstant(_Constant):
 
 
 def make_constant(value):
+    """Convert value to Pattern constant, best effort attempt
+    at determining root value type and corresponding conversion
+
+    Args:
+        value: value to convert to Pattern constant
+    """
     if isinstance(value, _Constant):
         return value
 
@@ -182,6 +233,16 @@ class _ObjectPathComponent(object):
 
 
 class BasicObjectPathComponent(_ObjectPathComponent):
+    """Basic object path component (for an observation or expression)
+
+    By "Basic", implies that the object path component is not a
+    list, object reference or futher referenced property, i.e. terminal
+    component
+
+    Args:
+        property_name (str): object property name
+        is_key (bool): is dictionary key, default: False
+    """
     def __init__(self, property_name, is_key=False):
         self.property_name = property_name
         # TODO: set is_key to True if this component is a dictionary key
@@ -192,6 +253,12 @@ class BasicObjectPathComponent(_ObjectPathComponent):
 
 
 class ListObjectPathComponent(_ObjectPathComponent):
+    """List object path component (for an observation or expression)
+
+    Args:
+        property_name (str): list object property name
+        index (int): index of the list property's value that is specified
+    """
     def __init__(self, property_name, index):
         self.property_name = property_name
         self.index = index
@@ -201,6 +268,11 @@ class ListObjectPathComponent(_ObjectPathComponent):
 
 
 class ReferenceObjectPathComponent(_ObjectPathComponent):
+    """Reference object path component (for an observation or expression)
+
+    Args:
+        reference_property_name (str): reference object property name
+    """
     def __init__(self, reference_property_name):
         self.property_name = reference_property_name
 
@@ -209,6 +281,12 @@ class ReferenceObjectPathComponent(_ObjectPathComponent):
 
 
 class ObjectPath(object):
+    """Pattern operand object (property) path
+
+    Args:
+        object_type_name (str): name of object type for corresponding object path component
+        property_path (_ObjectPathComponent OR str): object path
+    """
     def __init__(self, object_type_name, property_path):
         self.object_type_name = object_type_name
         self.property_path = [x if isinstance(x, _ObjectPathComponent) else
@@ -219,11 +297,17 @@ class ObjectPath(object):
         return "%s:%s" % (self.object_type_name, ".".join(["%s" % x for x in self.property_path]))
 
     def merge(self, other):
+        """Extend the object property with that of the supplied object property path"""
         self.property_path.extend(other.property_path)
         return self
 
     @staticmethod
     def make_object_path(lhs):
+        """Create ObjectPath from string encoded object path
+
+        Args:
+            lhs (str): object path of left-hand-side component of expression
+        """
         path_as_parts = lhs.split(":")
         return ObjectPath(path_as_parts[0], path_as_parts[1].split("."))
 
@@ -233,6 +317,14 @@ class _PatternExpression(object):
 
 
 class _ComparisonExpression(_PatternExpression):
+    """Pattern Comparison Expression
+
+    Args:
+        operator (str): operator of comparison expression
+        lhs (ObjectPath OR str): object path of left-hand-side component of expression
+        rhs (ObjectPath OR str): object path of right-hand-side component of expression
+        negated (bool): comparison expression negated. Default: False
+    """
     def __init__(self, operator, lhs, rhs, negated=False):
         if operator == "=" and isinstance(rhs, (ListConstant, list)):
             self.operator = "IN"
@@ -257,56 +349,134 @@ class _ComparisonExpression(_PatternExpression):
 
 
 class EqualityComparisonExpression(_ComparisonExpression):
+    """Pattern Equality Comparison Expression
+
+    Args:
+        lhs (ObjectPath OR str): object path of left-hand-side component of expression
+        rhs (ObjectPath OR str): object path of right-hand-side component of expression
+        negated (bool): comparison expression negated. Default: False
+    """
     def __init__(self, lhs, rhs, negated=False):
         super(EqualityComparisonExpression, self).__init__("=", lhs, rhs, negated)
 
 
 class GreaterThanComparisonExpression(_ComparisonExpression):
+    """Pattern Greater-than Comparison Expression
+
+    Args:
+        lhs (ObjectPath OR str): object path of left-hand-side component of expression
+        rhs (ObjectPath OR str): object path of right-hand-side component of expression
+        negated (bool): comparison expression negated. Default: False
+    """
     def __init__(self, lhs, rhs, negated=False):
         super(GreaterThanComparisonExpression, self).__init__(">", lhs, rhs, negated)
 
 
 class LessThanComparisonExpression(_ComparisonExpression):
+    """Pattern Less-than Comparison Expression
+
+    Args:
+        lhs (ObjectPath OR str): object path of left-hand-side component of expression
+        rhs (ObjectPath OR str): object path of right-hand-side component of expression
+        negated (bool): comparison expression negated. Default: False
+    """
     def __init__(self, lhs, rhs, negated=False):
         super(LessThanComparisonExpression, self).__init__("<", lhs, rhs, negated)
 
 
 class GreaterThanEqualComparisonExpression(_ComparisonExpression):
+    """Pattern Greater-Than-or-Equal-to Comparison Expression
+
+    Args:
+        lhs (ObjectPath OR str): object path of left-hand-side component of expression
+        rhs (ObjectPath OR str): object path of right-hand-side component of expression
+        negated (bool): comparison expression negated. Default: False
+    """
     def __init__(self, lhs, rhs, negated=False):
         super(GreaterThanEqualComparisonExpression, self).__init__(">=", lhs, rhs, negated)
 
 
 class LessThanEqualComparisonExpression(_ComparisonExpression):
+    """Pattern Less-Than-or-Equal-to Comparison Expression
+
+    Args:
+        lhs (ObjectPath OR str): object path of left-hand-side component of expression
+        rhs (ObjectPath OR str): object path of right-hand-side component of expression
+        negated (bool): comparison expression negated. Default: False
+    """
+
     def __init__(self, lhs, rhs, negated=False):
         super(LessThanEqualComparisonExpression, self).__init__("<=", lhs, rhs, negated)
 
 
 class InComparisonExpression(_ComparisonExpression):
+    """'in' Comparison Expression
+
+    Args:
+        lhs (ObjectPath OR str): object path of left-hand-side component of expression
+        rhs (ObjectPath OR str): object path of right-hand-side component of expression
+        negated (bool): comparison expression negated. Default: False
+    """
     def __init__(self, lhs, rhs, negated=False):
         super(InComparisonExpression, self).__init__("IN", lhs, rhs, negated)
 
 
 class LikeComparisonExpression(_ComparisonExpression):
+    """'like' Comparison Expression
+
+    Args:
+        lhs (ObjectPath OR str): object path of left-hand-side component of expression
+        rhs (ObjectPath OR str): object path of right-hand-side component of expression
+        negated (bool): comparison expression negated. Default: False
+    """
+
     def __init__(self, lhs, rhs, negated=False):
         super(LikeComparisonExpression, self).__init__("LIKE", lhs, rhs, negated)
 
 
 class MatchesComparisonExpression(_ComparisonExpression):
+    """'Matches' Comparison Expression
+
+    Args:
+        lhs (ObjectPath OR str): object path of left-hand-side component of expression
+        rhs (ObjectPath OR str): object path of right-hand-side component of expression
+        negated (bool): comparison expression negated. Default: False
+    """
     def __init__(self, lhs, rhs, negated=False):
         super(MatchesComparisonExpression, self).__init__("MATCHES", lhs, rhs, negated)
 
 
 class IsSubsetComparisonExpression(_ComparisonExpression):
-        def __init__(self, lhs, rhs, negated=False):
-            super(IsSubsetComparisonExpression, self).__init__("ISSUBSET", lhs, rhs, negated)
+    """ 'is subset' Comparison Expression
+
+    Args:
+        lhs (ObjectPath OR str): object path of left-hand-side component of expression
+        rhs (ObjectPath OR str): object path of right-hand-side component of expression
+        negated (bool): comparison expression negated. Default: False
+    """
+    def __init__(self, lhs, rhs, negated=False):
+        super(IsSubsetComparisonExpression, self).__init__("ISSUBSET", lhs, rhs, negated)
 
 
 class IsSupersetComparisonExpression(_ComparisonExpression):
-        def __init__(self, lhs, rhs, negated=False):
-            super(IsSupersetComparisonExpression, self).__init__("ISSUPERSET", lhs, rhs, negated)
+    """ 'is super set' Comparison Expression
+
+    Args:
+        lhs (ObjectPath OR str): object path of left-hand-side component of expression
+        rhs (ObjectPath OR str): object path of right-hand-side component of expression
+        negated (bool): comparison expression negated. Default: False
+    """
+    def __init__(self, lhs, rhs, negated=False):
+        super(IsSupersetComparisonExpression, self).__init__("ISSUPERSET", lhs, rhs, negated)
 
 
 class _BooleanExpression(_PatternExpression):
+    """Boolean Pattern Expression
+
+    Args:
+        operator (str): boolean operator
+        operands (list): boolean operands
+    """
     def __init__(self, operator, operands):
         self.operator = operator
         self.operands = []
@@ -322,21 +492,37 @@ class _BooleanExpression(_PatternExpression):
     def __str__(self):
         sub_exprs = []
         for o in self.operands:
-            sub_exprs.append("%s" % o)
+            sub_exprs.append(str(o))
         return (" " + self.operator + " ").join(sub_exprs)
 
 
 class AndBooleanExpression(_BooleanExpression):
+    """'AND' Boolean Pattern Expression. Only use if both operands are of
+    the same root object.
+
+    Args:
+        operands (list): AND operands
+    """
     def __init__(self, operands):
         super(AndBooleanExpression, self).__init__("AND", operands)
 
 
 class OrBooleanExpression(_BooleanExpression):
+    """'OR' Boolean Pattern Expression. Only use if both operands are of the same root object
+
+    Args:
+        operands (list): OR operands
+    """
     def __init__(self, operands):
         super(OrBooleanExpression, self).__init__("OR", operands)
 
 
 class ObservationExpression(_PatternExpression):
+    """Observation Expression
+
+    Args:
+        operand (str): observation expression operand
+    """
     def __init__(self, operand):
         self.operand = operand
 
@@ -345,6 +531,12 @@ class ObservationExpression(_PatternExpression):
 
 
 class _CompoundObservationExpression(_PatternExpression):
+    """Compound Observation Expression
+
+    Args:
+        operator (str): compound observation operator
+        operands (str): compound observation operands
+    """
     def __init__(self, operator, operands):
         self.operator = operator
         self.operands = operands
@@ -357,21 +549,41 @@ class _CompoundObservationExpression(_PatternExpression):
 
 
 class AndObservationExpression(_CompoundObservationExpression):
+    """'AND' Compound Observation Pattern Expression
+
+    Args:
+        operands (str): compound observation operands
+    """
     def __init__(self, operands):
         super(AndObservationExpression, self).__init__("AND", operands)
 
 
 class OrObservationExpression(_CompoundObservationExpression):
+    """Pattern 'OR' Compound Observation Expression
+
+    Args:
+        operands (str): compound observation operands
+    """
     def __init__(self, operands):
         super(OrObservationExpression, self).__init__("OR", operands)
 
 
 class FollowedByObservationExpression(_CompoundObservationExpression):
+    """Pattern 'Followed by' Compound Observation Expression
+
+    Args:
+        operands (str): compound observation operands
+    """
     def __init__(self, operands):
         super(FollowedByObservationExpression, self).__init__("FOLLOWEDBY", operands)
 
 
 class ParentheticalExpression(_PatternExpression):
+    """Pattern Parenthetical Observation Expression
+
+    Args:
+       exp (str): observation expression
+    """
     def __init__(self, exp):
         self.expression = exp
         if hasattr(exp, "root_type"):
@@ -386,6 +598,11 @@ class _ExpressionQualifier(_PatternExpression):
 
 
 class RepeatQualifier(_ExpressionQualifier):
+    """Pattern Repeat Qualifier
+
+    Args:
+        times_to_repeat (int): times the qualifiers is repeated
+    """
     def __init__(self, times_to_repeat):
         if isinstance(times_to_repeat, IntegerConstant):
             self.times_to_repeat = times_to_repeat
@@ -399,6 +616,11 @@ class RepeatQualifier(_ExpressionQualifier):
 
 
 class WithinQualifier(_ExpressionQualifier):
+    """Pattern 'Within' Qualifier
+
+    Args:
+        number_of_seconds (int): seconds value for 'within' qualifier
+    """
     def __init__(self, number_of_seconds):
         if isinstance(number_of_seconds, IntegerConstant):
             self.number_of_seconds = number_of_seconds
@@ -412,6 +634,12 @@ class WithinQualifier(_ExpressionQualifier):
 
 
 class StartStopQualifier(_ExpressionQualifier):
+    """Pattern Start/Stop Qualifier
+
+    Args:
+        start_time (TimestampConstant OR datetime.date): start timestamp for qualifier
+        stop_time (TimestampConstant OR datetime.date): stop timestamp for qualifier
+    """
     def __init__(self, start_time, stop_time):
         if isinstance(start_time, TimestampConstant):
             self.start_time = start_time
@@ -431,6 +659,12 @@ class StartStopQualifier(_ExpressionQualifier):
 
 
 class QualifiedObservationExpression(_PatternExpression):
+    """Pattern Qualified Observation Expression
+
+    Args:
+        observation_expression (PatternExpression OR _CompoundObservationExpression OR ): pattern expression
+        qualifier (_ExpressionQualifier): pattern expression qualifier
+    """
     def __init__(self, observation_expression, qualifier):
         self.observation_expression = observation_expression
         self.qualifier = qualifier


Test patch:
diff --git a/stix2/test/test_pattern_expressions.py b/stix2/test/test_pattern_expressions.py
index 14e3774..a4d0a5c 100644
--- a/stix2/test/test_pattern_expressions.py
+++ b/stix2/test/test_pattern_expressions.py
@@ -9,6 +9,7 @@ def test_create_comparison_expression():
 
     exp = stix2.EqualityComparisonExpression("file:hashes.'SHA-256'",
                                              stix2.HashConstant("aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f", "SHA-256"))   # noqa
+
     assert str(exp) == "file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f'"
 
 
@@ -18,6 +19,7 @@ def test_boolean_expression():
     exp2 = stix2.MatchesComparisonExpression("email-message:body_multipart[*].body_raw_ref.name",
                                              stix2.StringConstant("^Final Report.+\\.exe$"))
     exp = stix2.AndBooleanExpression([exp1, exp2])
+
     assert str(exp) == "email-message:from_ref.value MATCHES '.+\\\\@example\\\\.com$' AND email-message:body_multipart[*].body_raw_ref.name MATCHES '^Final Report.+\\\\.exe$'"  # noqa
 
 
@@ -66,9 +68,8 @@ def test_file_observable_expression():
                                                   "aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f",
                                                   'SHA-256'))
     exp2 = stix2.EqualityComparisonExpression("file:mime_type", stix2.StringConstant("application/x-pdf"))
-    bool_exp = stix2.AndBooleanExpression([exp1, exp2])
-    exp = stix2.ObservationExpression(bool_exp)
-    assert str(exp) == "[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f' AND file:mime_type = 'application/x-pdf']"  # noqa
+    bool_exp = stix2.ObservationExpression(stix2.AndBooleanExpression([exp1, exp2]))
+    assert str(bool_exp) == "[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f' AND file:mime_type = 'application/x-pdf']"  # noqa
 
 
 @pytest.mark.parametrize("observation_class, op", [
@@ -109,14 +110,12 @@ def test_artifact_payload():
                                               "application/vnd.tcpdump.pcap")
     exp2 = stix2.MatchesComparisonExpression("artifact:payload_bin",
                                              stix2.StringConstant("\\xd4\\xc3\\xb2\\xa1\\x02\\x00\\x04\\x00"))
-    and_exp = stix2.AndBooleanExpression([exp1, exp2])
-    exp = stix2.ObservationExpression(and_exp)
-    assert str(exp) == "[artifact:mime_type = 'application/vnd.tcpdump.pcap' AND artifact:payload_bin MATCHES '\\\\xd4\\\\xc3\\\\xb2\\\\xa1\\\\x02\\\\x00\\\\x04\\\\x00']"  # noqa
+    and_exp = stix2.ObservationExpression(stix2.AndBooleanExpression([exp1, exp2]))
+    assert str(and_exp) == "[artifact:mime_type = 'application/vnd.tcpdump.pcap' AND artifact:payload_bin MATCHES '\\\\xd4\\\\xc3\\\\xb2\\\\xa1\\\\x02\\\\x00\\\\x04\\\\x00']"  # noqa
 
 
 def test_greater_than_python_constant():
-    exp1 = stix2.GreaterThanComparisonExpression("file:extensions.windows-pebinary-ext.sections[*].entropy",
-                                                 7.0)
+    exp1 = stix2.GreaterThanComparisonExpression("file:extensions.windows-pebinary-ext.sections[*].entropy", 7.0)
     exp = stix2.ObservationExpression(exp1)
     assert str(exp) == "[file:extensions.windows-pebinary-ext.sections[*].entropy > 7.0]"
 
@@ -129,14 +128,14 @@ def test_greater_than():
 
 
 def test_less_than():
-    exp = stix2.LessThanComparisonExpression("file:size",
-                                             1024)
+    exp = stix2.LessThanComparisonExpression("file:size", 1024)
     assert str(exp) == "file:size < 1024"
 
 
 def test_greater_than_or_equal():
     exp = stix2.GreaterThanEqualComparisonExpression("file:size",
                                                      1024)
+
     assert str(exp) == "file:size >= 1024"
 
 
@@ -261,7 +260,7 @@ def test_invalid_integer_constant():
 def test_invalid_timestamp_constant():
     with pytest.raises(ValueError) as excinfo:
         stix2.TimestampConstant('foo')
-    assert 'must be a datetime object or timestamp string' in str(excinfo)
+    assert 'Must be a datetime object or timestamp string' in str(excinfo)
 
 
 def test_invalid_float_constant():