Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit f8aedde11860594a7bc9faae82967fbc21077e26 @rbranson committed Jul 28, 2012
@@ -0,0 +1,4 @@
+*.pyc
+*.sw?
+dist
+MANIFEST
@@ -0,0 +1 @@
+v0.1.0, 7/27/2012 -- Initial release.
@@ -0,0 +1,27 @@
+Copyright (c) 2012, Richard W. Branson
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and documentation are those
+of the authors and should not be interpreted as representing official policies,
+either expressed or implied, of Richard W. Branson or any associated
+organizations and/or employers: past, present, and/or future.
@@ -0,0 +1,2 @@
+include *.txt
+include *.md
@@ -0,0 +1,40 @@
+=======
+jsonfig
+=======
+
+Simple autoreloading configuration files in JSON. Will use the very fast UltraJSON library for parsing if available.
+
+Installation
+============
+
+ $ pip install jsonfig
+
+Usage
+=====
+
+Create a JSON file whose root is a dictionary:
+
+ {
+ "always_load_user_data": true,
+ "use_new_feature_123": false,
+ "feature_456_chance": 0.20
+ }
+
+Now it can be loaded as a dictionary:
+
+ >>> config = jsonfig.from_path("example.json")
+ >>> config["feature_456_chance"]
+ 0.20
+
+By default, when the file is changed, it will be automatically reloaded within one second:
+
+ {
+ "always_load_user_data": true,
+ "use_new_feature_123": false,
+ "feature_456_chance": 1.0
+ }
+
+Now the new value will show up in the config structure:
+
+ >>> config["feature_456_chance"]
+ 1.0
@@ -0,0 +1 @@
+from jsonfig.api import from_path
@@ -0,0 +1,21 @@
+from jsonfig.contents import IntervalReloadFileContents
+from jsonfig.parse import JsonFileContentsParser
+from jsonfig.util import DictionaryProxy
+
+def from_path(path, autoreload=True, autoreload_interval=1.0):
+ """ Returns a dictionary that loads it's contents from a JSON file,
+ specified by the :path: argument. Periodically it will check to see if the
+ contents of the file have changed and reload it.
+
+ :param path: The file containing the JSON data.
+ :keyword autoreload: Turns on periodic checks of the file for new data.
+ :keyword autoreload_interval: How often (in seconds) to check the file for
+ changes."""
+
+ if autoreload is True:
+ contents = IntervalReloadFileContents(path, interval = autoreload_interval)
+ else:
+ contents = FileContents(path)
+
+ parser = JsonFileContentsParser(contents)
+ return DictionaryProxy(lambda: parser.parsed)
@@ -0,0 +1,75 @@
+import time
+import os
+import hashlib
+
+class FileContents(object):
+ """Loads the contents of a file and holds it in memory.
+
+ :param path: Path to the file to load
+ """
+
+ def __init__(self, path):
+ self._path = path
+ self._contents = None
+ self._hash = None
+
+ self.load()
+
+ def load(self):
+ """ Refreshes the contents of the file. """
+ with open(self._path, "r") as f:
+ self._contents = f.read()
+ self._hash = self._hash_string_from_string(self._contents)
+
+ @property
+ def contents(self):
+ """ Returns the file contents. """
+ return self._contents
+
+ @property
+ def hash_string(self):
+ """ Returns a string that contains a hash for the loaded file contents. """
+ return self._hash
+
+ def _hash_string_from_string(self, s):
+ return hashlib.md5(s).hexdigest()
+
+class IntervalReloadFileContents(FileContents):
+ """Loads the contents of a file and holds it in memory. Each time the
+ file contents are requested, checks to see if the file has been modified
+ since last checked.
+
+ :param path: Path to the file to load.
+ :keyword interval: The interval (in seconds) between modification checks.
+ """
+
+ def __init__(self, path, interval = 1):
+ self._last_check = 0
+ self._last_mtime = 0
+ self._interval = interval
+
+ super(IntervalReloadFileContents, self).__init__(path)
+
+ @property
+ def contents(self):
+ """ Returns the file contents. """
+ self._check_reload()
+ return super(IntervalReloadFileContents, self).contents
+
+ @property
+ def hash_string(self):
+ """ Returns a string that contains a hash for the loaded file contents. """
+ self._check_reload()
+ return super(IntervalReloadFileContents, self).hash_string
+
+ def _check_reload(self):
+ ctime = time.time()
+
+ if ctime >= self._last_check + self._interval:
+ stat = os.stat(self._path)
+
+ if stat.st_mtime != self._last_mtime:
+ self.load()
+ self._last_mtime = stat.st_mtime
+
+ self._last_check = ctime
@@ -0,0 +1,47 @@
+class ParseError(Exception):
+ pass
+
+class BaseFileContentsParser(object):
+ """ Base class for parsing the contents of FileContents objects. Subclass
+ and implement the :_parse: method, returning the parsed data structures.
+ Only actually runs the :_parse: method if the file contents change."""
+
+ def __init__(self, contents):
+ self._contents = contents
+ self._last_hash = None
@mikeyk

mikeyk Jul 30, 2012

when does last_hash ever get set in this class?

+ self._parsed = None
+
+ @property
+ def parsed(self):
+ """ Parsed data from the file contents provided. """
+
+ # Try to only actually parse if the contents change
+ if self._contents.hash_string != self._last_hash:
+ self._parsed = self._parse(self._contents.contents)
+
+ return self._parsed
+
+ def _parse(self, data):
+ """ Subclassed method that parses the file contents """
+ raise NotImplementedError()
+
+class JsonFileContentsParser(BaseFileContentsParser):
+ """ Parses JSON from a file. C'mon. Uses ujson if available.
+
+ :param contents: The FileContents object containing the raw data.
+ """
+
+ def __init__(self, contents):
+ super(JsonFileContentsParser, self).__init__(contents)
+
+ def _parse(self, data):
+ try:
@mikeyk

mikeyk Jul 30, 2012

doing this import at top level would be more idiomatic

+ import ujson as json
+ except:
+ import json
+
+ try:
+ return json.loads(data)
+ except ValueError, e:
@mikeyk

mikeyk Jul 30, 2012

does the built-in json library also throw ValueError?

@rbranson

rbranson Jul 31, 2012

Owner

yeah it does.

import json
json.loads("{fooasdfasdf}")
Traceback (most recent call last):
File "", line 1, in
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/init.py", line 326, in loads
return _default_decoder.decode(s)
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/decoder.py", line 360, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/decoder.py", line 376, in raw_decode
obj, end = self.scan_once(s, idx)
ValueError: Expecting property name: line 1 column 1 (char 1)

+ raise ParseError(e)
+
No changes.
@@ -0,0 +1,32 @@
+import unittest
+import time
+
+from tempfile import NamedTemporaryFile
+import jsonfig
+
+class TestAPI(unittest.TestCase):
+ def test_from_path_parses_json_into_dictionary(self):
+ with NamedTemporaryFile() as f:
+ f.write('{ "foo": "bar", "bim": "baz" }')
+ f.flush()
+
+ instance = jsonfig.from_path(f.name)
+ self.assertEqual("bar", instance["foo"])
+ self.assertEqual("baz", instance["bim"])
+
+ # sleep here for the mtime rollover
+ time.sleep(1)
+
+ f.seek(0)
+ f.truncate()
+ f.write('{ "need": "there is none" }')
+ f.flush()
+
+ time.sleep(1)
+ self.assertEqual("there is none", instance["need"])
+ self.assertRaises(KeyError, lambda: instance["foo"])
+ self.assertRaises(KeyError, lambda: instance["bim"])
+
+ def test_from_path_doesnt_autoreload(self):
+ pass
+
@@ -0,0 +1,78 @@
+import unittest
+import time
+
+from tempfile import NamedTemporaryFile
+from jsonfig.contents import FileContents, IntervalReloadFileContents
+
+class TestFileContents(unittest.TestCase):
+ def test_file_contents_are_loaded(self):
+ with NamedTemporaryFile() as f:
+ data = "yolo yolo yolo"
+ data_md5 = "fd03e21c10f83acfed74f3ad832d3794"
+
+ f.write(data)
+ f.flush()
+
+ fc = FileContents(f.name)
+ self.assertEqual(data, fc.contents)
+ self.assertEqual(data_md5, fc.hash_string)
+
+ def test_file_contents_picked_up_on_reload(self):
+ with NamedTemporaryFile() as f:
+ data = "yolo yolo yolo"
+ data_md5 = "fd03e21c10f83acfed74f3ad832d3794"
+
+ f.write(data)
+ f.flush()
+
+ fc = FileContents(f.name)
+ self.assertEqual(data, fc.contents)
+ self.assertEqual(data_md5, fc.hash_string)
+
+ data2 = "foo bar bim baz"
+ data2_md5 = "b7752decac2d4475a3e87a4882768afc"
+
+ f.seek(0)
+ f.truncate()
+ f.write(data2)
+ f.flush()
+
+ self.assertEqual(data, fc.contents)
+ self.assertEqual(data_md5, fc.hash_string)
+
+ fc.load()
+ self.assertEqual(data2, fc.contents)
+ self.assertEqual(data2_md5, fc.hash_string)
+
+class TestIntervalReloadFileContents(unittest.TestCase):
+ def test_file_contents_autoreload(self):
+ with NamedTemporaryFile() as f:
+ data = "yolo yolo yolo"
+ data_md5 = "fd03e21c10f83acfed74f3ad832d3794"
+
+ f.write(data)
+ f.flush()
+
+ fc = IntervalReloadFileContents(f.name, interval=1)
+ self.assertEqual(data, fc.contents)
+ self.assertEqual(data_md5, fc.hash_string)
+
+ data2 = "foo bar bim baz"
+ data2_md5 = "b7752decac2d4475a3e87a4882768afc"
+
+ self.assertEqual(data, fc.contents)
+ self.assertEqual(data_md5, fc.hash_string)
+
+ # Have to sleep here to tick up the mtime
+ time.sleep(1)
+
+ f.seek(0)
+ f.truncate()
+ f.write(data2)
+ f.flush()
+
+ time.sleep(1)
+
+ self.assertEqual(data2, fc.contents)
+ self.assertEqual(data2_md5, fc.hash_string)
+
@@ -0,0 +1,33 @@
+import unittest
+import time
+
+from tempfile import NamedTemporaryFile
+from jsonfig.parse import JsonFileContentsParser, ParseError
+
+class MockFileContents(object):
+ def __init__(self, contents):
+ self.contents = contents
+ self.hash_string = "1234"
+
+class TestJsonFileContentsParser(unittest.TestCase):
+ def test_good_json_is_parsed(self):
+ data = '{ "good": "json" }'
+ struct = { "good": "json" }
+ mock = MockFileContents(data)
+
+ p = JsonFileContentsParser(mock)
+ self.assertEquals(struct, p.parsed)
+
+ data2 = '{ "good": "json2" }'
+ struct2 = { "good": "json2" }
+ mock.contents = data2
+ mock.hash_string = "12345"
+
+ self.assertEquals(struct2, p.parsed)
+
+ def test_bad_json_throws_error(self):
+ data = "badjson"
+ mock = MockFileContents(data)
+ p = JsonFileContentsParser(mock)
+ self.assertRaises(ParseError, lambda: p.parsed)
+
@@ -0,0 +1,12 @@
+class DictionaryProxy(object):
+ def __init__(self, func=dict):
+ self._f = func
+
+ def __getitem__(self, name):
+ return self._f().__getitem__(name)
+
+ def __setitem__(self, name, value):
+ return self._f().__setitem__(name, value)
+
+ def __getattr__(self, name):
+ return getattr(self._f(), name)
@@ -0,0 +1,12 @@
+from distutils.core import setup
+
+setup(
+ name = "jsonfig",
+ version = "0.1.0",
+ author = "Rick Branson",
+ author_email = "rick@diodeware.com",
+ url = "http://github.com/rbranson/jsconfig",
+ packages = [ "jsonfig" ],
+ license = "LICENSE.txt",
+ description = "Simple JSON auto-reloading configuration"
+)

0 comments on commit f8aedde

Please sign in to comment.