summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mcrute@gmail.com>2010-02-04 23:26:32 -0500
committerMike Crute <mcrute@gmail.com>2010-02-04 23:26:32 -0500
commit4f7c446b853e333caaf1b28b668ffbc1b011b78f (patch)
tree20f43e6c5b46ddd47e1a39b56e81f7e087254403
parentd977817fbc99e6752f804cab1d0fd82b96e569e6 (diff)
downloadkronos-4f7c446b853e333caaf1b28b668ffbc1b011b78f.tar.bz2
kronos-4f7c446b853e333caaf1b28b668ffbc1b011b78f.tar.xz
kronos-4f7c446b853e333caaf1b28b668ffbc1b011b78f.zip
Adding SQLite storage backend.
-rw-r--r--kronos/model.py17
-rw-r--r--kronos/parser.py8
-rw-r--r--kronos/storage.py177
-rw-r--r--kronos/tests/itest_sqlite_storage.py66
4 files changed, 261 insertions, 7 deletions
diff --git a/kronos/model.py b/kronos/model.py
new file mode 100644
index 0000000..d2ac67c
--- /dev/null
+++ b/kronos/model.py
@@ -0,0 +1,17 @@
1# vim: set filencoding=utf8
2"""
3Kronos Model Objects
4
5@author: Mike Crute (mcrute@ag.com)
6@organization: American Greetings Interactive
7@date: February 04, 2010
8"""
9
10
11class Activity(object):
12
13 def __init__(self, activity, description=None, category=None):
14 self.activity = activity
15 self.description = description
16 self.category = category
17 self.tags = []
diff --git a/kronos/parser.py b/kronos/parser.py
index f0543f9..147682f 100644
--- a/kronos/parser.py
+++ b/kronos/parser.py
@@ -7,13 +7,7 @@ Activity Parser
7@date: February 04, 2010 7@date: February 04, 2010
8""" 8"""
9 9
10class Activity(object): 10from kronos.model import Activity
11
12 def __init__(self, activity, description=None, category=None):
13 self.activity = activity
14 self.description = description
15 self.category = category
16 self.tags = []
17 11
18 12
19def parse_activity(text): 13def parse_activity(text):
diff --git a/kronos/storage.py b/kronos/storage.py
new file mode 100644
index 0000000..4add988
--- /dev/null
+++ b/kronos/storage.py
@@ -0,0 +1,177 @@
1# vim: set filencoding=utf8
2"""
3Storage Back-ends For Kronos
4
5@author: Mike Crute (mcrute@ag.com)
6@organization: American Greetings Interactive
7@date: February 04, 2010
8"""
9
10
11class StorageError(Exception):
12 """
13 Base class for all other storage exceptions.
14 """
15
16
17class NotConnected(StorageError):
18 """
19 Raised when a connection to the back-end is not avaiable.
20 """
21
22
23def _get_filtered_object_dict(dict_):
24 output = {}
25 for key, value in dict_.__dict__.items():
26 if not key.startswith('_') and not callable(value):
27 output[key] = value
28
29 return output
30
31
32class BaseStorageBackEnd(object):
33
34 def __init__(self):
35 self.load_engine()
36
37 def get(self, model_obj, **kwargs):
38 """
39 Get a model object instance from the storage back-end
40 where the criteria in the kwargs matches that in the
41 back-end.
42 """
43 name = model_obj.__name__
44 result = self.select(name, **kwargs)
45
46 instance = model_obj()
47 for key, value in result.items():
48 if key != 'id':
49 setattr(instance, key, value)
50 else:
51 instance.__db_id__ = value
52
53 return instance
54
55 def save(self, model_obj):
56 """
57 Persist a model object with the storage back-end.
58 """
59 name = model_obj.__class__.__name__
60 id_ = getattr(model_obj, '__db_id__', None)
61
62 values = _get_filtered_object_dict(model_obj)
63
64 if id_:
65 self.update(name, id_, **values)
66 else:
67 self.insert(name, **values)
68
69 def load_engine(self):
70 """
71 Load a storage engine module that contains a function
72 called connect suitable for setting up the actual
73 connection.
74 """
75
76 def select(self, table, **kwargs):
77 """
78 Select raw data from the storage engine and return
79 as a dictionary.
80 """
81
82 def insert(self, table, **values):
83 """
84 Insert values into the back-end named table or
85 equivalent data structure.
86 """
87
88 def update(self, table, criteria, **values):
89 """
90 Update values in the back-end table or equivalent
91 data structure based on the criteria.
92 """
93
94 def connect(self, database, *args, **kwargs):
95 """
96 Create a connection to the back-end.
97 """
98
99 def create_table(self, obj, **columns):
100 """
101 Create a table or equivalent data structure in the
102 storage back-end.
103 """
104
105
106class SQLiteBackEnd(BaseStorageBackEnd):
107
108 def __init__(self):
109 super(SQLiteBackEnd, self).__init__()
110 self.connection = None
111
112 def load_engine(self):
113 import sqlite3
114 self.engine = sqlite3
115
116 def _check_connection(self):
117 if not self.connection:
118 raise NotConnected("Not connected to storage back-end!")
119
120 def connect(self, database, *args, **kwargs):
121 self.connection = self.engine.connect(database)
122
123 def select(self, table, **kwargs):
124 sql = "SELECT * FROM {0} WHERE ".format(table)
125 for key in kwargs.keys():
126 sql += "{0}=? ".format(key)
127
128 return self._get_normalized_results(sql, **kwargs)[0]
129
130 def _get_normalized_results(self, sql, **kwargs):
131 self._check_connection()
132 cursor = self.connection.cursor()
133
134 results = cursor.execute(sql, kwargs.values())
135 return self._normalize_results(results)
136
137 def _normalize_results(self, results):
138 col_names = [col[0] for col in results.description]
139 output = []
140 for row in results.fetchall():
141 output.append(dict(zip(col_names, row)))
142
143 return output
144
145 def insert(self, table, **values):
146 placeholder = ("?," * len(values)).rstrip(',')
147 col_spec = ','.join(values.keys())
148 sql = "INSERT INTO {0}({1}) VALUES({2})".format(table, col_spec,
149 placeholder)
150
151 self._execute_modification(sql, values.values())
152
153 def update(self, table, criteria, **values):
154 sql = "UPDATE {0} SET ".format(table)
155 for key in values.keys():
156 sql += "{0}=?, ".format(key)
157 sql = sql[:-2] + "WHERE id=?"
158
159 binds = values.values() + [criteria]
160 self._execute_modification(sql, binds)
161
162 def create_table(self, obj, **columns):
163 table = obj.__name__
164
165 sql = "CREATE TABLE {0} (".format(table)
166 sql += "id INTEGER PRIMARY KEY"
167 for key, value in columns.items():
168 sql += ", {0} {1}".format(key, value)
169 sql += ")"
170
171 self._execute_modification(sql)
172
173 def _execute_modification(self, sql, binds=()):
174 self._check_connection()
175 cursor = self.connection.cursor()
176 cursor.execute(sql, binds)
177 self.connection.commit()
diff --git a/kronos/tests/itest_sqlite_storage.py b/kronos/tests/itest_sqlite_storage.py
new file mode 100644
index 0000000..4628a23
--- /dev/null
+++ b/kronos/tests/itest_sqlite_storage.py
@@ -0,0 +1,66 @@
1# vim: set filencoding=utf8
2"""
3SQLite Storage Back-End Test
4
5@author: Mike Crute (mcrute@ag.com)
6@organization: American Greetings Interactive
7@date: February 04, 2010
8
9A basic integration test that exercies the entire
10SQLite storage back-end.
11"""
12
13
14import unittest
15from nose.tools import assert_equals, assert_raises
16from kronos.storage import SQLiteBackEnd, NotConnected
17
18
19class SampleModel(object):
20
21 foo = None
22 bar = None
23 baz = None
24
25
26class TestSQLitBackEnd(unittest.TestCase):
27
28 def setUp(self):
29 self.storage = SQLiteBackEnd()
30 self.storage.connect(':memory:')
31
32 self.storage.create_table(SampleModel, foo='text',
33 bar='text', baz='text')
34
35 self.model1 = SampleModel()
36 self.model1.foo = '123'
37 self.model1.bar = '456'
38 self.model1.baz = '789'
39
40 def test_no_connect_should_cause_error(self):
41 self.storage.connection = None
42 assert_raises(NotConnected, self.storage.save, self.model1)
43
44 def test_save_and_select(self):
45 self.storage.save(self.model1)
46 results = self.storage.get(SampleModel, foo='123')
47
48 assert isinstance(results, SampleModel)
49 assert_equals(results.foo, '123')
50 assert_equals(results.bar, '456')
51 assert_equals(results.baz, '789')
52
53 def test_save_and_update(self):
54 self.storage.save(self.model1)
55 results = self.storage.get(SampleModel, foo='123')
56 results.foo = 'test'
57 self.storage.save(results)
58
59 assert isinstance(results, SampleModel)
60 assert_equals(results.foo, 'test')
61 assert_equals(results.bar, '456')
62 assert_equals(results.baz, '789')
63
64
65if __name__ == "__main__":
66 unittest.main()