diff options
Diffstat (limited to 'dodai/config/databases/__init__.py')
-rw-r--r-- | dodai/config/databases/__init__.py | 445 |
1 files changed, 179 insertions, 266 deletions
diff --git a/dodai/config/databases/__init__.py b/dodai/config/databases/__init__.py index da317e8..ba6b9a9 100644 --- a/dodai/config/databases/__init__.py +++ b/dodai/config/databases/__init__.py | |||
@@ -16,56 +16,35 @@ | |||
16 | # along with Dodai. If not, see <http://www.gnu.org/licenses/>. | 16 | # along with Dodai. If not, see <http://www.gnu.org/licenses/>. |
17 | 17 | ||
18 | 18 | ||
19 | from dodai.exception import DatabaseEmptyOptionException | ||
20 | from dodai.exception import DatabasePortException | ||
21 | from dodai.exception import DatabaseHostnameException | ||
22 | from dodai.exception import DatabaseProtocolException | ||
23 | from dodai.exception import DatabaseConnectionException | ||
24 | from dodai.exception import UnknownDatabaseConnectionException | ||
25 | from dodai.tools import list_to_english | ||
26 | |||
19 | class ConfigDatabases(object): | 27 | class ConfigDatabases(object): |
20 | """An object that is used for creating database connection objects | 28 | """An object that is used for creating database connection objects |
21 | 29 | ||
22 | """ | 30 | """ |
23 | def __init__(self): | 31 | def __init__(self, default_handler, default_name): |
24 | self.sections = {} | 32 | self.connections = {} |
25 | self._handlers = {} | 33 | self._handlers = {} |
34 | DatabaseConnection.DEFAULT_HANDLER = default_name | ||
35 | self.add_handler(default_name, default_handler) | ||
26 | 36 | ||
27 | def add(self, sections): | 37 | def add(self, sections): |
28 | """Adds a dictionary of section (objects) to this object that could | 38 | """Adds a dictionary of sections to this object that could |
29 | be used for creating database connection objects | 39 | be used for creating database connection objects. The dictionary |
30 | 40 | should be formatted as follows: | |
31 | """ | ||
32 | self._build_default_handler() | ||
33 | for name, section in sections.items(): | ||
34 | self.sections[name] = DatabaseConnectionValidator( | ||
35 | name, section, self._handlers) | ||
36 | 41 | ||
37 | def load(self, section_name): | 42 | sections[name] = {option: value, option: value, ....} |
38 | """Returns a database connection object of the given section_name. | ||
39 | Throws Exceptions for any type of configuration errors or missing | ||
40 | configuration data | ||
41 | 43 | ||
42 | """ | 44 | The 'name' is the section name. It is an identifier that is used |
43 | if section_name in self.sections: | 45 | to load the database: |
44 | return self.sections[section_name].load() | ||
45 | else: | ||
46 | raise UnknownDatabaseConnectionException(section_name) | ||
47 | 46 | ||
48 | def _build_default_handler(self): | 47 | The options and values can have the following: |
49 | # Creates the default sqlalchemy handler | ||
50 | if 'sa' not in self._handlers: | ||
51 | from dodai.config.databases.sa import Sa | ||
52 | from sqlalchemy.orm import sessionmaker | ||
53 | from sqlalchemy import create_engine | ||
54 | from dodai.db import Db | ||
55 | sa = Sa(create_engine, sessionmaker, Db) | ||
56 | self._handlers['sa'] = sa | ||
57 | |||
58 | def add_handler(self, name, handler): | ||
59 | """Adds the given handler and name to this objects handlers | ||
60 | |||
61 | """ | ||
62 | self._handlers[name] = obj | ||
63 | |||
64 | |||
65 | class DatabaseConnectionValidator(object): | ||
66 | """Database validation object that is used to validate a section | ||
67 | object which is basically a python object that can have the following | ||
68 | properties: | ||
69 | 48 | ||
70 | protocol: The database engine (postgresql, mysql, sqlite, | 49 | protocol: The database engine (postgresql, mysql, sqlite, |
71 | oracle, mssql etc..) | 50 | oracle, mssql etc..) |
@@ -97,276 +76,210 @@ class DatabaseConnectionValidator(object): | |||
97 | handler: The handler used to build the orm object. Dodai | 76 | handler: The handler used to build the orm object. Dodai |
98 | only works with sqlalchemy so the default 'sa' | 77 | only works with sqlalchemy so the default 'sa' |
99 | is used. | 78 | is used. |
100 | Not all of the above options are required. | ||
101 | 79 | ||
102 | """ | 80 | """ |
103 | OPTIONS_REQUIRED = { | 81 | for name, section in sections.items(): |
104 | 'server':['protocol', 'hostname', 'port', 'username', 'password', | 82 | self.connections[name] = DatabaseConnection(section, name, |
105 | 'database'], | 83 | self._handlers) |
106 | 'file' :['protocol', 'filename'] | 84 | |
107 | } | 85 | def load(self, section_name): |
108 | OPTIONS_EXTRA = ['protocol_extra', 'handler', 'schema'] | 86 | """Returns a database connection object of the given section_name. |
109 | SERVER_PROTOCOLS = ['postgresql', 'mysql', 'sqlite', 'mssql', 'oracle'] | 87 | Throws Exceptions for any type of configuration errors or missing |
110 | FILE_PROTOCOLS = ['sqlite'] | 88 | configuration data |
111 | DEFAULT_HANDLER = 'sa' | 89 | |
112 | 90 | """ | |
113 | def __init__(self, name, section, handlers): | 91 | if section_name in self.connections: |
92 | return self.connections[section_name].load() | ||
93 | else: | ||
94 | raise UnknownDatabaseConnectionException(section_name) | ||
95 | |||
96 | def add_handler(self, name, handler): | ||
97 | """Adds the given handler and name to this objects handlers | ||
98 | |||
99 | """ | ||
100 | self._handlers[name] = handler | ||
101 | |||
102 | class DatabaseConnection(object): | ||
103 | |||
104 | DEFAULT_HANDLER = None | ||
105 | |||
106 | def __init__(self, section, name, handlers): | ||
114 | self.section = section | 107 | self.section = section |
115 | self.name = name | 108 | self.name = name |
116 | self.database_type = self._database_type() | 109 | self.handlers = handlers |
117 | self._handlers = handlers | 110 | self.validate = DatabaseConnectionValidator.load(section, name) |
118 | self.obj = None | 111 | self.obj = None |
119 | 112 | ||
120 | def load(self): | 113 | def load(self): |
121 | if not self.obj: | 114 | if not self.obj: |
122 | self._validate() | 115 | self.validate() |
123 | handler = self._get_handler() | 116 | handler = self._get_handler() |
124 | self.obj = handler(self.name, self.section) | 117 | self.obj = handler(self.name, self.section) |
125 | return self.obj | 118 | return self.obj |
126 | 119 | ||
127 | def _get_handler(self): | 120 | def _get_handler(self): |
128 | if hasattr(self.section, 'handler'): | 121 | if self.section.has_key('handler'): |
129 | name = self.section.handler.lower() | 122 | name = self.section['handler'] |
130 | if handler in self._handlers.keys(): | 123 | if name in self.handlers: |
131 | return self._handlers[name] | 124 | return self.handlers[name] |
132 | return self._handlers[self.DEFAULT_HANDLER] | 125 | return self.handlers[self.DEFAULT_HANDLER] |
133 | 126 | ||
134 | def _validate(self): | 127 | |
135 | if self.database_type == 'server': | 128 | class DatabaseConnectionValidator(object): |
136 | self._validate_protocol() | 129 | |
137 | self._validate_port() | 130 | def __init__(self, section, name, validators=[]): |
138 | self._validate_option('username') | 131 | self.section = section |
139 | self._validate_option('password') | 132 | self.name = name |
140 | self._validate_option('database') | 133 | self.validators = validators |
141 | elif self.database_type == 'file': | 134 | |
142 | self._validate_protocol() | 135 | def __call__(self): |
143 | self._validate_option('filename') | 136 | return self.validate() |
144 | else: | 137 | |
145 | raise DatabaseConnectionException(self.name, self.OPTIONS_REQUIRED) | 138 | def validate(self): |
139 | """Validates the connection. Returns true if valid. Throws | ||
140 | DodaiDatabaseConnectionConfigurationError on any errors | ||
141 | |||
142 | """ | ||
143 | raise NotImplementedError() | ||
146 | 144 | ||
147 | def _validate_option(self, name): | 145 | def _validate_option(self, name): |
148 | if not self.section[name]: | 146 | try: |
147 | value = self.section[name] | ||
148 | except KeyError: | ||
149 | value = None | ||
150 | |||
151 | if not value: | ||
149 | raise DatabaseEmptyOptionException(self.name, name) | 152 | raise DatabaseEmptyOptionException(self.name, name) |
150 | return True | 153 | return True |
151 | 154 | ||
152 | def _validate_hostname(self): | 155 | def _validate_protocol(self): |
153 | if not self.section.hostname: | ||
154 | raise DatabaseHostnameException(self.name, self.section.hostname) | ||
155 | return True | ||
156 | 156 | ||
157 | def _validate_port(self): | 157 | self._validate_option('protocol') |
158 | try: | ||
159 | port = int(self.section.port) | ||
160 | except ValueError: | ||
161 | pass | ||
162 | else: | ||
163 | if port >=1 and port <=65535: | ||
164 | return True | ||
165 | raise DatabasePortException(self.name, self.section.port) | ||
166 | 158 | ||
167 | def _validate_protocol(self): | 159 | if self.section['protocol'] not in self.PROTOCOLS: |
168 | if self.database_type == 'server': | 160 | raise DatabaseProtocolException(self.name, |
169 | if self.section.protocol not in self.SERVER_PROTOCOLS: | 161 | self.section['protocol'], self.DB_TYPE, |
170 | raise DatabaseProtocolException(self.name, | 162 | self.PROTOCOLS) |
171 | self.section.protocol, self.database_type, | 163 | return True |
172 | self.SERVER_PROTOCOLS) | ||
173 | else: | ||
174 | return True | ||
175 | elif self.database_type == 'file': | ||
176 | if self.section.protocol not in self.FILE_PROTOCOLS: | ||
177 | raise DatabaseProtocolException(self.name, | ||
178 | self.section.protocol, self.database_type, | ||
179 | self.FILE_PROTOCOLS) | ||
180 | else: | ||
181 | return True | ||
182 | else: | ||
183 | return False | ||
184 | 164 | ||
185 | def _database_type(self): | 165 | @staticmethod |
186 | keys = self.section.___options___.keys() | 166 | def load(section, name): |
187 | for type_, pool in self.OPTIONS_REQUIRED.items(): | 167 | """Static method (factory) that loads the correct database |
188 | out = True | 168 | validation class. |
189 | for key in keys: | ||
190 | if key not in pool: | ||
191 | out = False | ||
192 | if out: | ||
193 | return type_ | ||
194 | return False | ||
195 | 169 | ||
170 | Attributes: | ||
171 | section: Dictionary of key val connection information | ||
172 | name: String name of the section | ||
196 | 173 | ||
197 | class DatabaseEmptyOptionException(Exception): | 174 | """ |
198 | """Exception raised for an emption option in the config | 175 | action = None |
176 | validators = [DatabaseConnectionValidatorServer, | ||
177 | DatabaseConnectionValidatorFile] | ||
199 | 178 | ||
200 | Attributes: | 179 | for validator in validators: |
201 | section_name: The section of the config file that contains | 180 | obj = validator.load(section, name) |
202 | the invalid protocol | 181 | if obj: |
203 | option_name: The name of the empty option | 182 | return obj |
204 | 183 | ||
205 | """ | 184 | return DatabaseConnectionValidatorUnknown(section, name, validators) |
206 | MESSAGE = "In the '{section_name}' section, the '{option_name}' "\ | ||
207 | "was not set. Please set this option." | ||
208 | 185 | ||
209 | def __init__(self, section_name, option_name): | 186 | class DatabaseConnectionValidatorServer(DatabaseConnectionValidator): |
210 | self.section_name = section_name | ||
211 | self.option_name = option_name | ||
212 | self.msg = self._build_message() | ||
213 | 187 | ||
214 | def _build_message(self): | 188 | DB_TYPE = 'server' |
215 | return self.MESSAGE.format(section_name=self.section_name, | 189 | REQUIRED = ['protocol', 'hostname', 'port', 'username', 'password', |
216 | option_name=self.option_name) | 190 | 'database'] |
191 | PROTOCOLS = ['postgresql', 'mysql', 'mssql', 'oracle'] | ||
217 | 192 | ||
193 | def _validate_port(self): | ||
194 | self._validate_option('port') | ||
218 | 195 | ||
219 | class DatabasePortException(Exception): | 196 | try: |
220 | """Exception raised for invalid database port connection | 197 | port = int(self.section['port']) |
198 | except ValueError: | ||
199 | port = self.section['port'] | ||
200 | else: | ||
201 | if port >=1 and port <=65535: | ||
202 | return True | ||
203 | raise DatabasePortException(self.name, port) | ||
221 | 204 | ||
222 | Attributes: | 205 | def validate(self): |
223 | section_name: The section of the config file that contains | 206 | """Validates the connection. Returns true if valid. Throws |
224 | the invalid protocol | 207 | DodaiDatabaseConnectionConfigurationError on any errors |
225 | section_port: The port value that was listed in the | ||
226 | config file | ||
227 | 208 | ||
228 | """ | 209 | """ |
229 | MESSAGE = "In the '{section_name}' section, the port of "\ | 210 | self._validate_protocol() |
230 | "'{section_port}' is invalid. The port must be a "\ | 211 | self._validate_port() |
231 | "number between 1 and 65535" | 212 | self._validate_option('hostname') |
213 | self._validate_option('username') | ||
214 | self._validate_option('password') | ||
215 | self._validate_option('database') | ||
216 | return True | ||
232 | 217 | ||
233 | def __init__(self, section_name, section_port): | 218 | @classmethod |
234 | self.section_name = section_name | 219 | def load(cls, section, name): |
235 | self.section_port = section_port | 220 | """Return this validation class if it is possible that the |
236 | self.msg = self._build_message() | 221 | given connection information contains enough data to make |
222 | a database server connection. | ||
237 | 223 | ||
238 | def _build_message(self): | 224 | Attributes: |
239 | return self.MESSAGE.format(section_name=self.section_name, | 225 | section: Dictionary of key val connection information |
240 | section_port=self.section_port) | 226 | name: String name of the section |
241 | 227 | ||
228 | """ | ||
229 | if section.has_key('protocol'): | ||
230 | if section['protocol'] in cls.PROTOCOLS: | ||
231 | return cls(section, name) | ||
232 | keys = section.keys() | ||
233 | for key in keys: | ||
234 | if key in ['hostname', 'port']: | ||
235 | return cls(section, name) | ||
242 | 236 | ||
243 | class DatabaseHostnameException(Exception): | ||
244 | """Exception raised for invalid database hostname | ||
245 | 237 | ||
246 | Attributes: | 238 | class DatabaseConnectionValidatorFile(DatabaseConnectionValidator): |
247 | section_name: The section of the config file that contains | ||
248 | the invalid protocol | ||
249 | section_hostname: The hostname value that was listed in the | ||
250 | config file | ||
251 | 239 | ||
252 | """ | 240 | DB_TYPE = 'file' |
253 | MESSAGE = "In the '{section_name}' section, the hostname of "\ | 241 | REQUIRED = ['protocol', 'filename'] |
254 | "'{section_hostname}' is invalid. Please use a valid "\ | 242 | PROTOCOLS = ['sqlite'] |
255 | "hostname." | ||
256 | |||
257 | MSG_NON = "In the '{section_name}' section, the hostname was "\ | ||
258 | "not set. Please set the hostname." | ||
259 | |||
260 | def __init__(self, section_name, section_hostname): | ||
261 | self.section_name = section_name | ||
262 | self.section_hostname = section_hostname | ||
263 | self.msg = self._build_message() | ||
264 | |||
265 | def _build_message(self): | ||
266 | if self.section_hostname: | ||
267 | return self.MESSAGE.format(section_name=self.section_name, | ||
268 | section_hostname=self.section_hostname) | ||
269 | else: | ||
270 | return self.MSG_NON.format(section_name=self.section_name) | ||
271 | 243 | ||
244 | def validate(self): | ||
245 | """Validates the connection. Returns true if valid. Throws | ||
246 | DodaiDatabaseConnectionConfigurationError on any errors | ||
272 | 247 | ||
273 | class DatabaseProtocolException(Exception): | 248 | """ |
274 | """Exception raised for invalid database connection protocols | 249 | self._validate_protocol() |
250 | self._validate_option('filename') | ||
251 | return True | ||
275 | 252 | ||
276 | Attributes: | 253 | @classmethod |
277 | section_name: The section of the config file that contains | 254 | def load(cls, section, name): |
278 | the invalid protocol | 255 | """Return this validation class if it is possible that the |
279 | section_protocol: The protocol value that was listed in the | 256 | given connection information contains enough data to make |
280 | config file | 257 | a database file connection. |
281 | database_type: Usually 'server' or 'file' | ||
282 | protocols: List of valid protocols | ||
283 | 258 | ||
284 | """ | 259 | Attributes: |
285 | MESSAGE = "In the '{section_name}' section, the protocol of "\ | 260 | section: Dictionary of key val connection information |
286 | "'{section_protocol}' is invalid. The valid protocols "\ | 261 | name: String name of the section |
287 | "for a '{database_type}' connection are: {protocols}" | ||
288 | |||
289 | def __init__(self, section_name, section_protocol, database_type, | ||
290 | protocols): | ||
291 | self.section_name = section_name | ||
292 | self.section_protocol = section_protocol | ||
293 | self.database_type = database_type | ||
294 | self.protocols = protocols | ||
295 | self.msg = self._build_message() | ||
296 | |||
297 | def _build_message(self): | ||
298 | protocols = ', '.join(self.protocols) | ||
299 | return self.MESSAGE.format(section_name=self.section_name, | ||
300 | section_protocol=self.section_protocol, | ||
301 | database_type=self.database_type, | ||
302 | protocols=protocols) | ||
303 | |||
304 | |||
305 | class DatabaseConnectionException(Exception): | ||
306 | """Exception raised for missing database connection parameters | ||
307 | |||
308 | Attributes: | ||
309 | section_name: The section of the config file that contains | ||
310 | the invalid connection information | ||
311 | options_required: A dictionary containing the database_type | ||
312 | as the key and a list of required options | ||
313 | as the value | ||
314 | 262 | ||
315 | """ | 263 | """ |
316 | MESSAGE = "The '{section_name}' section does not contain all of the "\ | 264 | if section.has_key('protocol'): |
317 | "correct information needed to make a database connection." | 265 | if section['protocol'] in cls.PROTOCOLS: |
318 | MSG_TYPE = "To make a '{database_type}' connection please make sure "\ | 266 | return cls(section, name) |
319 | "To have all of the following options: {options}." | 267 | keys = section.keys() |
320 | MSG_END = "Please remember that the option names are case sensitive." | 268 | for key in keys: |
321 | 269 | if key in ['filename']: | |
322 | def __init__(self, section_name, options_required): | 270 | return cls(section, name) |
323 | self.section_name = section_name | ||
324 | self.options_required = options_required | ||
325 | self.msg = self._build_message() | ||
326 | |||
327 | def _build_message(self): | ||
328 | out = [] | ||
329 | out.append(self.MESSAGE.format(section_name=self.section_name)) | ||
330 | for database_type, options in self.options_required.items(): | ||
331 | options = list_to_english(options) | ||
332 | out.append(self.MSG_TYPE.format(database_type=database_type, | ||
333 | options=options)) | ||
334 | out.append(self.MSG_END) | ||
335 | return ' '.join(out) | ||
336 | |||
337 | |||
338 | class UnknownDatabaseConnectionException(Exception): | ||
339 | """Exception raised for missing database connection parameters | ||
340 | |||
341 | Attributes: | ||
342 | section: The requested section of the config file that can not | ||
343 | be found. | ||
344 | 271 | ||
345 | """ | ||
346 | MESSAGE = "Unable to find the '{section}' section to create a "\ | ||
347 | "database connection." | ||
348 | 272 | ||
349 | def __init__(self, section): | 273 | class DatabaseConnectionValidatorUnknown(DatabaseConnectionValidator): |
350 | self.section = section | ||
351 | self.msg = self._build_message() | ||
352 | 274 | ||
353 | def _build_message(self): | 275 | DB_TYPE = 'unkonwn' |
354 | return self.MESSAGE.format(section=self.section) | 276 | REQUIRED = [] |
277 | PROTOCOLS = [] | ||
355 | 278 | ||
279 | def validate(self): | ||
280 | """Validates the connection. Returns true if valid. Throws | ||
281 | DodaiDatabaseConnectionConfigurationError on any errors | ||
356 | 282 | ||
357 | def list_to_english(data): | 283 | """ |
358 | """Takes the input list and creates a string with each option | ||
359 | encased in single quotes and seperated by a comma with exception | ||
360 | of the last option which is prefixed with the word 'and' | ||
361 | 284 | ||
362 | """ | 285 | raise DatabaseConnectionException(self.name, self.validators) |
363 | if data: | ||
364 | if len(data) > 1: | ||
365 | out = [] | ||
366 | last = "'{0}'".format(data.pop()) | ||
367 | for row in data: | ||
368 | out.append("'{0}'".format(row)) | ||
369 | out = ', '.join(out) | ||
370 | return "{0} and {1}".format(out, last) | ||
371 | else: | ||
372 | return "'{0}'".format(data.pop()) | ||