Merge pull request #110 from xrotwang/master
extended test coverage for the freeze package, thereby fixing some bugs.
This commit is contained in:
commit
d2f36d6413
@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from sqlalchemy.exc import ProgrammingError
|
from sqlalchemy.exc import ProgrammingError, OperationalError
|
||||||
from dataset.util import FreezeException
|
from dataset.util import FreezeException
|
||||||
from dataset.persistence.table import Table
|
from dataset.persistence.table import Table
|
||||||
from dataset.persistence.database import Database
|
from dataset.persistence.database import Database
|
||||||
@ -11,13 +11,16 @@ from dataset.freeze.format import get_serializer
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
|
def create_parser():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
description='Generate static JSON and CSV extracts from a SQL database.',
|
description='Generate static JSON and CSV extracts from a SQL database.',
|
||||||
epilog='For further information, please check the documentation.')
|
epilog='For further information, please check the documentation.')
|
||||||
parser.add_argument('config', metavar='CONFIG', type=str,
|
parser.add_argument('config', metavar='CONFIG', type=str,
|
||||||
help='freeze file cofiguration')
|
help='freeze file cofiguration')
|
||||||
parser.add_argument('--db', default=None,
|
parser.add_argument('--db', default=None,
|
||||||
help='Override the freezefile database URI')
|
help='Override the freezefile database URI')
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
def freeze(result, format='csv', filename='freeze.csv', fileobj=None,
|
def freeze(result, format='csv', filename='freeze.csv', fileobj=None,
|
||||||
@ -102,28 +105,32 @@ def freeze_export(export, result=None):
|
|||||||
serializer_cls = get_serializer(export)
|
serializer_cls = get_serializer(export)
|
||||||
serializer = serializer_cls(export, query)
|
serializer = serializer_cls(export, query)
|
||||||
serializer.serialize()
|
serializer.serialize()
|
||||||
except ProgrammingError as pe:
|
except (OperationalError, ProgrammingError) as e:
|
||||||
raise FreezeException("Invalid query: %s" % pe)
|
raise FreezeException("Invalid query: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def freeze_with_config(config, db=None):
|
||||||
# Set up default logger.
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
|
|
||||||
try:
|
|
||||||
args = parser.parse_args()
|
|
||||||
config = Configuration(args.config)
|
|
||||||
for export in config.exports:
|
for export in config.exports:
|
||||||
if args.db is not None:
|
if db is not None:
|
||||||
export.data['database'] = args.db
|
export.data['database'] = db
|
||||||
if export.skip:
|
if export.skip:
|
||||||
log.info("Skipping: %s", export.name)
|
log.info("Skipping: %s", export.name)
|
||||||
continue
|
continue
|
||||||
log.info("Running: %s", export.name)
|
log.info("Running: %s", export.name)
|
||||||
freeze_export(export)
|
freeze_export(export)
|
||||||
|
|
||||||
|
|
||||||
|
def main(): # pragma: no cover
|
||||||
|
# Set up default logger.
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
try:
|
||||||
|
parser = create_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
freeze_with_config(Configuration(args.config), args.db)
|
||||||
except FreezeException as fe:
|
except FreezeException as fe:
|
||||||
log.error(fe)
|
log.error(fe)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__': # pragma: no cover
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
main()
|
main()
|
||||||
|
|||||||
@ -28,7 +28,7 @@ class Serializer(object):
|
|||||||
self._get_basepath()
|
self._get_basepath()
|
||||||
|
|
||||||
if export.get('filename') == '-':
|
if export.get('filename') == '-':
|
||||||
export['fileobj'] = sys.stdout
|
export.data['fileobj'] = sys.stdout
|
||||||
self.fileobj = export.get('fileobj')
|
self.fileobj = export.get('fileobj')
|
||||||
|
|
||||||
def _get_basepath(self):
|
def _get_basepath(self):
|
||||||
|
|||||||
@ -34,7 +34,7 @@ class CSVSerializer(Serializer):
|
|||||||
|
|
||||||
# handle fileobj that has been passed in:
|
# handle fileobj that has been passed in:
|
||||||
if path is not None:
|
if path is not None:
|
||||||
if PY3:
|
if PY3: # pragma: no cover
|
||||||
fh = open(path, 'wt', encoding='utf8', newline='')
|
fh = open(path, 'wt', encoding='utf8', newline='')
|
||||||
else:
|
else:
|
||||||
fh = open(path, 'wb')
|
fh = open(path, 'wb')
|
||||||
@ -42,7 +42,7 @@ class CSVSerializer(Serializer):
|
|||||||
fh = self.fileobj
|
fh = self.fileobj
|
||||||
|
|
||||||
writer = csv.writer(fh)
|
writer = csv.writer(fh)
|
||||||
if PY3:
|
if PY3: # pragma: no cover
|
||||||
writer.writerow(keys)
|
writer.writerow(keys)
|
||||||
else:
|
else:
|
||||||
writer.writerow([k.encode('utf-8') for k in keys])
|
writer.writerow([k.encode('utf-8') for k in keys])
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime, date
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from six import PY3
|
||||||
|
|
||||||
from dataset.freeze.format.common import Serializer
|
from dataset.freeze.format.common import Serializer
|
||||||
|
|
||||||
|
|
||||||
class JSONEncoder(json.JSONEncoder):
|
class JSONEncoder(json.JSONEncoder):
|
||||||
|
|
||||||
def default(self, obj):
|
def default(self, obj):
|
||||||
if isinstance(obj, datetime):
|
if isinstance(obj, (datetime, date)):
|
||||||
return obj.isoformat()
|
return obj.isoformat()
|
||||||
|
|
||||||
|
|
||||||
@ -37,7 +39,13 @@ class JSONSerializer(Serializer):
|
|||||||
for path, result in self.buckets.items():
|
for path, result in self.buckets.items():
|
||||||
result = self.wrap(result)
|
result = self.wrap(result)
|
||||||
|
|
||||||
fh = open(path, 'wb') if self.fileobj is None else self.fileobj
|
if self.fileobj is None:
|
||||||
|
if PY3: # pragma: no cover
|
||||||
|
fh = open(path, 'w', encoding='utf8')
|
||||||
|
else:
|
||||||
|
fh = open(path, 'wb')
|
||||||
|
else:
|
||||||
|
fh = self.fileobj
|
||||||
|
|
||||||
data = json.dumps(result,
|
data = json.dumps(result,
|
||||||
cls=JSONEncoder,
|
cls=JSONEncoder,
|
||||||
|
|||||||
@ -3,12 +3,7 @@ import threading
|
|||||||
import re
|
import re
|
||||||
from sqlalchemy.util import safe_reraise
|
from sqlalchemy.util import safe_reraise
|
||||||
|
|
||||||
try:
|
from six.moves.urllib.parse import urlencode, parse_qs
|
||||||
from urllib.parse import urlencode
|
|
||||||
from urllib.parse import parse_qs
|
|
||||||
except ImportError:
|
|
||||||
from urllib import urlencode
|
|
||||||
from urlparse import parse_qs
|
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.pool import NullPool
|
from sqlalchemy.pool import NullPool
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from inspect import isgenerator
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
except ImportError:
|
except ImportError: # pragma: no cover
|
||||||
from ordereddict import OrderedDict
|
from ordereddict import OrderedDict
|
||||||
|
|
||||||
from sqlalchemy import Integer, UnicodeText, Float, DateTime, Boolean, types, Table, event
|
from sqlalchemy import Integer, UnicodeText, Float, DateTime, Boolean, types, Table, event
|
||||||
|
|||||||
@ -1,2 +1,5 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
description-file = README.md
|
description-file = README.md
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
ignore = E501,E123,E124,E126,E127,E128
|
||||||
|
|||||||
@ -60,3 +60,18 @@ class FreezeTestCase(unittest.TestCase):
|
|||||||
else:
|
else:
|
||||||
v1 = '%s' % v1
|
v1 = '%s' % v1
|
||||||
self.assertEqual(v2, v1)
|
self.assertEqual(v2, v1)
|
||||||
|
|
||||||
|
|
||||||
|
class SerializerTestCase(unittest.TestCase):
|
||||||
|
def test_Serializer(self):
|
||||||
|
from dataset.freeze.format.common import Serializer
|
||||||
|
from dataset.freeze.config import Export
|
||||||
|
from dataset.util import FreezeException
|
||||||
|
|
||||||
|
self.assertRaises(FreezeException, Serializer, {}, {})
|
||||||
|
s = Serializer(Export({'filename': 'f'}, {'mode': 'nomode'}), '')
|
||||||
|
self.assertRaises(FreezeException, getattr, s, 'wrap')
|
||||||
|
s = Serializer(Export({'filename': 'f'}, {}), '')
|
||||||
|
s.wrap
|
||||||
|
s = Serializer(Export({'filename': '-'}, {}), '')
|
||||||
|
self.assert_(s.fileobj)
|
||||||
|
|||||||
62
test/test_freeze_app.py
Normal file
62
test/test_freeze_app.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
"""
|
||||||
|
Test CLI following the recipe at http://dustinrcollins.com/testing-python-command-line-apps
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from unittest import TestCase
|
||||||
|
from tempfile import mkdtemp
|
||||||
|
from shutil import rmtree
|
||||||
|
from copy import copy
|
||||||
|
|
||||||
|
from six import StringIO
|
||||||
|
|
||||||
|
from dataset import connect
|
||||||
|
from dataset.util import FreezeException
|
||||||
|
from dataset.freeze.config import Configuration, Export
|
||||||
|
from dataset.freeze.app import create_parser, freeze_with_config, freeze_export
|
||||||
|
from .sample_data import TEST_DATA
|
||||||
|
|
||||||
|
|
||||||
|
class FreezeTestCase(TestCase):
|
||||||
|
"""
|
||||||
|
Base TestCase class, sets up a CLI parser
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
parser = create_parser()
|
||||||
|
self.parser = parser
|
||||||
|
self.d = mkdtemp()
|
||||||
|
self.db_path = os.path.abspath(os.path.join(self.d, 'db.sqlite'))
|
||||||
|
self.db = 'sqlite:///' + self.db_path
|
||||||
|
_db = connect(self.db)
|
||||||
|
tbl = _db['weather']
|
||||||
|
for i, row in enumerate(TEST_DATA):
|
||||||
|
_row = copy(row)
|
||||||
|
_row['count'] = i
|
||||||
|
_row['bool'] = True
|
||||||
|
_row['none'] = None
|
||||||
|
tbl.insert(_row)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
rmtree(self.d, ignore_errors=True)
|
||||||
|
|
||||||
|
def test_with_empty_args(self):
|
||||||
|
"""
|
||||||
|
User passes no args, should fail with SystemExit
|
||||||
|
"""
|
||||||
|
self.assertRaises(SystemExit, self.parser.parse_args, [])
|
||||||
|
|
||||||
|
def test_with_config(self):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
cfg = Configuration(os.path.join(os.path.dirname(__file__), 'Freezefile.yaml'))
|
||||||
|
cfg.data['common']['database'] = self.db
|
||||||
|
cfg.data['common']['prefix'] = self.d
|
||||||
|
cfg.data['common']['query'] = 'SELECT * FROM weather'
|
||||||
|
cfg.data['exports'] = [
|
||||||
|
{'filename': '{{identity:count}}.json', 'mode': 'item', 'transform': {'bool': 'identity'}},
|
||||||
|
{'filename': 'weather.json', 'format': 'tabson'},
|
||||||
|
{'filename': 'weather.csv', 'fileobj': StringIO(), 'format': 'csv'},
|
||||||
|
{'filename': 'weather.json', 'fileobj': StringIO(), 'format': 'tabson'},
|
||||||
|
{'filename': 'weather.json', 'format': 'tabson', 'callback': 'read'},
|
||||||
|
{'skip': True}]
|
||||||
|
freeze_with_config(cfg, db=self.db)
|
||||||
|
self.assertRaises(FreezeException, freeze_export, Export(cfg.data['common'], {'query': 'SELECT * FROM notable'}))
|
||||||
@ -4,7 +4,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
except ImportError:
|
except ImportError: # pragma: no cover
|
||||||
from ordereddict import OrderedDict # Python < 2.7 drop-in
|
from ordereddict import OrderedDict # Python < 2.7 drop-in
|
||||||
|
|
||||||
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user