From 418e0be1efac11942df653ff64283eda59aba036 Mon Sep 17 00:00:00 2001
From: Sandy Walsh <sandy@sandywalsh.com>
Date: Wed, 21 May 2014 12:00:06 +0000
Subject: [PATCH] first pass ... the essentials

---
 README.md                      |  3 ++
 notification_utils/__init__.py | 65 ++++++++++++++++++++++++++++++++++
 requirements.txt               |  1 +
 setup.cfg                      | 25 +++++++++++++
 setup.py                       |  8 +++++
 test_requirements.txt          |  1 +
 tests/test_utils.py            | 43 ++++++++++++++++++++++
 tox.ini                        | 14 ++++++++
 8 files changed, 160 insertions(+)
 create mode 100644 notification_utils/__init__.py
 create mode 100644 requirements.txt
 create mode 100644 setup.cfg
 create mode 100644 setup.py
 create mode 100644 test_requirements.txt
 create mode 100644 tests/test_utils.py
 create mode 100644 tox.ini

diff --git a/README.md b/README.md
index bd6620d..3d87489 100644
--- a/README.md
+++ b/README.md
@@ -2,3 +2,6 @@ notification_utils
 ==================
 
 Utilities for dealing with OpenStack Notifications
+
+Includes datetime <-> Decimal conversion and Json marshalling handlers
+to name a few (for now)
diff --git a/notification_utils/__init__.py b/notification_utils/__init__.py
new file mode 100644
index 0000000..862ce44
--- /dev/null
+++ b/notification_utils/__init__.py
@@ -0,0 +1,65 @@
+# Copyright (c) 2014 Dark Secret Software Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import calendar
+import collections
+import datetime
+import decimal
+import json
+
+
+def now():
+    """Broken out for testing."""
+    return datetime.datetime.utcnow()
+
+
+def dt_to_decimal(utc):
+    decimal.getcontext().prec = 30
+    return decimal.Decimal(str(calendar.timegm(utc.utctimetuple()))) + \
+           (decimal.Decimal(str(utc.microsecond)) /
+           decimal.Decimal("1000000.0"))
+
+
+def dt_from_decimal(dec):
+    if dec == None:
+        return "n/a"
+    integer = int(dec)
+    micro = (dec - decimal.Decimal(integer)) * decimal.Decimal(1000000)
+
+    daittyme = datetime.datetime.utcfromtimestamp(integer)
+    return daittyme.replace(microsecond=micro)
+
+
+class DateTimeEncoder(json.JSONEncoder):
+    def default(self, obj):
+        if isinstance(obj, datetime.datetime):
+            if obj.utcoffset() is not None:
+                obj = obj - obj.utcoffset()
+            return str(dt_to_decimal(obj))
+        return super(DateTimeEncoder, self).default(obj)
+
+
+# This is a hack for comparing structures load'ed from json
+# (which are always unicode) back to strings. It's used
+# for assertEqual() in the tests and is very slow and expensive.
+def unicode_to_string(data):
+    if isinstance(data, basestring):
+        return str(data)
+    elif isinstance(data, collections.Mapping):
+        return dict(map(unicode_to_string, data.iteritems()))
+    elif isinstance(data, collections.Iterable):
+        return type(data)(map(unicode_to_string, data))
+    else:
+        return data
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..80ab0c7
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+dateutils
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..549d312
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,25 @@
+[metadata]
+name = notification_utils
+author = Dark Secret Software Inc.
+author-email = admin@darksecretsoftware.com
+summary = Utilities for dealing with OpenStack Notifications
+description-file = README.md
+license = Apache-2
+classifier =
+    Development Status :: 2 - Pre-Alpha
+        Environment :: Console
+        Intended Audience :: Developers
+        Intended Audience :: Information Technology
+        License :: OSI Approved :: Apache Software License
+        Operating System :: OS Independent
+        Programming Language :: Python
+        Topic :: Software Development :: Libraries :: Python Modules
+home-page = https://github.com/StackTach/notification_utils
+keywords =
+    openstack
+    notifications
+    events
+    utilities
+[files]
+packages =
+    notification_utils
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..aa2d8a0
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,8 @@
+#!/usr/bin/env python
+
+from setuptools import setup
+
+setup(
+    setup_requires=['pbr'],
+    pbr=True,
+)
diff --git a/test_requirements.txt b/test_requirements.txt
new file mode 100644
index 0000000..f3c7e8e
--- /dev/null
+++ b/test_requirements.txt
@@ -0,0 +1 @@
+nose
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..caab86e
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,43 @@
+import datetime
+import decimal
+import unittest
+
+import dateutil.tz
+
+import notification_utils
+
+
+class TestUtils(unittest.TestCase):
+    def setUp(self):
+        self.handler = notification_utils.DateTimeEncoder()
+
+    def test_handle_datetime_non_datetime(self):
+        self.assertRaises(TypeError, self.handler.default, "text")
+
+    def test_handle_datetime(self):
+        now = datetime.datetime(day=1, month=2, year=2014,
+                                hour=10, minute=11, second=12)
+        self.assertEqual("1391249472", self.handler.default(now))
+
+    def test_handle_datetime_offset(self):
+        now = datetime.datetime(day=1, month=2, year=2014,
+                                hour=10, minute=11, second=12,
+                                tzinfo=dateutil.tz.tzoffset(None, 4*60*60))
+        self.assertEqual("1391220672", self.handler.default(now))
+
+
+class TestDatetimeToDecimal(unittest.TestCase):
+    def test_datetime_to_decimal(self):
+        expected_decimal = decimal.Decimal('1356093296.123')
+        utc_datetime = datetime.datetime.utcfromtimestamp(expected_decimal)
+        actual_decimal = notification_utils.dt_to_decimal(utc_datetime)
+        self.assertEqual(actual_decimal, expected_decimal)
+
+    def test_decimal_to_datetime(self):
+        expected_decimal = decimal.Decimal('1356093296.123')
+        expected_datetime = datetime.datetime.utcfromtimestamp(expected_decimal)
+        actual_datetime = notification_utils.dt_from_decimal(expected_decimal)
+        self.assertEqual(actual_datetime, expected_datetime)
+
+    def test_dt_from_decimal_none(self):
+        self.assertEqual("n/a",notification_utils.dt_from_decimal(None))
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..efc494f
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,14 @@
+[tox]
+envlist = py26,py27
+
+[testenv]
+deps = 
+    -r{toxinidir}/requirements.txt
+    -r{toxinidir}/test_requirements.txt
+
+setenv = VIRTUAL_ENV={envdir}
+
+commands =
+   nosetests tests 
+
+sitepackages = False