From 60ed3e40507cf428f5dd3598a22af08e3de96037 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Thu, 9 Jun 2016 18:05:58 -0700 Subject: [PATCH] TOX / Travis build matrix (#593) * Building on many dbs * Fixing some translation related error msg --- .travis.yml | 39 ++-- caravel/assets/js_build.sh | 6 + caravel/assets/visualizations/nvd3_vis.js | 186 +++++++++--------- caravel/bin/caravel | 10 +- caravel/migrations/env.py | 2 +- ...3_fix_wrong_constraint_on_table_columns.py | 35 ++-- caravel/models.py | 3 +- caravel/utils.py | 78 +++++--- caravel/views.py | 3 - caravel/viz.py | 16 +- dev-reqs.txt | 2 + run_tests.sh | 2 + setup.py | 3 +- tests/caravel_test_config.py | 6 + tests/core_tests.py | 10 +- tox.ini | 69 +++++++ 16 files changed, 298 insertions(+), 172 deletions(-) create mode 100755 caravel/assets/js_build.sh create mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml index c3023d67e6..7396b1cc75 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,23 +1,30 @@ language: python -python: - - "2.7" - - "3.4" - - "3.5" +addons: + apt: + sources: + - deadsnakes + packages: + - python3.5 cache: directories: - $HOME/.wheelhouse/ +env: + global: + - TRAVIS_CACHE=$HOME/.travis_cache/ + matrix: + #- TOX_ENV=py27-mysql + - TOX_ENV=py27-sqlite + #- TOX_ENV=py27-postgres + - TOX_ENV=py34-sqlite + - TOX_ENV=py34-mysql + - TOX_ENV=javascript before_install: - npm install -g npm@'>=2.7.1' +before_script: + - mysql -e 'drop database if exists caravel; create database caravel DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci' -u root + - psql -c 'create database caravel;' -U postgres + - export PATH=${PATH}:/tmp/hive/bin install: - - pip wheel -w $HOME/.wheelhouse -f $HOME/.wheelhouse . - - pip install --find-links=$HOME/.wheelhouse --no-index . - - pip install -r dev-reqs.txt - - cd caravel/assets - - npm --version - - npm install - - npm run lint - - npm run prod - - cd $TRAVIS_BUILD_DIR -script: bash run_tests.sh -after_success: - - coveralls + - pip install --upgrade pip + - pip install tox tox-travis +script: tox -e $TOX_ENV diff --git a/caravel/assets/js_build.sh b/caravel/assets/js_build.sh new file mode 100755 index 0000000000..78439fcd21 --- /dev/null +++ b/caravel/assets/js_build.sh @@ -0,0 +1,6 @@ +#!/bin/bash +cd "$(dirname "$0")" +npm --version +npm install +npm run lint +npm run prod diff --git a/caravel/assets/visualizations/nvd3_vis.js b/caravel/assets/visualizations/nvd3_vis.js index a6ffe54b20..b80554a125 100644 --- a/caravel/assets/visualizations/nvd3_vis.js +++ b/caravel/assets/visualizations/nvd3_vis.js @@ -13,11 +13,15 @@ function nvd3Vis(slice) { var colorKey = 'key'; var render = function () { - $.getJSON(slice.jsonEndpoint(), function (payload) { + d3.json(slice.jsonEndpoint(), function (error, payload) { + slice.container.html(''); + if (error) { + slice.error(error.responseText); + return ''; + } var fd = payload.form_data; var viz_type = fd.viz_type; var f = d3.format('.3s'); - slice.container.html(''); nv.addGraph(function () { switch (viz_type) { @@ -25,8 +29,7 @@ function nvd3Vis(slice) { if (fd.show_brush) { chart = nv.models.lineWithFocusChart(); chart.lines2.xScale(d3.time.scale.utc()); - chart - .x2Axis + chart.x2Axis .showMaxMin(fd.x_axis_showminmax) .staggerLabels(false); } else { @@ -133,103 +136,100 @@ function nvd3Vis(slice) { default: throw new Error("Unrecognized visualization for nvd3" + viz_type); - } - - if ("showLegend" in chart && typeof fd.show_legend !== 'undefined') { - chart.showLegend(fd.show_legend); - } - - var height = slice.height(); - height -= 15; // accounting for the staggered xAxis - - chart.height(height); - slice.container.css('height', height + 'px'); - - if ((viz_type === "line" || viz_type === "area") && fd.rich_tooltip) { - chart.useInteractiveGuideline(true); - } - if (fd.y_axis_zero) { - chart.forceY([0]); - } else if (fd.y_log_scale) { - chart.yScale(d3.scale.log()); - } - if (fd.x_log_scale) { - chart.xScale(d3.scale.log()); - } - var xAxisFormatter = null; - if (viz_type === 'bubble') { - xAxisFormatter = d3.format('.3s'); - } else if (fd.x_axis_format === 'smart_date') { - xAxisFormatter = px.formatDate; - chart.xAxis.tickFormat(xAxisFormatter); - } else if (fd.x_axis_format !== undefined) { - xAxisFormatter = px.timeFormatFactory(fd.x_axis_format); - chart.xAxis.tickFormat(xAxisFormatter); - } - - if (chart.hasOwnProperty("x2Axis")) { - chart.x2Axis.tickFormat(xAxisFormatter); - height += 30; - } - - if (viz_type === 'bubble') { - chart.xAxis.tickFormat(d3.format('.3s')); - } else if (fd.x_axis_format === 'smart_date') { - chart.xAxis.tickFormat(px.formatDate); - } else if (fd.x_axis_format !== undefined) { - chart.xAxis.tickFormat(px.timeFormatFactory(fd.x_axis_format)); - } - if (chart.yAxis !== undefined) { - chart.yAxis.tickFormat(d3.format('.3s')); - } - - if (fd.contribution || fd.num_period_compare || viz_type === 'compare') { - chart.yAxis.tickFormat(d3.format('.3p')); - if (chart.y2Axis !== undefined) { - chart.y2Axis.tickFormat(d3.format('.3p')); } - } else if (fd.y_axis_format) { - chart.yAxis.tickFormat(d3.format(fd.y_axis_format)); - if (chart.y2Axis !== undefined) { - chart.y2Axis.tickFormat(d3.format(fd.y_axis_format)); + if ("showLegend" in chart && typeof fd.show_legend !== 'undefined') { + chart.showLegend(fd.show_legend); } - } - chart.color(function (d, i) { - return px.color.category21(d[colorKey]); + + var height = slice.height(); + height -= 15; // accounting for the staggered xAxis + + chart.height(height); + slice.container.css('height', height + 'px'); + + if ((viz_type === "line" || viz_type === "area") && fd.rich_tooltip) { + chart.useInteractiveGuideline(true); + } + if (fd.y_axis_zero) { + chart.forceY([0]); + } else if (fd.y_log_scale) { + chart.yScale(d3.scale.log()); + } + if (fd.x_log_scale) { + chart.xScale(d3.scale.log()); + } + var xAxisFormatter = null; + if (viz_type === 'bubble') { + xAxisFormatter = d3.format('.3s'); + } else if (fd.x_axis_format === 'smart_date') { + xAxisFormatter = px.formatDate; + chart.xAxis.tickFormat(xAxisFormatter); + } else if (fd.x_axis_format !== undefined) { + xAxisFormatter = px.timeFormatFactory(fd.x_axis_format); + chart.xAxis.tickFormat(xAxisFormatter); + } + + if (chart.hasOwnProperty("x2Axis")) { + chart.x2Axis.tickFormat(xAxisFormatter); + height += 30; + } + + if (viz_type === 'bubble') { + chart.xAxis.tickFormat(d3.format('.3s')); + } else if (fd.x_axis_format === 'smart_date') { + chart.xAxis.tickFormat(px.formatDate); + } else if (fd.x_axis_format !== undefined) { + chart.xAxis.tickFormat(px.timeFormatFactory(fd.x_axis_format)); + } + if (chart.yAxis !== undefined) { + chart.yAxis.tickFormat(d3.format('.3s')); + } + + if (fd.contribution || fd.num_period_compare || viz_type === 'compare') { + chart.yAxis.tickFormat(d3.format('.3p')); + if (chart.y2Axis !== undefined) { + chart.y2Axis.tickFormat(d3.format('.3p')); + } + } else if (fd.y_axis_format) { + chart.yAxis.tickFormat(d3.format(fd.y_axis_format)); + + if (chart.y2Axis !== undefined) { + chart.y2Axis.tickFormat(d3.format(fd.y_axis_format)); + } + } + chart.color(function (d, i) { + return px.color.category21(d[colorKey]); + }); + + var svg = d3.select(slice.selector).select("svg"); + if (svg.empty()) { + svg = d3.select(slice.selector).append("svg"); + } + + svg + .datum(payload.data) + .transition().duration(500) + .attr('height', height) + .call(chart); + + return chart; }); - var svg = d3.select(slice.selector).select("svg"); - if (svg.empty()) { - svg = d3.select(slice.selector).append("svg"); - } - - svg - .datum(payload.data) - .transition().duration(500) - .attr('height', height) - .call(chart); - - return chart; + slice.done(payload); }); + }; - slice.done(payload); - }) - .fail(function (xhr) { - slice.error(xhr.responseText); - }); -}; + var update = function () { + if (chart && chart.update) { + chart.update(); + } + }; -var update = function () { - if (chart && chart.update) { - chart.update(); - } -}; - -return { - render: render, - resize: update -}; + return { + render: render, + resize: update + }; } module.exports = nvd3Vis; diff --git a/caravel/bin/caravel b/caravel/bin/caravel index ccec716be2..83c6bb6071 100755 --- a/caravel/bin/caravel +++ b/caravel/bin/caravel @@ -58,16 +58,20 @@ def init(): """Inits the Caravel application""" utils.init(caravel) -@manager.command -def version(): +@manager.option( + '-v', '--verbose', action='store_true', + help="Show extra information") +def version(verbose): """Prints the current version number""" s = ( "\n{boat}\n\n" "-----------------------\n" "Caravel {version}\n" - "-----------------------\n").format( + "-----------------------").format( boat=ascii_art.boat, version=caravel.VERSION) print(s) + if verbose: + print("[DB] : " + "{}".format(db.engine)) @manager.option( '-t', '--load-test-data', action='store_true', diff --git a/caravel/migrations/env.py b/caravel/migrations/env.py index 22a5ea76c0..3a313439ba 100755 --- a/caravel/migrations/env.py +++ b/caravel/migrations/env.py @@ -4,7 +4,7 @@ import logging from logging.config import fileConfig from alembic import context -from flask.ext.appbuilder import Base +from flask_appbuilder import Base from sqlalchemy import engine_from_config, pool # this is the Alembic Config object, which provides diff --git a/caravel/migrations/versions/1226819ee0e3_fix_wrong_constraint_on_table_columns.py b/caravel/migrations/versions/1226819ee0e3_fix_wrong_constraint_on_table_columns.py index 9b1d8018c3..e5a9d4fe85 100644 --- a/caravel/migrations/versions/1226819ee0e3_fix_wrong_constraint_on_table_columns.py +++ b/caravel/migrations/versions/1226819ee0e3_fix_wrong_constraint_on_table_columns.py @@ -11,28 +11,41 @@ revision = '1226819ee0e3' down_revision = '956a063c52b3' from alembic import op -import sqlalchemy as sa +from caravel import db, models from caravel.utils import generic_find_constraint_name +import logging naming_convention = { - "fk": - "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", } + def find_constraint_name(upgrade=True): cols = {'column_name'} if upgrade else {'datasource_name'} - return generic_find_constraint_name(table='columns', columns=cols, referenced='datasources') + return generic_find_constraint_name( + table='columns', columns=cols, referenced='datasources', db=db) + def upgrade(): - constraint = find_constraint_name() or 'fk_columns_column_name_datasources' - with op.batch_alter_table("columns", - naming_convention=naming_convention) as batch_op: - batch_op.drop_constraint(constraint, type_="foreignkey") - batch_op.create_foreign_key('fk_columns_datasource_name_datasources', 'datasources', ['datasource_name'], ['datasource_name']) + try: + constraint = find_constraint_name() or 'fk_columns_column_name_datasources' + with op.batch_alter_table("columns", + naming_convention=naming_convention) as batch_op: + batch_op.drop_constraint(constraint, type_="foreignkey") + batch_op.create_foreign_key( + 'fk_columns_datasource_name_datasources', + 'datasources', + ['datasource_name'], ['datasource_name']) + except: + logging.warning( + "Could not find or drop constraint on `columns`") def downgrade(): constraint = find_constraint_name(False) or 'fk_columns_datasource_name_datasources' - with op.batch_alter_table("columns", + with op.batch_alter_table("columns", naming_convention=naming_convention) as batch_op: batch_op.drop_constraint(constraint, type_="foreignkey") - batch_op.create_foreign_key('fk_columns_column_name_datasources', 'datasources', ['column_name'], ['datasource_name']) + batch_op.create_foreign_key( + 'fk_columns_column_name_datasources', + 'datasources', + ['column_name'], ['datasource_name']) diff --git a/caravel/models.py b/caravel/models.py index 6a73cb2223..3ee66847b2 100644 --- a/caravel/models.py +++ b/caravel/models.py @@ -734,7 +734,6 @@ class SqlaTable(Model, Queryable, AuditMixinNullable): qry.compile( engine, compile_kwargs={"literal_binds": True},), ) - print(sql) df = pd.read_sql_query( sql=sql, con=engine @@ -1040,7 +1039,7 @@ class DruidDatasource(Model, AuditMixinNullable, Queryable): @classmethod def sync_to_db(cls, name, cluster): """Fetches metadata for that datasource and merges the Caravel db""" - print("Syncing Druid datasource [{}]".format(name)) + logging.info("Syncing Druid datasource [{}]".format(name)) session = get_session() datasource = session.query(cls).filter_by(datasource_name=name).first() if not datasource: diff --git a/caravel/utils.py b/caravel/utils.py index 9a48b42a67..718b3641e6 100644 --- a/caravel/utils.py +++ b/caravel/utils.py @@ -4,16 +4,16 @@ from __future__ import division from __future__ import print_function from __future__ import unicode_literals +from datetime import datetime import functools import json import logging import numpy -from datetime import datetime +import time import parsedatetime import sqlalchemy as sa from dateutil.parser import parse -from alembic import op from flask import flash, Markup from flask_appbuilder.security.sqla import models as ab_models from markdown import markdown as md @@ -183,21 +183,22 @@ def init(caravel): public_role_like_gamma = \ public_role and config.get('PUBLIC_ROLE_LIKE_GAMMA', False) for perm in perms: - if (perm.view_menu and perm.view_menu.name not in ( - 'ResetPasswordView', - 'RoleModelView', - 'UserDBModelView', - 'Security') and - perm.permission.name not in ( - 'all_datasource_access', - 'can_add', - 'can_download', - 'can_delete', - 'can_edit', - 'can_save', - 'datasource_access', - 'muldelete', - )): + if ( + perm.view_menu and perm.view_menu.name not in ( + 'ResetPasswordView', + 'RoleModelView', + 'UserDBModelView', + 'Security') and + perm.permission.name not in ( + 'all_datasource_access', + 'can_add', + 'can_download', + 'can_delete', + 'can_edit', + 'can_save', + 'datasource_access', + 'muldelete', + )): sm.add_permission_role(gamma, perm) if public_role_like_gamma: sm.add_permission_role(public_role, perm) @@ -222,6 +223,14 @@ def datetime_f(dttm): return "{}".format(dttm) +def base_json_conv(obj): + + if isinstance(obj, numpy.int64): + return int(obj) + elif isinstance(obj, set): + return list(obj) + + def json_iso_dttm_ser(obj): """ json serializer that deals with dates @@ -230,10 +239,25 @@ def json_iso_dttm_ser(obj): >>> json.dumps({'dttm': dttm}, default=json_iso_dttm_ser) '{"dttm": "1970-01-01T00:00:00"}' """ + val = base_json_conv(obj) + if val is not None: + return val if isinstance(obj, datetime): obj = obj.isoformat() - elif isinstance(obj, numpy.int64): - obj = int(obj) + else: + raise TypeError( + "Unserializable object {} of type {}".format(obj, type(obj)) + ) + return obj + + +def json_int_dttm_ser(obj): + """json serializer that deals with dates""" + val = base_json_conv(obj) + if val is not None: + return val + if isinstance(obj, datetime): + obj = int(time.mktime(obj.timetuple())) * 1000 else: raise TypeError( "Unserializable object {} of type {}".format(obj, type(obj)) @@ -259,16 +283,12 @@ def readfile(filepath): return content -def generic_find_constraint_name(table, columns, referenced): - """ - Utility to find a constraint name in alembic migrations - """ - engine = op.get_bind().engine - m = sa.MetaData({}) - t = sa.Table(table, m, autoload=True, autoload_with=engine) +def generic_find_constraint_name(table, columns, referenced, db): + """Utility to find a constraint name in alembic migrations""" + t = sa.Table(table, db.metadata, autoload=True, autoload_with=db.engine) for fk in t.foreign_key_constraints: - if fk.referred_table.name == referenced and \ - set(fk.column_keys) == columns: + if ( + fk.referred_table.name == referenced and + set(fk.column_keys) == columns): return fk.name - return None diff --git a/caravel/views.py b/caravel/views.py index d7ebacc271..f9f5b8a8de 100644 --- a/caravel/views.py +++ b/caravel/views.py @@ -99,7 +99,6 @@ class FilterDashboard(CaravelFilter): .query(Slice.id) .filter(Slice.perm.in_(self.get_perms())) ) - print([r for r in slice_ids_qry.all()]) query = query.filter( Dash.id.in_( db.session.query(Dash.id) @@ -108,7 +107,6 @@ class FilterDashboard(CaravelFilter): .filter(Slice.id.in_(slice_ids_qry)) ) ) - print(query) return query @@ -727,7 +725,6 @@ class Caravel(BaseView): resp = Response( payload, status=status, - headers=generate_download_headers("json"), mimetype="application/json") return resp elif request.args.get("csv") == "true": diff --git a/caravel/viz.py b/caravel/viz.py index 74c30e000c..c908e913f6 100644 --- a/caravel/viz.py +++ b/caravel/viz.py @@ -10,7 +10,6 @@ from __future__ import unicode_literals import copy import hashlib -import json import logging import uuid from collections import OrderedDict, defaultdict @@ -20,7 +19,7 @@ import numpy as np from flask import request from flask_babelpkg import lazy_gettext as _ from markdown import markdown -from pandas.io.json import dumps +import json from six import string_types from werkzeug.datastructures import ImmutableMultiDict from werkzeug.urls import Href @@ -143,6 +142,7 @@ class BaseViz(object): df.timestamp = pd.to_datetime(df.timestamp, utc=False) if self.datasource.offset: df.timestamp += timedelta(hours=self.datasource.offset) + df.replace([np.inf, -np.inf], np.nan) df = df.fillna(0) return df @@ -262,7 +262,7 @@ class BaseViz(object): def json_dumps(self, obj): """Used by get_json, can be overridden to use specific switches""" - return dumps(obj) + return json.dumps(obj, default=utils.json_int_dttm_ser) @property def data(self): @@ -303,7 +303,7 @@ class BaseViz(object): @property def json_data(self): - return dumps(self.data) + return json.dumps(self.data) class TableViz(BaseViz): @@ -824,7 +824,7 @@ class BigNumberViz(BaseViz): def get_data(self): form_data = self.form_data df = self.get_df() - df.sort(columns=df.columns[0], inplace=True) + df.sort_values(by=df.columns[0], inplace=True) compare_lag = form_data.get("compare_lag", "") compare_lag = int(compare_lag) if compare_lag and compare_lag.isdigit() else 0 return { @@ -873,7 +873,7 @@ class BigNumberTotalViz(BaseViz): def get_data(self): form_data = self.form_data df = self.get_df() - df = df.sort(columns=df.columns[0]) + df.sort_values(by=df.columns[0], inplace=True) return { 'data': df.values.tolist(), 'subheader': form_data.get('subheader', ''), @@ -975,7 +975,7 @@ class NVD3TimeSeriesViz(NVD3Viz): for col in df.columns: if col == '': cols.append('N/A') - elif col == None: + elif col is None: cols.append('NULL') else: cols.append(col) @@ -1103,7 +1103,7 @@ class DistributionPieViz(NVD3Viz): df = df.pivot_table( index=self.groupby, values=[self.metrics[0]]) - df.sort(self.metrics[0], ascending=False, inplace=True) + df.sort_values(by=self.metrics[0], ascending=False, inplace=True) return df def get_data(self): diff --git a/dev-reqs.txt b/dev-reqs.txt index 8bbd6bdac5..849a48fd17 100644 --- a/dev-reqs.txt +++ b/dev-reqs.txt @@ -1,6 +1,8 @@ coveralls mock +mysqlclient nose +psycopg2 sphinx sphinx_bootstrap_theme sphinxcontrib.youtube diff --git a/run_tests.sh b/run_tests.sh index 9fbb244b95..37ea9249bb 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash +echo $DB rm /tmp/caravel_unittests.db rm -f .coverage export CARAVEL_CONFIG=tests.caravel_test_config set -e caravel/bin/caravel db upgrade +caravel/bin/caravel version -v python setup.py nosetests diff --git a/setup.py b/setup.py index 993c6c007c..bce802cc63 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ -import imp, os +import imp +import os from setuptools import setup, find_packages version = imp.load_source( diff --git a/tests/caravel_test_config.py b/tests/caravel_test_config.py index 14d9ef75fd..a7de456949 100644 --- a/tests/caravel_test_config.py +++ b/tests/caravel_test_config.py @@ -4,3 +4,9 @@ AUTH_USER_REGISTRATION_ROLE = 'alpha' SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/caravel_unittests.db' DEBUG = True CARAVEL_WEBSERVER_PORT = 8081 + +# Allowing SQLALCHEMY_DATABASE_URI to be defined as an env var for +# continuous integration +if 'CARAVEL__SQLALCHEMY_DATABASE_URI' in os.environ: + SQLALCHEMY_DATABASE_URI = os.environ.get('CARAVEL__SQLALCHEMY_DATABASE_URI') + diff --git a/tests/core_tests.py b/tests/core_tests.py index b450c3e81a..9d5eb9d8ba 100644 --- a/tests/core_tests.py +++ b/tests/core_tests.py @@ -139,12 +139,12 @@ class CoreTests(CaravelTestCase): urls = [] for slc in db.session.query(Slc).all(): urls += [ - (slc.slice_name, slc.slice_url), - (slc.slice_name, slc.viz.json_endpoint), - (slc.slice_name, slc.viz.csv_endpoint), + (slc.slice_name, 'slice_url', slc.slice_url), + (slc.slice_name, 'json_endpoint', slc.viz.json_endpoint), + (slc.slice_name, 'csv_endpoint', slc.viz.csv_endpoint), ] - for name, url in urls: - print("Slice: " + name) + for name, method, url in urls: + print("[name]/[{method}]: {url}".format(**locals())) self.client.get(url) def test_dashboard(self): diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000000..67ddb4d95d --- /dev/null +++ b/tox.ini @@ -0,0 +1,69 @@ +[tox] +envlist = + py27-mysql + py27-sqlite + py27-postgres + py34-mysql + py35-mysql +skipsdist=True + +[global] +wheel_dir = {homedir}/.wheelhouse +find_links = + {homedir}/.wheelhouse + {homedir}/.pip-cache + +[testenv] +deps = + wheel + coveralls +passenv = + HOME + TRAVIS + TRAVIS_BRANCH + TRAVIS_BUILD_DIR + TRAVIS_JOB_ID + USER + TRAVIS_CACHE + TRAVIS_PULL_REQUEST + PATH +commands = + python --version + pip wheel -w {homedir}/.wheelhouse -f {homedir}/.wheelhouse . + pip install --find-links={homedir}/.wheelhouse --no-index . + pip install -r dev-reqs.txt + {toxinidir}/run_tests.sh + coveralls + +[testenv:py27-mysql] +basepython = python2.7 +setenv = + CARAVEL__SQLALCHEMY_DATABASE_URI = mysql://root@localhost/caravel + +[testenv:py34-mysql] +basepython = python3.4 +setenv = + CARAVEL__SQLALCHEMY_DATABASE_URI = mysql://root@localhost/caravel + +[testenv:py35-mysql] +basepython = python3.5 +setenv = + CARAVEL__SQLALCHEMY_DATABASE_URI = mysql://root@localhost/caravel + +[testenv:py27-sqlite] +basepython = python2.7 +setenv = + CARAVEL__SQLALCHEMY_DATABASE_URI = sqlite:////tmp/caravel.db + +[testenv:py34-sqlite] +basepython = python3.4 +setenv = + CARAVEL__SQLALCHEMY_DATABASE_URI = sqlite:////tmp/caravel.db + +[testenv:py27-postgres] +basepython = python2.7 +setenv = + CARAVEL__SQLALCHEMY_DATABASE_URI = postgresql+psycopg2://postgres@localhost/caravel + +[testenv:javascript] +commands = {toxinidir}/caravel/assets/js_build.sh