├── VERSION ├── tests ├── __init__.py ├── test_client.py └── test_api.py ├── Procfile ├── docs ├── vue-logo.png ├── altair-logo.png ├── flask-logo.png ├── python-logo.png └── vuetify-logo.png ├── public ├── favicon.ico └── index.html ├── src ├── assets │ ├── logo.png │ ├── flask-logo.png │ ├── vue-logo.png │ ├── altair-logo.png │ ├── python-logo.png │ ├── vuetify_logo.png │ └── logo.svg ├── plugins │ └── vuetify.js ├── views │ ├── Home.vue │ ├── Api.vue │ └── Landing.vue ├── main.js ├── filters.js ├── router.js ├── backend.js ├── store.js ├── components │ ├── VegaCars.vue │ ├── VueInfo.vue │ └── Books.vue └── App.vue ├── .bumpversion.cfg ├── tox.ini ├── run.py ├── .flaskenv ├── HISTORY.md ├── app ├── client.py ├── __init__.py ├── api │ ├── __init__.py │ ├── security.py │ └── resources.py └── config.py ├── .gitignore ├── Pipfile ├── .travis.yml ├── app.json ├── vue.config.js ├── LICENSE.md ├── package.json ├── README.md └── Pipfile.lock /VERSION: -------------------------------------------------------------------------------- 1 | 0.3.0 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:app --log-file - 2 | -------------------------------------------------------------------------------- /docs/vue-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xujiboy/flask-altair-vuejs-vuetify-example/HEAD/docs/vue-logo.png -------------------------------------------------------------------------------- /docs/altair-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xujiboy/flask-altair-vuejs-vuetify-example/HEAD/docs/altair-logo.png -------------------------------------------------------------------------------- /docs/flask-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xujiboy/flask-altair-vuejs-vuetify-example/HEAD/docs/flask-logo.png -------------------------------------------------------------------------------- /docs/python-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xujiboy/flask-altair-vuejs-vuetify-example/HEAD/docs/python-logo.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xujiboy/flask-altair-vuejs-vuetify-example/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xujiboy/flask-altair-vuejs-vuetify-example/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /docs/vuetify-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xujiboy/flask-altair-vuejs-vuetify-example/HEAD/docs/vuetify-logo.png -------------------------------------------------------------------------------- /src/assets/flask-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xujiboy/flask-altair-vuejs-vuetify-example/HEAD/src/assets/flask-logo.png -------------------------------------------------------------------------------- /src/assets/vue-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xujiboy/flask-altair-vuejs-vuetify-example/HEAD/src/assets/vue-logo.png -------------------------------------------------------------------------------- /src/assets/altair-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xujiboy/flask-altair-vuejs-vuetify-example/HEAD/src/assets/altair-logo.png -------------------------------------------------------------------------------- /src/assets/python-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xujiboy/flask-altair-vuejs-vuetify-example/HEAD/src/assets/python-logo.png -------------------------------------------------------------------------------- /src/assets/vuetify_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xujiboy/flask-altair-vuejs-vuetify-example/HEAD/src/assets/vuetify_logo.png -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.3.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:VERSION] 7 | 8 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts=--maxfail=2 --cov app --cov-report=html 3 | testpaths=tests 4 | [testenv] 5 | deps=pytest 6 | commands=pytest 7 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import os 2 | from app import app 3 | 4 | app.run(port=5000) 5 | 6 | # To Run: 7 | # python run.py 8 | # or 9 | # python -m flask run 10 | -------------------------------------------------------------------------------- /.flaskenv: -------------------------------------------------------------------------------- 1 | # Production Enviroment should be set to 'production' 2 | FLASK_ENV = "development" 3 | FLASK_APP = "app" 4 | # Uncomment this to debug: 5 | # FLASK_DEBUG=1 6 | -------------------------------------------------------------------------------- /src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify/lib' 3 | import 'vuetify/src/stylus/app.styl' 4 | 5 | Vue.use(Vuetify, { 6 | iconfont: 'md' 7 | }) 8 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ### 0.3.0 2 | * Npm > Yarn 3 | * Updated to Flask 1.0 4 | * Clean up config, use env vars 5 | * Simplified client view 6 | * Flatten Templated Folder Structure 7 | * Removed Cli Commads 8 | 9 | ### 0.2.3 10 | * Updated to Vue Cli 3 11 | 12 | ### 0.1.0 13 | * Initial Release 14 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | """ pytests for Flask """ 2 | 3 | import pytest 4 | from app import app 5 | 6 | @pytest.fixture(scope="module") 7 | def client(): 8 | app.config['TESTING'] = True 9 | return app.test_client() 10 | 11 | def test_api(client): 12 | resp = client.get('/') 13 | assert resp.status_code == 200 14 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import './plugins/vuetify' 6 | 7 | import './filters' 8 | 9 | Vue.config.productionTip = false 10 | 11 | new Vue({ 12 | router, 13 | store, 14 | render: h => h(App) 15 | }).$mount('#app') 16 | -------------------------------------------------------------------------------- /app/client.py: -------------------------------------------------------------------------------- 1 | """ Client App """ 2 | 3 | import os 4 | from flask import Blueprint, render_template 5 | 6 | client_bp = Blueprint('client_app', __name__, 7 | url_prefix='', 8 | static_url_path='', 9 | static_folder='./dist/static/', 10 | template_folder='./dist/', 11 | ) 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .cache 3 | .mypy_cache\ 4 | .env 5 | 6 | /dist 7 | .coverage 8 | .pytest_cache/ 9 | htmlcov/ 10 | 11 | .DS_Store 12 | node_modules/ 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | selenium-debug.log 17 | 18 | # Editor directories and files 19 | .idea 20 | .vscode 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *security-sgps.py 26 | *security_backend.py 27 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | gunicorn = "==19.7.1" 8 | flask-restplus = "*" 9 | python-dotenv = "*" 10 | flask = "*" 11 | altair = "*" 12 | vega-datasets = "*" 13 | 14 | [dev-packages] 15 | pytest = "*" 16 | bumpversion = "*" 17 | pytest-sugar = "*" 18 | pytest-cov = "*" 19 | pylint = "*" 20 | 21 | [requires] 22 | python_version = "3.6" 23 | -------------------------------------------------------------------------------- /src/filters.js: -------------------------------------------------------------------------------- 1 | // Vue.js Filters 2 | // https://vuejs.org/v2/guide/filters.html 3 | 4 | import Vue from 'vue' 5 | 6 | let filters = { 7 | 8 | formatTimestamp (timestamp) { 9 | let datetime = new Date(timestamp) 10 | return datetime.toLocaleTimeString('en-US') 11 | } 12 | } 13 | 14 | // Register All Filters on import 15 | Object.keys(filters).forEach(function (filterName) { 16 | Vue.filter(filterName, filters[filterName]) 17 | }) 18 | -------------------------------------------------------------------------------- /src/views/Api.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | 24 | 26 | -------------------------------------------------------------------------------- /src/views/Landing.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 46 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | git: 3 | depth: 2 4 | python: 5 | # - 3.5 6 | - 3.6 7 | branches: 8 | only: 9 | - master 10 | - dev/main 11 | script: 12 | - pytest --cov=app --cov-report=xml 13 | install: 14 | - "pip install --upgrade pip" 15 | - "pip install pipenv" 16 | - "pipenv install --dev" 17 | - "pip install codecov" 18 | after_success: 19 | - codecov 20 | cache: yarn 21 | before_install: 22 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.9.4 23 | - export PATH="$HOME/.yarn/bin:$PATH" 24 | - yarn install 25 | - yarn build 26 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from './views/Home.vue' 4 | import Api from './views/Api.vue' 5 | import Landing from './views/Landing.vue' 6 | 7 | Vue.use(Router) 8 | 9 | export default new Router({ 10 | routes: [ 11 | { 12 | path: '/', 13 | name: 'landing', 14 | component: Landing 15 | }, 16 | { 17 | path: '/home', 18 | name: 'home', 19 | component: Home 20 | }, 21 | { 22 | path: '/api', 23 | name: 'api', 24 | component: Api 25 | } 26 | ] 27 | }) 28 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, current_app, send_file 3 | 4 | from .api import api_bp 5 | from .client import client_bp 6 | 7 | app = Flask(__name__, static_folder='../dist/static') 8 | app.register_blueprint(api_bp) 9 | # app.register_blueprint(client_bp) 10 | 11 | from .config import Config 12 | app.logger.info('>>> {}'.format(Config.FLASK_ENV)) 13 | 14 | @app.route('/') 15 | def index_client(): 16 | dist_dir = current_app.config['DIST_DIR'] 17 | entry = os.path.join(dist_dir, 'index.html') 18 | return send_file(entry) 19 | 20 | 21 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "Flask VueJs Template", 4 | "description": "", 5 | "repository": "https://github.com/gtalarico/flask-vuejs-template", 6 | "logo": "https://raw.githubusercontent.com/gtalarico/flask-vuejs-template/master/app/client/app/src/assets/img/logo.png", 7 | "keywords": ["flask", "vue"], 8 | "env": { 9 | "FLASK_CONFIG": { 10 | "description": "Flask Enviroment Config", 11 | "value": "Production" 12 | }, 13 | "SECRET": { 14 | "description": "Flask Secret Key", 15 | "value": "YourKeyHere" 16 | } 17 | }, 18 | "addons": [ 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /app/api/__init__.py: -------------------------------------------------------------------------------- 1 | """ API Blueprint Application """ 2 | 3 | from flask import Blueprint, current_app 4 | from flask_restplus import Api 5 | 6 | api_bp = Blueprint('api_bp', __name__, url_prefix='/api') #/api is the new endpoint 7 | api_rest = Api(api_bp, version='1.0', doc='/doc') # doc=False to turn off SwaggerUI 8 | 9 | @api_bp.after_request 10 | def add_header(response): 11 | response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization' 12 | return response 13 | 14 | 15 | # Import resources to ensure view is registered 16 | from .resources import * # NOQA 17 | -------------------------------------------------------------------------------- /app/api/security.py: -------------------------------------------------------------------------------- 1 | """ Security Related things """ 2 | from functools import wraps 3 | from flask import request 4 | from flask_restplus import abort 5 | 6 | 7 | def require_auth(func): 8 | """ Secure method decorator """ 9 | @wraps(func) 10 | def wrapper(*args, **kwargs): 11 | # Verify if User is Authenticated 12 | # Authentication logic goes here 13 | if request.headers.get('authorization'): 14 | return func(*args, **kwargs) 15 | else: 16 | return abort(code=401, message='Authorization Required.') 17 | return wrapper 18 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | // const IS_PRODUCTION = process.env.NODE_ENV === 'production' 2 | 3 | module.exports = { 4 | outputDir: 'dist', 5 | assetsDir: 'static', 6 | // baseUrl: IS_PRODUCTION 7 | // ? 'http://cdn123.com' 8 | // : '/', 9 | // For Production, replace set baseUrl to CDN 10 | // And set the CDN origin to `yourdomain.com/static` 11 | // Whitenoise will serve once to CDN which will then cache 12 | // and distribute 13 | devServer: { 14 | proxy: { 15 | '/api*': { 16 | // Forward frontend dev server request for /api to django dev server 17 | target: 'http://localhost:5000/' 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Global Flask Application Setting 3 | 4 | set FLASK_CONFIG to 'development 5 | """ 6 | 7 | import os 8 | from app import app 9 | 10 | 11 | class Config(object): 12 | # If not set fall back to production for safety 13 | FLASK_ENV = os.getenv('FLASK_ENV', 'production') 14 | # Set FLASK_SECRET on your production Environment 15 | SECRET_KEY = os.getenv('FLASK_SECRET', 'Secret') 16 | 17 | APP_DIR = os.path.dirname(__file__) 18 | ROOT_DIR = os.path.dirname(APP_DIR) 19 | DIST_DIR = os.path.join(ROOT_DIR, 'dist') 20 | 21 | if not os.path.exists(DIST_DIR): 22 | raise Exception( 23 | 'DIST_DIR not found: {}'.format(DIST_DIR)) 24 | 25 | app.config.from_object('app.config.Config') 26 | app.config['SWAGGER_UI_JSONEDITOR'] = True # turn on JSON editor in swaggerUI 27 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Flask + Vega + Vue.js + Vuetify Example 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2018] [Gui Talarico] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/backend.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | let $axios = axios.create({ 4 | baseURL: '/api/', 5 | timeout: 5000, 6 | headers: {'Content-Type': 'application/json'} 7 | }) 8 | 9 | // Request Interceptor 10 | $axios.interceptors.request.use(function (config) { 11 | config.headers['Authorization'] = 'Fake Token' 12 | return config 13 | }) 14 | 15 | // Response Interceptor to handle and log errors 16 | $axios.interceptors.response.use(function (response) { 17 | return response 18 | }, function (error) { 19 | // Handle Error 20 | console.log(error) 21 | return Promise.reject(error) 22 | }) 23 | 24 | export default { 25 | 26 | fetchResource () { 27 | return $axios.get(`resource/q`) 28 | .then(response => response.data) 29 | }, 30 | 31 | fetchSecureResource () { 32 | return $axios.get(`secure-resource/xxx`) 33 | .then(response => response.data) 34 | }, 35 | 36 | fetchResourceTest () { 37 | return $axios.get(`test`) 38 | .then(response => response.data) 39 | }, 40 | 41 | fetchBooks () { 42 | return $axios.get(`books`) 43 | .then(response => response.data) 44 | }, 45 | 46 | fetchCars () { 47 | return $axios.get(`vega_cars`) 48 | .then(response => response) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | """ pytests for Flask """ 2 | 3 | import pytest 4 | from app import app 5 | 6 | @pytest.fixture(scope="module") 7 | def client(): 8 | app.config['TESTING'] = True 9 | return app.test_client() 10 | 11 | def test_api(client): 12 | resp = client.get('/api/') 13 | assert resp.status_code == 200 14 | 15 | def test_resource_one(client): 16 | resp = client.get('/api/resource/one') 17 | assert resp.status_code == 200 18 | 19 | def test_resource_one_post(client): 20 | resp = client.post('/api/resource/one') 21 | assert resp.status_code == 201 22 | 23 | def test_resource_one_patch(client): 24 | resp = client.patch('/api/resource/one') 25 | assert resp.status_code == 405 26 | 27 | def test_secure_resource_fail(client): 28 | resp = client.get('/api/secure-resource/two') 29 | assert resp.status_code == 401 30 | 31 | def test_secure_resource_pass(client): 32 | resp = client.get('/api/secure-resource/two', 33 | headers={'authorization': 'Bearer x'}) 34 | assert resp.status_code == 200 35 | 36 | @pytest.fixture(scope="module") 37 | def request_context(): 38 | return app.test_request_context('') 39 | 40 | def test_session(request_context): 41 | with request_context: 42 | # Do something that requires request context 43 | assert True 44 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import $backend from './backend.js' 4 | 5 | Vue.use(Vuex) 6 | 7 | export default new Vuex.Store({ 8 | state: { 9 | cars: null, 10 | books: [], 11 | error: '' 12 | }, 13 | mutations: { // must be synchronous 14 | SET_BOOKS: (state, books) => { 15 | state.books = books 16 | }, 17 | CLEAR_BOOKS: (state) => { 18 | state.books = [] 19 | }, 20 | SET_CARS: (state, cars) => { 21 | state.cars = cars 22 | }, 23 | CLEAR_CARS: (state) => { 24 | state.cars = null 25 | }, 26 | SET_ERROR: (state, error) => { 27 | state.error = error 28 | } 29 | }, 30 | actions: { // can be asynchronous 31 | setCars: (context) => { 32 | $backend.fetchCars() 33 | .then(response => context.commit('SET_CARS', response.data)) 34 | .catch(error => context.commit('SET_ERROR', error)) 35 | }, 36 | clearCars: (context) => { 37 | context.commit('CLEAR_CARS') 38 | }, 39 | setBooks: (context) => { 40 | $backend.fetchBooks() 41 | .then(response => context.commit('SET_BOOKS', response.data)) 42 | .catch(error => context.commit('SET_ERROR', error)) 43 | }, 44 | clearBooks: (context) => { 45 | context.commit('CLEAR_BOOKS') 46 | } 47 | } 48 | }) 49 | -------------------------------------------------------------------------------- /src/components/VegaCars.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue_app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve --open", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "postinstall": "yarn build" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.18.0", 13 | "material-design-icons-iconfont": "^4.0.2", 14 | "swagger-ui": "^3.19.5", 15 | "vega": "^4.3.0", 16 | "vega-embed": "^3.24.1", 17 | "vega-lite": "^3.0.0-rc8", 18 | "vue": "^2.5.13", 19 | "vue-router": "^3.0.1", 20 | "vuetify": "^1.3.8", 21 | "vuex": "^3.0.1" 22 | }, 23 | "devDependencies": { 24 | "@vue/cli-plugin-babel": "^3.0.0-beta.6", 25 | "@vue/cli-plugin-eslint": "^3.0.0-beta.6", 26 | "@vue/cli-service": "^3.0.0-beta.6", 27 | "@vue/eslint-config-standard": "^3.0.0-beta.6", 28 | "lint-staged": "^6.0.0", 29 | "node-sass": "^4.7.2", 30 | "sass-loader": "^6.0.6", 31 | "stylus": "^0.54.5", 32 | "stylus-loader": "^3.0.1", 33 | "vue-cli-plugin-vuetify": "^0.4.6", 34 | "vue-template-compiler": "^2.5.13", 35 | "vuetify-loader": "^1.0.5" 36 | }, 37 | "babel": { 38 | "presets": [ 39 | "@vue/app" 40 | ] 41 | }, 42 | "eslintConfig": { 43 | "root": true, 44 | "extends": [ 45 | "plugin:vue/essential", 46 | "@vue/standard" 47 | ] 48 | }, 49 | "postcss": { 50 | "plugins": { 51 | "autoprefixer": {} 52 | } 53 | }, 54 | "browserslist": [ 55 | "> 1%", 56 | "last 2 versions", 57 | "not ie <= 8" 58 | ], 59 | "gitHooks": { 60 | "pre-commit": "lint-staged" 61 | }, 62 | "lint-staged": { 63 | "*.js": [ 64 | "vue-cli-service lint", 65 | "git add" 66 | ], 67 | "*.vue": [ 68 | "vue-cli-service lint", 69 | "git add" 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/VueInfo.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 40 | 41 | 42 | 58 | -------------------------------------------------------------------------------- /src/components/Books.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 78 | -------------------------------------------------------------------------------- /app/api/resources.py: -------------------------------------------------------------------------------- 1 | """ 2 | REST API Resource Routing 3 | http://flask-restplus.readthedocs.io 4 | """ 5 | 6 | from datetime import datetime 7 | from flask import request 8 | from flask_restplus import Resource, fields 9 | 10 | from .security import require_auth 11 | from . import api_rest 12 | 13 | import altair 14 | from vega_datasets import data 15 | cars = data.cars() 16 | 17 | 18 | class SecureResource(Resource): 19 | """ Calls require_auth decorator on all requests """ 20 | method_decorators = [require_auth] 21 | 22 | 23 | @api_rest.route('/resource/') 24 | class ResourceOne(Resource): 25 | """ Unsecure Resource Class: Inherit from Resource """ 26 | 27 | def get(self, resource_id): 28 | timestamp = datetime.utcnow().isoformat() 29 | return {'timestamp': timestamp} 30 | 31 | def post(self, resource_id): 32 | json_payload = request.json 33 | return {'timestamp': json_payload}, 201 34 | 35 | 36 | @api_rest.route('/secure-resource/') 37 | class SecureResourceOne(SecureResource): 38 | """ Unsecure Resource Class: Inherit from Resource """ 39 | 40 | def get(self, resource_id): 41 | timestamp = datetime.utcnow().isoformat() 42 | return {'timestamp': timestamp} 43 | 44 | 45 | model_book = api_rest.model('Books', { 46 | 'title' : fields.String('Title of the book.'), 47 | 'author' : fields.String('Author of the book.') 48 | }) 49 | 50 | books = [ 51 | {'title': 'The Adventures of Huckleberry Finn', 'author': 'Mark Twain'}, 52 | {'title': 'Frankenstein', 'author': 'Mary Shelley'}, 53 | {'title': 'Pride and Prejudice', 'author': 'Jane Austen'}, 54 | {'title': 'The Little Prince', 'author': 'Antoine de Saint-Exupéry'}, 55 | {'title': 'The Color Purple', 'author': 'Alice Walker'}, 56 | {'title': 'Little Women', 'author': 'Louisa May Alcott'}, 57 | {'title': 'Great Expectations', 'author': 'Charles Dickens'}, 58 | {'title': 'The Scarlet Letter', 'author': 'Nathaniel Hawthorne'}, 59 | {'title': 'Gone with the Wind', 'author': 'Margaret Mitchell'}, 60 | {'title': 'The Picture of Dorian Gray', 'author': 'Oscar Wilde'}, 61 | {'title': 'The Metamorphosis', 'author': 'Franz Kafka'} 62 | ] 63 | 64 | 65 | @api_rest.route('/books') 66 | class Books(Resource): 67 | 68 | @api_rest.marshal_with(model_book, envelope='data') 69 | def get(self): 70 | return books 71 | 72 | # @api_rest.expect(model_book) 73 | # def post(self): 74 | # new_book = api_rest.payload 75 | # new_book['id'] = len(books) + 1 76 | # books.append(new_book) 77 | # return {'result' : 'book added'}, 201 78 | 79 | @api_rest.route('/vega_cars') 80 | class VegaCars(Resource): 81 | 82 | def get(self): 83 | chart = altair.Chart(cars).mark_point().encode( 84 | x='Horsepower', 85 | y='Miles_per_Gallon', 86 | color='Origin', 87 | tooltip='Name' 88 | ).interactive() 89 | return chart.to_dict() 90 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 84 | 85 | 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask-Altair-VueJs-Vuetify Example 2 | 3 | _An demonstration of data-visualization web app with basic dashboard UI_ 4 | 5 | ![Flask Logo](/docs/flask-logo.png "Flask Logo") 6 | ![Altair logo](docs/altair-logo.png "altair Logo") 7 | ![Vue Logo](/docs/vue-logo.png "Vue Logo") 8 | ![Vuetify Logo](/docs/vuetify-logo.png "Vuetify Logo") 9 | 10 | ## Intro 11 | This example is built upon Gui Talarico's [Flask-Vuejs-Template](https://github.com/gtalarico/flask-vuejs-template) with additional inspirations from these following online resources: 12 | * [xhochy/altair-vue-vega-example](https://github.com/xhochy/altair-vue-vega-example) 13 | * [oleg-agapov/flask-vue-spa](https://github.com/oleg-agapov/flask-vue-spa) 14 | * [oleg-agapov/basic-spa-vue-firebase](https://github.com/oleg-agapov/basic-spa-vue-firebase/tree/part-1) 15 | 16 | 17 | ## Features 18 | * Flask backend with [Flask-RestPlus](http://flask-restplus.readthedocs.io) API 19 | * [Altair](https://altair-viz.github.io/) plotting and Vega spec json object serving from backend 20 | * [Vega](https://vega.github.io/vega/) and [Vega-Embed](https://github.com/vega/vega-embed) for plot rendering at frontend 21 | * [vue-cli 3](https://github.com/vuejs/vue-cli/blob/dev/docs/README.md) + yarn 22 | * [Vuex](https://vuex.vuejs.org/) state management 23 | * [Vue Router](https://router.vuejs.org/) 24 | * [Vuetify material design](https://vuetifyjs.com/en/) for dashboard UI 25 | * [Axios](https://vuex.vuejs.org/) for backend communication 26 | * Heroku Configuration with one-click deployment + Gunicorn 27 | 28 | ## Demo 29 | [Live Demo](https://flask-altair-vuejs-vuetify-exp.herokuapp.com/#/api) 30 | 31 | ## Repo Structure and Usage 32 | 33 | The details on the directory structure and usage can be found in the `README.md` file of Gui Talarico's [repo](https://github.com/gtalarico/flask-vuejs-template). For reader's convenience, here I will only copy and paste the [key files](#Key-Files) and some basic [installation](Installation) instructions below. 34 | 35 | #### Key Files 36 | 37 | | Location | Content | 38 | |----------------------|--------------------------------------------| 39 | | `/app` | Flask Application | 40 | | `/app/api` | Flask Rest Api (`/api`) | 41 | | `/app/client.py` | Flask Client (`/`) | 42 | | `/src` | Vue App . | 43 | | `/src/main.js` | JS Application Entry Point | 44 | | `/public/index.html` | Html Application Entry Point (`/`) | 45 | | `/public/static` | Static Assets | 46 | | `/dist/` | Bundled Assets Output (generated at `yarn build` | 47 | 48 | 49 | ## Installation 50 | 51 | ##### Before you start 52 | 53 | Before getting started, you should have the following installed and running: 54 | 55 | - [X] Yarn - [instructions](https://yarnpkg.com/en/docs/install#mac-stable) 56 | - [X] Vue Cli 3 - [instructions](https://cli.vuejs.org/guide/installation.html) 57 | - [X] Python 3 58 | - [X] Pipenv (optional) 59 | - [X] Heroku Cli (if deploying to Heroku) 60 | 61 | ##### Template and Dependencies 62 | 63 | * Clone this repository: 64 | 65 | ``` 66 | $ git clone git@github.com:xujiboy/flask-vega-vuejs-vuetify-example.git 67 | ``` 68 | 69 | * Setup virtual environment, install dependencies, and activate it: 70 | 71 | ``` 72 | $ pipenv install --dev 73 | $ pipenv shell 74 | ``` 75 | 76 | * Install JS dependencies 77 | 78 | ``` 79 | $ yarn install 80 | ``` 81 | 82 | 83 | ## Development Server 84 | 85 | Run Flask Api development server: 86 | 87 | ``` 88 | $ python run.py 89 | ``` 90 | 91 | From another tab in the same directory, start the webpack dev server: 92 | 93 | ``` 94 | $ yarn serve 95 | ``` 96 | 97 | The Vuejs application will be served from `localhost:8080` and the Flask Api 98 | and static files will be served from `localhost:5000`. 99 | 100 | The dual dev-server setup allows you to take advantage of 101 | webpack's development server with hot module replacement. 102 | 103 | Proxy config in `vue.config.js` is used to route the requests 104 | back to Flask's Api on port 5000. 105 | 106 | If you would rather run a single dev server, you can run Flask's 107 | development server only on `:5000`, but you have to build build the Vue app first 108 | and the page will not reload on changes. 109 | 110 | ``` 111 | $ yarn build 112 | $ python run.py 113 | ``` 114 | 115 | 116 | ## Production Server and Heroku Deployment 117 | 118 | Please refer to the `README.md` file of Gui Talarico's [repo](https://github.com/gtalarico/flask-vuejs-template). 119 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "888a7c368fb141f0ebdf0fa3e39ef9ae87ea8b8f5314329de8c8aa98a2c093b2" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "altair": { 20 | "hashes": [ 21 | "sha256:6bdbc62a9b3fc22212e548a2a5f068c0dd378a9f5d99f1016346d7637894cfa9", 22 | "sha256:c158699026eb5a19f95c1ca742e2e82bc20c27013ef5785f10836283e2233f8a" 23 | ], 24 | "index": "pypi", 25 | "version": "==2.2.2" 26 | }, 27 | "aniso8601": { 28 | "hashes": [ 29 | "sha256:547e7bc88c19742e519fb4ca39f4b8113fdfb8fca322e325f16a8bfc6cfc553c", 30 | "sha256:e7560de91bf00baa712b2550a2fdebf0188c5fce2fcd1162fbac75c19bb29c95" 31 | ], 32 | "version": "==4.0.1" 33 | }, 34 | "click": { 35 | "hashes": [ 36 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 37 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 38 | ], 39 | "version": "==7.0" 40 | }, 41 | "entrypoints": { 42 | "hashes": [ 43 | "sha256:10ad569bb245e7e2ba425285b9fa3e8178a0dc92fc53b1e1c553805e15a8825b", 44 | "sha256:d2d587dde06f99545fb13a383d2cd336a8ff1f359c5839ce3a64c917d10c029f" 45 | ], 46 | "version": "==0.2.3" 47 | }, 48 | "flask": { 49 | "hashes": [ 50 | "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", 51 | "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" 52 | ], 53 | "index": "pypi", 54 | "version": "==1.0.2" 55 | }, 56 | "flask-restplus": { 57 | "hashes": [ 58 | "sha256:3fad697e1d91dfc13c078abcb86003f438a751c5a4ff41b84c9050199d2eab62", 59 | "sha256:cdc27b5be63f12968a7f762eaa355e68228b0c904b4c96040a314ba7dc6d0e69" 60 | ], 61 | "index": "pypi", 62 | "version": "==0.12.1" 63 | }, 64 | "gunicorn": { 65 | "hashes": [ 66 | "sha256:75af03c99389535f218cc596c7de74df4763803f7b63eb09d77e92b3956b36c6", 67 | "sha256:eee1169f0ca667be05db3351a0960765620dad53f53434262ff8901b68a1b622" 68 | ], 69 | "index": "pypi", 70 | "version": "==19.7.1" 71 | }, 72 | "itsdangerous": { 73 | "hashes": [ 74 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 75 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 76 | ], 77 | "version": "==1.1.0" 78 | }, 79 | "jinja2": { 80 | "hashes": [ 81 | "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", 82 | "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" 83 | ], 84 | "version": "==2.10" 85 | }, 86 | "jsonschema": { 87 | "hashes": [ 88 | "sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08", 89 | "sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02" 90 | ], 91 | "version": "==2.6.0" 92 | }, 93 | "markupsafe": { 94 | "hashes": [ 95 | "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", 96 | "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", 97 | "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", 98 | "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", 99 | "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", 100 | "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", 101 | "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", 102 | "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", 103 | "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", 104 | "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", 105 | "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", 106 | "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", 107 | "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", 108 | "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", 109 | "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", 110 | "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", 111 | "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", 112 | "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", 113 | "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", 114 | "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", 115 | "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", 116 | "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", 117 | "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", 118 | "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", 119 | "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", 120 | "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", 121 | "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", 122 | "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" 123 | ], 124 | "version": "==1.1.0" 125 | }, 126 | "numpy": { 127 | "hashes": [ 128 | "sha256:0df89ca13c25eaa1621a3f09af4c8ba20da849692dcae184cb55e80952c453fb", 129 | "sha256:154c35f195fd3e1fad2569930ca51907057ae35e03938f89a8aedae91dd1b7c7", 130 | "sha256:18e84323cdb8de3325e741a7a8dd4a82db74fde363dce32b625324c7b32aa6d7", 131 | "sha256:1e8956c37fc138d65ded2d96ab3949bd49038cc6e8a4494b1515b0ba88c91565", 132 | "sha256:23557bdbca3ccbde3abaa12a6e82299bc92d2b9139011f8c16ca1bb8c75d1e95", 133 | "sha256:24fd645a5e5d224aa6e39d93e4a722fafa9160154f296fd5ef9580191c755053", 134 | "sha256:36e36b6868e4440760d4b9b44587ea1dc1f06532858d10abba98e851e154ca70", 135 | "sha256:3d734559db35aa3697dadcea492a423118c5c55d176da2f3be9c98d4803fc2a7", 136 | "sha256:416a2070acf3a2b5d586f9a6507bb97e33574df5bd7508ea970bbf4fc563fa52", 137 | "sha256:4a22dc3f5221a644dfe4a63bf990052cc674ef12a157b1056969079985c92816", 138 | "sha256:4d8d3e5aa6087490912c14a3c10fbdd380b40b421c13920ff468163bc50e016f", 139 | "sha256:4f41fd159fba1245e1958a99d349df49c616b133636e0cf668f169bce2aeac2d", 140 | "sha256:561ef098c50f91fbac2cc9305b68c915e9eb915a74d9038ecf8af274d748f76f", 141 | "sha256:56994e14b386b5c0a9b875a76d22d707b315fa037affc7819cda08b6d0489756", 142 | "sha256:73a1f2a529604c50c262179fcca59c87a05ff4614fe8a15c186934d84d09d9a5", 143 | "sha256:7da99445fd890206bfcc7419f79871ba8e73d9d9e6b82fe09980bc5bb4efc35f", 144 | "sha256:99d59e0bcadac4aa3280616591fb7bcd560e2218f5e31d5223a2e12a1425d495", 145 | "sha256:a4cc09489843c70b22e8373ca3dfa52b3fab778b57cf81462f1203b0852e95e3", 146 | "sha256:a61dc29cfca9831a03442a21d4b5fd77e3067beca4b5f81f1a89a04a71cf93fa", 147 | "sha256:b1853df739b32fa913cc59ad9137caa9cc3d97ff871e2bbd89c2a2a1d4a69451", 148 | "sha256:b1f44c335532c0581b77491b7715a871d0dd72e97487ac0f57337ccf3ab3469b", 149 | "sha256:b261e0cb0d6faa8fd6863af26d30351fd2ffdb15b82e51e81e96b9e9e2e7ba16", 150 | "sha256:c857ae5dba375ea26a6228f98c195fec0898a0fd91bcf0e8a0cae6d9faf3eca7", 151 | "sha256:cf5bb4a7d53a71bb6a0144d31df784a973b36d8687d615ef6a7e9b1809917a9b", 152 | "sha256:db9814ff0457b46f2e1d494c1efa4111ca089e08c8b983635ebffb9c1573361f", 153 | "sha256:df04f4bad8a359daa2ff74f8108ea051670cafbca533bb2636c58b16e962989e", 154 | "sha256:ecf81720934a0e18526177e645cbd6a8a21bb0ddc887ff9738de07a1df5c6b61", 155 | "sha256:edfa6fba9157e0e3be0f40168eb142511012683ac3dc82420bee4a3f3981b30e" 156 | ], 157 | "version": "==1.15.4" 158 | }, 159 | "pandas": { 160 | "hashes": [ 161 | "sha256:11975fad9edbdb55f1a560d96f91830e83e29bed6ad5ebf506abda09818eaf60", 162 | "sha256:12e13d127ca1b585dd6f6840d3fe3fa6e46c36a6afe2dbc5cb0b57032c902e31", 163 | "sha256:1c87fcb201e1e06f66e23a61a5fea9eeebfe7204a66d99df24600e3f05168051", 164 | "sha256:242e9900de758e137304ad4b5663c2eff0d798c2c3b891250bd0bd97144579da", 165 | "sha256:26c903d0ae1542890cb9abadb4adcb18f356b14c2df46e4ff657ae640e3ac9e7", 166 | "sha256:2e1e88f9d3e5f107b65b59cd29f141995597b035d17cc5537e58142038942e1a", 167 | "sha256:31b7a48b344c14691a8e92765d4023f88902ba3e96e2e4d0364d3453cdfd50db", 168 | "sha256:4fd07a932b4352f8a8973761ab4e84f965bf81cc750fb38e04f01088ab901cb8", 169 | "sha256:5b24ca47acf69222e82530e89111dd9d14f9b970ab2cd3a1c2c78f0c4fbba4f4", 170 | "sha256:647b3b916cc8f6aeba240c8171be3ab799c3c1b2ea179a3be0bd2712c4237553", 171 | "sha256:66b060946046ca27c0e03e9bec9bba3e0b918bafff84c425ca2cc2e157ce121e", 172 | "sha256:6efa9fa6e1434141df8872d0fa4226fc301b17aacf37429193f9d70b426ea28f", 173 | "sha256:be4715c9d8367e51dbe6bc6d05e205b1ae234f0dc5465931014aa1c4af44c1ba", 174 | "sha256:bea90da782d8e945fccfc958585210d23de374fa9294a9481ed2abcef637ebfc", 175 | "sha256:d318d77ab96f66a59e792a481e2701fba879e1a453aefeebdb17444fe204d1ed", 176 | "sha256:d785fc08d6f4207437e900ffead930a61e634c5e4f980ba6d3dc03c9581748c7", 177 | "sha256:de9559287c4fe8da56e8c3878d2374abc19d1ba2b807bfa7553e912a8e5ba87c", 178 | "sha256:f4f98b190bb918ac0bc0e3dd2ab74ff3573da9f43106f6dba6385406912ec00f", 179 | "sha256:f71f1a7e2d03758f6e957896ed696254e2bc83110ddbc6942018f1a232dd9dad", 180 | "sha256:fb944c8f0b0ab5c1f7846c686bc4cdf8cde7224655c12edcd59d5212cd57bec0" 181 | ], 182 | "version": "==0.23.4" 183 | }, 184 | "python-dateutil": { 185 | "hashes": [ 186 | "sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93", 187 | "sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02" 188 | ], 189 | "version": "==2.7.5" 190 | }, 191 | "python-dotenv": { 192 | "hashes": [ 193 | "sha256:122290a38ece9fe4f162dc7c95cae3357b983505830a154d3c98ef7f6c6cea77", 194 | "sha256:4a205787bc829233de2a823aa328e44fd9996fedb954989a21f1fc67c13d7a77" 195 | ], 196 | "index": "pypi", 197 | "version": "==0.9.1" 198 | }, 199 | "pytz": { 200 | "hashes": [ 201 | "sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca", 202 | "sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6" 203 | ], 204 | "version": "==2018.7" 205 | }, 206 | "six": { 207 | "hashes": [ 208 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", 209 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" 210 | ], 211 | "version": "==1.11.0" 212 | }, 213 | "toolz": { 214 | "hashes": [ 215 | "sha256:929f0a7ea7f61c178bd951bdae93920515d3fbdbafc8e6caf82d752b9b3b31c9" 216 | ], 217 | "version": "==0.9.0" 218 | }, 219 | "typing": { 220 | "hashes": [ 221 | "sha256:4027c5f6127a6267a435201981ba156de91ad0d1d98e9ddc2aa173453453492d", 222 | "sha256:57dcf675a99b74d64dacf6fba08fb17cf7e3d5fdff53d4a30ea2a5e7e52543d4", 223 | "sha256:a4c8473ce11a65999c8f59cb093e70686b6c84c98df58c1dae9b3b196089858a" 224 | ], 225 | "version": "==3.6.6" 226 | }, 227 | "vega-datasets": { 228 | "hashes": [ 229 | "sha256:1fa672ba89ded093b30c6d59fce10aca3ac7c927df254e588da7b6d14f695181", 230 | "sha256:4d93760d2ca439180a8f4d70e9bd9a4fe695113757dbde9d8dcaafe3676b0e84" 231 | ], 232 | "index": "pypi", 233 | "version": "==0.5.0" 234 | }, 235 | "werkzeug": { 236 | "hashes": [ 237 | "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", 238 | "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" 239 | ], 240 | "version": "==0.14.1" 241 | } 242 | }, 243 | "develop": { 244 | "astroid": { 245 | "hashes": [ 246 | "sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be", 247 | "sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d" 248 | ], 249 | "version": "==2.0.4" 250 | }, 251 | "atomicwrites": { 252 | "hashes": [ 253 | "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", 254 | "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" 255 | ], 256 | "version": "==1.2.1" 257 | }, 258 | "attrs": { 259 | "hashes": [ 260 | "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", 261 | "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" 262 | ], 263 | "version": "==18.2.0" 264 | }, 265 | "bumpversion": { 266 | "hashes": [ 267 | "sha256:6744c873dd7aafc24453d8b6a1a0d6d109faf63cd0cd19cb78fd46e74932c77e", 268 | "sha256:6753d9ff3552013e2130f7bc03c1007e24473b4835952679653fb132367bdd57" 269 | ], 270 | "index": "pypi", 271 | "version": "==0.5.3" 272 | }, 273 | "coverage": { 274 | "hashes": [ 275 | "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", 276 | "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", 277 | "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", 278 | "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", 279 | "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", 280 | "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", 281 | "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", 282 | "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", 283 | "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", 284 | "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", 285 | "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", 286 | "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", 287 | "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", 288 | "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", 289 | "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", 290 | "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", 291 | "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", 292 | "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", 293 | "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", 294 | "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", 295 | "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", 296 | "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", 297 | "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", 298 | "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", 299 | "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", 300 | "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", 301 | "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", 302 | "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", 303 | "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", 304 | "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", 305 | "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" 306 | ], 307 | "version": "==4.5.2" 308 | }, 309 | "isort": { 310 | "hashes": [ 311 | "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", 312 | "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", 313 | "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" 314 | ], 315 | "version": "==4.3.4" 316 | }, 317 | "lazy-object-proxy": { 318 | "hashes": [ 319 | "sha256:350173a273e734c9233d1a6141f998e1864adc189b9c05996f5bb3ef53562283" 320 | ], 321 | "version": "==1.3.1" 322 | }, 323 | "mccabe": { 324 | "hashes": [ 325 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 326 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 327 | ], 328 | "version": "==0.6.1" 329 | }, 330 | "more-itertools": { 331 | "hashes": [ 332 | "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", 333 | "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", 334 | "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d" 335 | ], 336 | "version": "==4.3.0" 337 | }, 338 | "packaging": { 339 | "hashes": [ 340 | "sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807", 341 | "sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9" 342 | ], 343 | "version": "==18.0" 344 | }, 345 | "pluggy": { 346 | "hashes": [ 347 | "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", 348 | "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" 349 | ], 350 | "version": "==0.8.0" 351 | }, 352 | "py": { 353 | "hashes": [ 354 | "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", 355 | "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" 356 | ], 357 | "version": "==1.7.0" 358 | }, 359 | "pylint": { 360 | "hashes": [ 361 | "sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec", 362 | "sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb" 363 | ], 364 | "index": "pypi", 365 | "version": "==2.1.1" 366 | }, 367 | "pyparsing": { 368 | "hashes": [ 369 | "sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b", 370 | "sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592" 371 | ], 372 | "version": "==2.3.0" 373 | }, 374 | "pytest": { 375 | "hashes": [ 376 | "sha256:488c842647bbeb350029da10325cb40af0a9c7a2fdda45aeb1dda75b60048ffb", 377 | "sha256:c055690dfefa744992f563e8c3a654089a6aa5b8092dded9b6fafbd70b2e45a7" 378 | ], 379 | "index": "pypi", 380 | "version": "==4.0.0" 381 | }, 382 | "pytest-cov": { 383 | "hashes": [ 384 | "sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7", 385 | "sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762" 386 | ], 387 | "index": "pypi", 388 | "version": "==2.6.0" 389 | }, 390 | "pytest-sugar": { 391 | "hashes": [ 392 | "sha256:26cf8289fe10880cbbc130bd77398c4e6a8b936d8393b116a5c16121d95ab283", 393 | "sha256:fcd87a74b2bce5386d244b49ad60549bfbc4602527797fac167da147983f58ab" 394 | ], 395 | "index": "pypi", 396 | "version": "==0.9.2" 397 | }, 398 | "six": { 399 | "hashes": [ 400 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", 401 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" 402 | ], 403 | "version": "==1.11.0" 404 | }, 405 | "termcolor": { 406 | "hashes": [ 407 | "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b" 408 | ], 409 | "version": "==1.1.0" 410 | }, 411 | "typed-ast": { 412 | "hashes": [ 413 | "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe" 414 | ], 415 | "markers": "python_version < '3.7' and implementation_name == 'cpython'", 416 | "version": "==1.1.0" 417 | }, 418 | "wrapt": { 419 | "hashes": [ 420 | "sha256:7bb495f09894e0a375d0a867c1f3f0d444eed0dfed0c2a498023383012fe7f9a" 421 | ], 422 | "version": "==1.10.11" 423 | } 424 | } 425 | } 426 | --------------------------------------------------------------------------------