New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
A proposed SQLite data access layer (DAL) #2
base: master
Are you sure you want to change the base?
Changes from 4 commits
5a2cd60
4caef3f
a75de4f
274fa21
842f1db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
|
||
from line_dal import LineDAL | ||
from sqlite_dal import SQLiteDAL |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,25 +13,114 @@ | |
|
||
* Not human readable - Binary format makes it harder to read/grep | ||
* More complex - More logic = more work = more time/resources? | ||
|
||
""" | ||
|
||
import os | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This appears to be an unused import, and while built-in, can set the wrong expectation for the following code. |
||
import sqlite3 | ||
|
||
import boltons | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another unused import, but this one is more concerning. Because it's a third-party package, this can really harm the reader's impression of the following code's reusability. You should add PyLint to your editor. PyLint will tell you about unused imports. |
||
|
||
_MISSING = object() | ||
SEP = '$' # '$' is valid in sqlite column names, no escaping required | ||
|
||
TABLE_NAME = 'on_import_data' | ||
CREATE_TABLE = ('CREATE TABLE IF NOT EXISTS ' + TABLE_NAME + | ||
'(id integer primary key asc, ' + | ||
'hostfqdn, hostname, python$argv, python$bin, ' + | ||
'python$build_date, python$compiler, python$have_readline, ' + | ||
'python$have_ucs4, python$is_64bit, python$version, python$version_full, ' + | ||
'time$utc_epoch real, time$std_utc_offset real, ' + | ||
'uname$0, uname$1, uname$2, uname$3, uname$4, uname$5, ' + | ||
'username, uuid)') | ||
|
||
total_count = 0 | ||
|
||
sqlite3.enable_callback_tracebacks(True) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This type of code at the module-level results in the infamous "import-time side effect". While the intentions may be good, this unexpected behavior can have serious implications. It's best to leave functions like this to the modules closer to the main code entry point. In this case, because there may be other future sqlite usages in the project, I would centralize this sort of behavior in |
||
|
||
|
||
class SQLiteDAL(object): | ||
_extension = '.db' | ||
|
||
def __init__(self, file_path): | ||
self.file_path = file_path | ||
self.__file_path = file_path | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not use name mangling unless absolutely necessary (I have only used it once in 7 years). Name mangling is not private variables in Python, and is really just a workaround for a very specific inheritance corner case. If you want to communicate to other developers that a variable is for internal use, a single underscore is preferred, e.g., |
||
|
||
conn = sqlite3.connect(file_path) | ||
conn.execute(CREATE_TABLE) | ||
|
||
def add_record(self, in_dict): | ||
pass | ||
query = ('INSERT INTO ' + TABLE_NAME + | ||
' (hostfqdn, hostname, python$argv, python$bin, ' + | ||
'python$build_date, python$compiler, python$have_readline, ' + | ||
'python$have_ucs4, python$is_64bit, python$version, python$version_full, ' + | ||
'time$utc_epoch, time$std_utc_offset, ' + | ||
'uname$0, uname$1, uname$2, uname$3, uname$4, uname$5, ' + | ||
'username, uuid) VALUES (') | ||
query += '"' + in_dict['hostfqdn'] + '", ' | ||
query += '"' + in_dict['hostname'] + '", ' | ||
query += '"' + in_dict['python']['argv'] + '", ' | ||
query += '"' + in_dict['python']['bin'] + '", ' | ||
query += '"' + in_dict['python']['build_date'] + '", ' | ||
query += '"' + in_dict['python']['compiler'] + '", ' | ||
query += '"' + str(in_dict['python']['have_readline'])[0] + '", ' | ||
query += '"' + str(in_dict['python']['have_ucs4'])[0] + '", ' | ||
query += '"' + str(in_dict['python']['is_64bit'])[0] + '", ' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a very unidiomatic way to convert a |
||
query += '"' + in_dict['python']['version'] + '", ' | ||
query += '"' + in_dict['python']['version_full'] + '", ' | ||
query += '"' + str(in_dict['time']['utc_epoch']) + '", ' | ||
query += '"' + str(in_dict['time']['std_utc_offset']) + '", ' | ||
query += '"' + in_dict['uname'][0] + '", ' | ||
query += '"' + in_dict['uname'][1] + '", ' | ||
query += '"' + in_dict['uname'][2] + '", ' | ||
query += '"' + in_dict['uname'][3] + '", ' | ||
query += '"' + in_dict['uname'][4] + '", ' | ||
query += '"' + in_dict['uname'][5] + '", ' | ||
query += '"' + in_dict['username'] + '", ' | ||
query += '"' + in_dict['uuid'] | ||
query += '")' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This block of string construction of SQL has many problems:
At the very least use the |
||
|
||
try: | ||
conn = sqlite3.connect(self.__file_path) | ||
conn.isolation_level = None # autocommit | ||
conn.execute(query) | ||
conn.close() | ||
except: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This catch-all except clause is a major antipattern. Ignoring all exceptions is unpythonic. It's ok to pause a second and be specific about which exceptions you want to handle. This block of code is not best-effort. This method should allow any exceptions raised here to propagate. The calling function can catch exceptions if it wants. Furthermore, even though it's only four lines, so many different things happen in this try block, that I would also call this try block too large. |
||
pass | ||
|
||
global total_count | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parallel to the comment above, the global keyword is a big yellow flag. Uses are rare enough in production code that a code comment is warranted. In this case, |
||
total_count += 1 | ||
return | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At over forty lines, this function is too long, especially for the level of other problems it has. Factor out a less brittle approach. Expect more power out of a line of Python. |
||
|
||
def raw_query(self, query): | ||
pass | ||
try: | ||
conn = sqlite3.connect(self.__file_path) | ||
rows = conn.execute(query).fetchall() | ||
return rows | ||
except: | ||
return -1 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apart from problems described in other comments, this simple function commits a crime against usability. In the success case, this function returns a list. In the failure case, it returns an integer.
In the |
||
|
||
def select_records(self, limit=None, group_by=None): | ||
pass | ||
ret = {} | ||
if not group_by: | ||
return ret | ||
group_by = group_by.replace('.', '$') | ||
query = 'SELECT ROWID, ' + group_by + ', COUNT(*) FROM ' + TABLE_NAME | ||
query += ' GROUP BY ' + str(group_by) | ||
if limit: | ||
query += ' ORDER BY ROWID DESC LIMIT ' + str(limit) | ||
|
||
try: | ||
conn = sqlite3.connect(self.__file_path) | ||
rows = conn.execute(query).fetchall() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Connecting and executing are common among all the methods of this class. Factor out logic like this to a central method. |
||
except: | ||
return | ||
ret['counts'] = {} | ||
for _, group_key, group_count in rows: | ||
ret['counts'][group_key] = group_count | ||
|
||
ret['grouped_by'] = group_by | ||
ret['grouped_key_count'] = len(rows) | ||
ret['record_count'] = sum(ret['counts'].values()) | ||
return ret | ||
|
||
|
||
if __name__ == '__main__': | ||
SQLiteDAL('test.db') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good change.
NB: This is an affirmative comment. All other comments inline are critiques, unless otherwise specified. Only the
sqlite_dal.py
file is of material interest. Because there are so many comments, it may be useful to uncheck the "Show notes" option above to do an initial read through. Alternatively, use the "View" button in the top right of thesqlite_dal.py
file below.