├── tests
├── __init__.py
├── test_client.py
└── test_api.py
├── docs
├── vue-logo.png
├── flask-logo.png
├── python-logo.png
└── project-logo.png
├── requirements.txt
├── public
├── favicon.ico
└── index.html
├── src
├── assets
│ ├── logo.png
│ ├── flask-logo.png
│ ├── vue-logo.png
│ └── python-logo.png
├── store.js
├── main.js
├── views
│ ├── Home.vue
│ └── Api.vue
├── router.js
├── filters.js
├── backend.js
├── App.vue
└── components
│ └── HelloWorld.vue
├── run.py
├── .flaskenv
├── Pipfile
├── app
├── client.py
├── api
│ ├── __init__.py
│ ├── security.py
│ └── resources.py
├── __init__.py
└── config.py
├── .github
├── CODE_OF_CONDUCT.md
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ └── build_and_deploy.yaml
├── vue.config.js
├── LICENSE.md
├── package.json
├── .gitignore
├── CONTRIBUTING.md
├── README.md
└── Pipfile.lock
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/vue-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/flask-vuejs-webapp/HEAD/docs/vue-logo.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/flask-vuejs-webapp/HEAD/requirements.txt
--------------------------------------------------------------------------------
/docs/flask-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/flask-vuejs-webapp/HEAD/docs/flask-logo.png
--------------------------------------------------------------------------------
/docs/python-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/flask-vuejs-webapp/HEAD/docs/python-logo.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/flask-vuejs-webapp/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/flask-vuejs-webapp/HEAD/src/assets/logo.png
--------------------------------------------------------------------------------
/docs/project-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/flask-vuejs-webapp/HEAD/docs/project-logo.png
--------------------------------------------------------------------------------
/src/assets/flask-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/flask-vuejs-webapp/HEAD/src/assets/flask-logo.png
--------------------------------------------------------------------------------
/src/assets/vue-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/flask-vuejs-webapp/HEAD/src/assets/vue-logo.png
--------------------------------------------------------------------------------
/src/assets/python-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/flask-vuejs-webapp/HEAD/src/assets/python-logo.png
--------------------------------------------------------------------------------
/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 Environment should be set to 'production'
2 | FLASK_ENV = "development"
3 | FLASK_APP = "app"
4 | # Uncomment this to debug:
5 | # FLASK_DEBUG=1
6 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | Vue.use(Vuex)
5 |
6 | export default new Vuex.Store({
7 | state: {
8 |
9 | },
10 | mutations: {
11 |
12 | },
13 | actions: {
14 |
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/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 |
6 | import './filters'
7 |
8 | Vue.config.productionTip = false
9 |
10 | new Vue({
11 | router,
12 | store,
13 | render: h => h(App)
14 | }).$mount('#app')
15 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
12 | [dev-packages]
13 | pytest = "*"
14 | bumpversion = "*"
15 | pytest-sugar = "*"
16 | pytest-cov = "*"
17 |
18 | [requires]
19 | python_version = "3.6"
20 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
18 |
19 |
22 |
--------------------------------------------------------------------------------
/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 |
6 | Vue.use(Router)
7 |
8 | export default new Router({
9 | routes: [
10 | {
11 | path: '/',
12 | name: 'home',
13 | component: Home
14 | },
15 | {
16 | path: '/api',
17 | name: 'api',
18 | component: Api
19 | }
20 | ]
21 | })
22 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Microsoft Open Source Code of Conduct
2 |
3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
4 |
5 | Resources:
6 |
7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
10 |
--------------------------------------------------------------------------------
/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')
7 | api_rest = Api(api_bp)
8 |
9 |
10 | @api_bp.after_request
11 | def add_header(response):
12 | response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization'
13 | return response
14 |
15 |
16 | # Import resources to ensure view is registered
17 | from .resources import * # NOQA
18 |
--------------------------------------------------------------------------------
/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(401)
17 | return wrapper
18 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Flask + Vue.js Template
9 |
10 |
11 |
14 |
15 |
16 |
17 |
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 flask dev server
17 | target: 'http://localhost:5000/'
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/config.py:
--------------------------------------------------------------------------------
1 | """
2 | Global Flask Application Setting
3 |
4 | See `.flaskenv` for default settings.
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 |
--------------------------------------------------------------------------------
/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/xxx`)
28 | .then(response => response.data)
29 | },
30 |
31 | fetchSecureResource () {
32 | return $axios.get(`secure-resource/zzz`)
33 | .then(response => response.data)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
4 | > Please provide us with the following information:
5 | > ---------------------------------------------------------------
6 |
7 | ### This issue is for a: (mark with an `x`)
8 | ```
9 | - [ ] bug report -> please search issues before submitting
10 | - [ ] feature request
11 | - [ ] documentation issue or request
12 | - [ ] regression (a behavior that used to work and stopped in a new release)
13 | ```
14 |
15 | ### Minimal steps to reproduce
16 | >
17 |
18 | ### Any log messages given by the failure
19 | >
20 |
21 | ### Expected/desired behavior
22 | >
23 |
24 | ### OS and Version?
25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?)
26 |
27 | ### Versions
28 | >
29 |
30 | ### Mention any other details that might be useful
31 |
32 | > ---------------------------------------------------------------
33 | > Thanks! We'll be in touch soon.
34 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Purpose
2 |
3 | * ...
4 |
5 | ## Does this introduce a breaking change?
6 |
7 | ```
8 | [ ] Yes
9 | [ ] No
10 | ```
11 |
12 | ## Pull Request Type
13 | What kind of change does this Pull Request introduce?
14 |
15 |
16 | ```
17 | [ ] Bugfix
18 | [ ] Feature
19 | [ ] Code style update (formatting, local variables)
20 | [ ] Refactoring (no functional changes, no api changes)
21 | [ ] Documentation content changes
22 | [ ] Other... Please describe:
23 | ```
24 |
25 | ## How to Test
26 | * Get the code
27 |
28 | ```
29 | git clone [repo-address]
30 | cd [repo-name]
31 | git checkout [branch-name]
32 | npm install
33 | ```
34 |
35 | * Test the code
36 |
37 | ```
38 | ```
39 |
40 | ## What to Check
41 | Verify that the following are valid
42 | * ...
43 |
44 | ## Other Information
45 |
--------------------------------------------------------------------------------
/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
9 |
10 | from .security import require_auth
11 | from . import api_rest
12 |
13 |
14 | class SecureResource(Resource):
15 | """ Calls require_auth decorator on all requests """
16 | method_decorators = [require_auth]
17 |
18 |
19 | @api_rest.route('/resource/')
20 | class ResourceOne(Resource):
21 | """ Unsecure Resource Class: Inherit from Resource """
22 |
23 | def get(self, resource_id):
24 | timestamp = datetime.utcnow().isoformat()
25 | return {'timestamp': timestamp}
26 |
27 | def post(self, resource_id):
28 | json_payload = request.json
29 | return {'timestamp': json_payload}, 201
30 |
31 |
32 | @api_rest.route('/secure-resource/')
33 | class SecureResourceOne(SecureResource):
34 | """ Unsecure Resource Class: Inherit from Resource """
35 |
36 | def get(self, resource_id):
37 | timestamp = datetime.utcnow().isoformat()
38 | return {'timestamp': timestamp}
39 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation.
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
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Home |
5 | API Sample
6 |
7 |
8 |
9 |
10 | +
11 |
12 |
13 |
14 |
15 |
16 |
17 |

18 |
19 |
20 |
21 |
22 |
49 |
--------------------------------------------------------------------------------
/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/views/Api.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Backend Resources Demo
4 |
Click on the links below to fetch data from the Flask server
5 |
Fetch
6 |
Fetch Secure Resource
7 |
Results
8 |
9 | Server Timestamp: {{r.timestamp | formatTimestamp }}
10 |
11 |
{{error}}
12 |
13 |
14 |
15 |
48 |
49 |
51 |
--------------------------------------------------------------------------------
/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 | },
10 | "dependencies": {
11 | "axios": "^0.18.0",
12 | "vue": "^2.5.13",
13 | "vue-router": "^3.0.1",
14 | "vuex": "^3.0.1"
15 | },
16 | "devDependencies": {
17 | "@vue/cli-plugin-babel": "^3.0.0-beta.6",
18 | "@vue/cli-plugin-eslint": "^3.0.0-beta.6",
19 | "@vue/cli-service": "^3.0.0-beta.6",
20 | "@vue/eslint-config-standard": "^3.0.0-beta.6",
21 | "lint-staged": "^6.0.0",
22 | "node-sass": "^4.7.2",
23 | "sass-loader": "^6.0.6",
24 | "vue-template-compiler": "^2.5.13"
25 | },
26 | "babel": {
27 | "presets": [
28 | "@vue/app"
29 | ]
30 | },
31 | "eslintConfig": {
32 | "root": true,
33 | "extends": [
34 | "plugin:vue/essential",
35 | "@vue/standard"
36 | ]
37 | },
38 | "postcss": {
39 | "plugins": {
40 | "autoprefixer": {}
41 | }
42 | },
43 | "browserslist": [
44 | "> 1%",
45 | "last 2 versions",
46 | "not ie <= 8"
47 | ],
48 | "gitHooks": {
49 | "pre-commit": "lint-staged"
50 | },
51 | "lint-staged": {
52 | "*.js": [
53 | "vue-cli-service lint",
54 | "git add"
55 | ],
56 | "*.vue": [
57 | "vue-cli-service lint",
58 | "git add"
59 | ]
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ msg }}
4 |
5 | For guide and recipes on how to configure / customize this project,
6 | check out the
7 | vue-cli documentation.
8 |
9 |
Installed CLI Plugins
10 |
14 |
Essential Links
15 |
21 |
Ecosystem
22 |
29 |
30 |
31 |
32 |
40 |
41 |
42 |
58 |
--------------------------------------------------------------------------------
/.github/workflows/build_and_deploy.yaml:
--------------------------------------------------------------------------------
1 | # For more info on Python, GitHub Actions, and Azure App Service
2 | # please head to https://aka.ms/python-webapps-actions
3 |
4 | name: Build and deploy Flask + Vue.js app to Azure App Service
5 |
6 | on:
7 | push:
8 | branches:
9 | - main
10 |
11 | env:
12 | WEBAPP_NAME: '' # Replace with the name of your Azure web app
13 | RESOURCE_GROUP: '' # Replace with the name of your Resource Group
14 |
15 | jobs:
16 | build-and-test:
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - uses: actions/checkout@v2
21 |
22 | - name: Set up Python
23 | uses: actions/setup-python@v2
24 | with:
25 | python-version: 3.6
26 |
27 | - name: Set up Node.js
28 | uses: actions/setup-node@v1
29 | with:
30 | node-version: 12
31 |
32 | - name: Install and build Vue.js project
33 | run: |
34 | npm install
35 | npm run build
36 |
37 | - name: Create and start virtual environment
38 | run: |
39 | python3 -m venv venv
40 | source venv/bin/activate
41 |
42 | - name: Install dependencies
43 | run: pip install -r requirements.txt
44 |
45 | - name: test with PyTest
46 | run: pytest --cov=app --cov-report=xml
47 |
48 | - name: Upload artifact for deployment jobs
49 | uses: actions/upload-artifact@v2
50 | with:
51 | name: python-app
52 | path: |
53 | .
54 | !node_modules/
55 | !venv/
56 |
57 | deploy-to-webapp:
58 | needs: build-and-test
59 | runs-on: ubuntu-latest
60 |
61 | steps:
62 | - uses: actions/download-artifact@v2
63 | with:
64 | name: python-app
65 | path: .
66 |
67 | - name: Log in to Azure CLI
68 | uses: azure/login@v1
69 | with:
70 | creds: ${{ secrets.AZURE_SERVICE_PRINCIPAL }}
71 |
72 | - name: Configure deployment and runtime settings on the webapp
73 | run: |
74 | az configure --defaults ${{ env.RESOURCE_GROUP }}
75 | az webapp config appsettings --name ${{ env.WEBAPP_NAME }} --settings \
76 | SCM_DO_BUILD_DURING_DEPLOYMENT=true \
77 | FLASK_ENV=production
78 |
79 | az webapp config set --name ${{ env.WEBAPP_NAME }} \
80 | --startup-file "gunicorn --bind=0.0.0.0 --timeout 600 app:app"
81 |
82 | - name: Deploy to App Service
83 | uses: azure/webapps-deploy@v2
84 | with:
85 | app-name: ${{ env.WEBAPP_NAME}}
86 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | *.pyc
132 | .cache
133 | .mypy_cache\
134 | .env
135 | venv/
136 |
137 | /dist
138 | .coverage
139 | .pytest_cache/
140 | htmlcov/
141 |
142 | .DS_Store
143 | node_modules/
144 | npm-debug.log*
145 | yarn-debug.log*
146 | yarn-error.log*
147 | selenium-debug.log
148 | package-lock.json
149 |
150 | # Editor directories and files
151 | .idea
152 | .vscode
153 | *.suo
154 | *.ntvs*
155 | *.njsproj
156 | *.sln
157 | *security-sgps.py
158 | *security_backend.py
159 |
160 | node_modules/*
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to this Flask + Vue.js sample app
2 |
3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a
4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
6 |
7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide
8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
9 | provided by the bot. You will only need to do this once across all repos using our CLA.
10 |
11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
14 |
15 | - [Code of Conduct](#coc)
16 | - [Issues and Bugs](#issue)
17 | - [Feature Requests](#feature)
18 | - [Submission Guidelines](#submit)
19 |
20 | ## Code of Conduct
21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
22 |
23 | ## Found an Issue?
24 | If you find a bug in the source code or a mistake in the documentation, you can help us by
25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can
26 | [submit a Pull Request](#submit-pr) with a fix.
27 |
28 | ## Want a Feature?
29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub
30 | Repository. If you would like to *implement* a new feature, please submit an issue with
31 | a proposal for your work first, to be sure that we can use it.
32 |
33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr).
34 |
35 | ## Submission Guidelines
36 |
37 | ### Submitting an Issue
38 | Before you submit an issue, search the archive, maybe your question was already answered.
39 |
40 | If your issue appears to be a bug, and hasn't been reported, open a new issue.
41 | Help us to maximize the effort we can spend fixing issues and adding new
42 | features, by not reporting duplicate issues. Providing the following information will increase the
43 | chances of your issue being dealt with quickly:
44 |
45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps
46 | * **Version** - what version is affected (e.g. 0.1.2)
47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you
48 | * **Browsers and Operating System** - is this a problem with all browsers?
49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps
50 | * **Related Issues** - has a similar issue been reported before?
51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be
52 | causing the problem (line of code or commit)
53 |
54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new].
55 |
56 | ### Submitting a Pull Request (PR)
57 | Before you submit your Pull Request (PR) consider the following guidelines:
58 |
59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR
60 | that relates to your submission. You don't want to duplicate effort.
61 |
62 | * Make your changes in a new git fork:
63 |
64 | * Commit your changes using a descriptive commit message
65 | * Push your fork to GitHub:
66 | * In GitHub, create a pull request
67 | * If we suggest changes then:
68 | * Make the required updates.
69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request):
70 |
71 | ```shell
72 | git rebase master -i
73 | git push -f
74 | ```
75 |
76 | That's it! Thank you for your contribution!
77 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flask-VueJs-Template 🌶️✌
2 |
3 | > A Flask + Vue.js sample application adapted from [this repository](https://github.com/gtalarico/flask-vuejs-template).
4 |
5 |  
6 |
7 | ## Features
8 |
9 | * Minimal Flask 1.0 App
10 | * [Flask-RestPlus](http://flask-restplus.readthedocs.io) API with class-based secure resource routing
11 | * Starter [PyTest](http://pytest.org) test suite
12 | * [vue-cli 3](https://github.com/vuejs/vue-cli/blob/dev/docs/README.md) + yarn
13 | * [Vuex](https://vuex.vuejs.org/)
14 | * [Vue Router](https://router.vuejs.org/)
15 | * [Axios](https://github.com/axios/axios/) for backend communication
16 | * Sample Vue [Filters](https://vuejs.org/v2/guide/filters.html)
17 |
18 | ## Template Structure
19 |
20 | The template uses Flask & Flask-RestPlus to create a minimal REST style API,
21 | and let's VueJs + vue-cli handle the front end and asset pipline.
22 | Data from the python server to the Vue application is passed by making Ajax requests.
23 |
24 | ### Application Structure
25 |
26 | #### Rest Api
27 |
28 | The Api is served using a Flask blueprint at `/api/` using Flask RestPlus class-based
29 | resource routing.
30 |
31 | #### Client Application
32 |
33 | A Flask view is used to serve the `index.html` as an entry point into the Vue app at the endpoint `/`.
34 |
35 | The template uses vue-cli 3 and assumes Vue Cli & Webpack will manage front-end resources and assets, so it does overwrite template delimiter.
36 |
37 | The Vue instance is preconfigured with Filters, Vue-Router, Vuex; each of these can easilly removed if they are not desired.
38 |
39 | #### Important Files
40 |
41 | | Location | Content |
42 | |----------------------|--------------------------------------------|
43 | | `/app` | Flask Application |
44 | | `/app/api` | Flask Rest Api (`/api`) |
45 | | `/app/client.py` | Flask Client (`/`) |
46 | | `/src` | Vue App . |
47 | | `/src/main.js` | JS Application Entry Point |
48 | | `/public/index.html` | Html Application Entry Point (`/`) |
49 | | `/public/static` | Static Assets |
50 | | `/dist/` | Bundled Assets Output (generated at `yarn build` |
51 | | [`.github/workflows/build_and_deploy.yaml`](.github/workflows/build_and_deploy.yaml) | The deployment workflow |
52 |
53 | ## Installation
54 |
55 | ### Before you start
56 |
57 | Before getting started, you should have the following installed and running:
58 |
59 | - [X] NPM - [instructions](https://yarnpkg.com/en/docs/install#mac-stable)
60 | - [X] Vue Cli 3 - [instructions](https://cli.vuejs.org/guide/installation.html)
61 | - [X] Python 3
62 | - [X] Pipenv (optional)
63 |
64 | ### Get started
65 |
66 | * Clone this repository:
67 |
68 | ```bash
69 | git clone https://github.com/Azure-Samples/flask-vuejs-webapp.git
70 | ```
71 |
72 | * Setup virtual environment, install dependencies, and activate it:
73 |
74 | ```bash
75 | python -m venv venv
76 | source venv/bin/activate # or ".\venv\Scripts\activate" on Windows
77 | pip install -r requirements.txt
78 | ```
79 |
80 | * Install JS dependencies
81 |
82 | ```bash
83 | npm install
84 | ```
85 |
86 | ### Development Servers
87 |
88 | Run Flask API development server:
89 |
90 | ```bash
91 | python run.py
92 | ```
93 |
94 | From another tab in the same directory, start the webpack dev server:
95 |
96 | ```bash
97 | npm run serve
98 | ```
99 |
100 | The Vuejs application will be served from `localhost:8080` and the Flask API and static files will be served from `localhost:5000`. The dual dev-server setup allows you to take advantage of webpack's development server with hot module replacement. The proxy is configured in `vue.config.js` and is used to route the requests back to Flask's API on port 5000.
101 |
102 | If you would rather run a single dev server, you can run Flask's development server only on `:5000`, but you have to build build the Vue app first and the page will not reload on changes. Run the commands below to build the Vue.js app and serve it on Python's dev server.
103 |
104 | ```bash
105 | npm build
106 | python run.py
107 | ```
108 |
109 | ## Production Server
110 |
111 | This template is configured to be built with GitHub Actions and run on Azure App Service. You can take a look at the GitHub Actions [workflow file](.github/workflows/build_and_deploy.yaml) to see the build and deployment steps.
112 |
113 | ### JS Build Process
114 |
115 | The Vue.js application is built and minified to the `dist/` folder. Once the application is deployed to App Service, Flask serves the static content via the Flask Blueprint in [`client.py`](app/client.py).
116 |
117 | ### Python Build Process
118 |
119 | The GitHub Actions workflow builds and tests the Vue.js and Flask applications before deploying. If the tests pass successfully, the Flask app is deployed to the web app along with the minified Vue.js. Once deployed, the App Service build pipeline will re-install the Python dependencies so they are correctly installed for the runtime image.
120 |
121 | ### Production Sever Setup
122 |
123 | The second job of the deployment workflow uses the Azure CLI to set some configuration settings on the web app. These are outlined below.
124 |
125 | - `SCM_DO_BUILD_DURING_DEPLOYMENT`: When set to true, the app setting will force the SCM site to re-build the Flask application. This ensures that the Python libraries are installed properly for the runtime OS.
126 | - `FLASK_ENV`: This app setting tells Flask which configuration to use.
127 | - `startup-script`: This setting tells App Service how to initialize the application. In our case, we use gunicorn to serve the "app" object in [`app.py`](app/__init__.py). For this particular application, set the startup script to `gunicorn --bind=0.0.0.0 --timeout 600 app:app`.
128 |
--------------------------------------------------------------------------------
/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "86fcf75e89c8bcab95ada34b3d66e96a8fbb94949d95bf087307aa2acbd42312"
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 | "aniso8601": {
20 | "hashes": [
21 | "sha256:547e7bc88c19742e519fb4ca39f4b8113fdfb8fca322e325f16a8bfc6cfc553c",
22 | "sha256:e7560de91bf00baa712b2550a2fdebf0188c5fce2fcd1162fbac75c19bb29c95"
23 | ],
24 | "version": "==4.0.1"
25 | },
26 | "click": {
27 | "hashes": [
28 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
29 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
30 | ],
31 | "version": "==7.0"
32 | },
33 | "flask": {
34 | "hashes": [
35 | "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
36 | "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
37 | ],
38 | "index": "pypi",
39 | "version": "==1.0.2"
40 | },
41 | "flask-restplus": {
42 | "hashes": [
43 | "sha256:3fad697e1d91dfc13c078abcb86003f438a751c5a4ff41b84c9050199d2eab62",
44 | "sha256:cdc27b5be63f12968a7f762eaa355e68228b0c904b4c96040a314ba7dc6d0e69"
45 | ],
46 | "index": "pypi",
47 | "version": "==0.12.1"
48 | },
49 | "gunicorn": {
50 | "hashes": [
51 | "sha256:75af03c99389535f218cc596c7de74df4763803f7b63eb09d77e92b3956b36c6",
52 | "sha256:eee1169f0ca667be05db3351a0960765620dad53f53434262ff8901b68a1b622"
53 | ],
54 | "index": "pypi",
55 | "version": "==19.7.1"
56 | },
57 | "itsdangerous": {
58 | "hashes": [
59 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
60 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
61 | ],
62 | "version": "==1.1.0"
63 | },
64 | "jinja2": {
65 | "hashes": [
66 | "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
67 | "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
68 | ],
69 | "version": "==2.10"
70 | },
71 | "jsonschema": {
72 | "hashes": [
73 | "sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08",
74 | "sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02"
75 | ],
76 | "version": "==2.6.0"
77 | },
78 | "markupsafe": {
79 | "hashes": [
80 | "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
81 | "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
82 | "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
83 | "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
84 | "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
85 | "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
86 | "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
87 | "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
88 | "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
89 | "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
90 | "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
91 | "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
92 | "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
93 | "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
94 | "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
95 | "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
96 | "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
97 | "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
98 | "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
99 | "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
100 | "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
101 | "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
102 | "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
103 | "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
104 | "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
105 | "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
106 | "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
107 | "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
108 | ],
109 | "version": "==1.1.0"
110 | },
111 | "python-dotenv": {
112 | "hashes": [
113 | "sha256:122290a38ece9fe4f162dc7c95cae3357b983505830a154d3c98ef7f6c6cea77",
114 | "sha256:4a205787bc829233de2a823aa328e44fd9996fedb954989a21f1fc67c13d7a77"
115 | ],
116 | "index": "pypi",
117 | "version": "==0.9.1"
118 | },
119 | "pytz": {
120 | "hashes": [
121 | "sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca",
122 | "sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6"
123 | ],
124 | "version": "==2018.7"
125 | },
126 | "six": {
127 | "hashes": [
128 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
129 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
130 | ],
131 | "version": "==1.11.0"
132 | },
133 | "werkzeug": {
134 | "hashes": [
135 | "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
136 | "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
137 | ],
138 | "version": "==0.14.1"
139 | }
140 | },
141 | "develop": {
142 | "atomicwrites": {
143 | "hashes": [
144 | "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
145 | "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
146 | ],
147 | "version": "==1.2.1"
148 | },
149 | "attrs": {
150 | "hashes": [
151 | "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
152 | "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
153 | ],
154 | "version": "==18.2.0"
155 | },
156 | "bumpversion": {
157 | "hashes": [
158 | "sha256:6744c873dd7aafc24453d8b6a1a0d6d109faf63cd0cd19cb78fd46e74932c77e",
159 | "sha256:6753d9ff3552013e2130f7bc03c1007e24473b4835952679653fb132367bdd57"
160 | ],
161 | "index": "pypi",
162 | "version": "==0.5.3"
163 | },
164 | "coverage": {
165 | "hashes": [
166 | "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f",
167 | "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe",
168 | "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d",
169 | "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0",
170 | "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607",
171 | "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d",
172 | "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b",
173 | "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3",
174 | "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e",
175 | "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815",
176 | "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36",
177 | "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1",
178 | "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14",
179 | "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c",
180 | "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794",
181 | "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b",
182 | "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840",
183 | "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd",
184 | "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82",
185 | "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952",
186 | "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389",
187 | "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f",
188 | "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4",
189 | "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da",
190 | "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647",
191 | "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d",
192 | "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42",
193 | "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478",
194 | "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b",
195 | "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb",
196 | "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9"
197 | ],
198 | "version": "==4.5.2"
199 | },
200 | "more-itertools": {
201 | "hashes": [
202 | "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092",
203 | "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e",
204 | "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"
205 | ],
206 | "version": "==4.3.0"
207 | },
208 | "packaging": {
209 | "hashes": [
210 | "sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807",
211 | "sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9"
212 | ],
213 | "version": "==18.0"
214 | },
215 | "pluggy": {
216 | "hashes": [
217 | "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095",
218 | "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f"
219 | ],
220 | "version": "==0.8.0"
221 | },
222 | "py": {
223 | "hashes": [
224 | "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
225 | "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
226 | ],
227 | "version": "==1.7.0"
228 | },
229 | "pyparsing": {
230 | "hashes": [
231 | "sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b",
232 | "sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592"
233 | ],
234 | "version": "==2.3.0"
235 | },
236 | "pytest": {
237 | "hashes": [
238 | "sha256:3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec",
239 | "sha256:e246cf173c01169b9617fc07264b7b1316e78d7a650055235d6d897bc80d9660"
240 | ],
241 | "index": "pypi",
242 | "version": "==3.10.1"
243 | },
244 | "pytest-cov": {
245 | "hashes": [
246 | "sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7",
247 | "sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762"
248 | ],
249 | "index": "pypi",
250 | "version": "==2.6.0"
251 | },
252 | "pytest-sugar": {
253 | "hashes": [
254 | "sha256:26cf8289fe10880cbbc130bd77398c4e6a8b936d8393b116a5c16121d95ab283",
255 | "sha256:fcd87a74b2bce5386d244b49ad60549bfbc4602527797fac167da147983f58ab"
256 | ],
257 | "index": "pypi",
258 | "version": "==0.9.2"
259 | },
260 | "six": {
261 | "hashes": [
262 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
263 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
264 | ],
265 | "version": "==1.11.0"
266 | },
267 | "termcolor": {
268 | "hashes": [
269 | "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"
270 | ],
271 | "version": "==1.1.0"
272 | }
273 | }
274 | }
275 |
--------------------------------------------------------------------------------