From 09dc83ff44384f2f5e69133550bfb8527b1a0ffd Mon Sep 17 00:00:00 2001 From: davidgonzalezs <–dmgs16@hotmail.com> Date: Wed, 11 Dec 2019 18:37:03 +0000 Subject: [PATCH 1/5] Backend Testing script --- backend/TestingBackendD/backendTesting.py | 121 ++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 backend/TestingBackendD/backendTesting.py diff --git a/backend/TestingBackendD/backendTesting.py b/backend/TestingBackendD/backendTesting.py new file mode 100644 index 0000000..6d981ec --- /dev/null +++ b/backend/TestingBackendD/backendTesting.py @@ -0,0 +1,121 @@ +import requests +import json + +def test(json): + headers = {'Content-type': 'application/json', 'Accept': 'application/json'} + r = requests.post("http://34.89.126.252/getHouses", data = json, headers = headers) + return r + +def testCaseCorrectParams(): + json_dicc = { "lat":50.82838, "lon":-0.13947, "limit":4, "radius":2000} + json_data = json.dumps(json_dicc) + r = test(json_data) + json_data = r.json() + if (r.status_code != 200): + return -1 + else: + return 1 + +def testCaseCorrectParams2(): + json_dicc = { "lat":50.82838, "lon":-0.13947, "limit":6, "radius":4000} + json_data = json.dumps(json_dicc) + r = test(json_data) + json_data = r.json() + if (r.status_code != 200): + return -1 + else: + return 1 + +def testCaseLatOut(): + json_dicc = { "lat":50.0, "lon":-0.13947, "limit":4, "radius":2000} + json_data = json.dumps(json_dicc) + r = test(json_data) + if (r.status_code != 200): + return -1 + else: + json_data = r.json() + if("error" in dict.keys(json_data)): + return 1 + else: + return -1 + +def testCaseLonOut(): + json_dicc = { "lat":50.82838, "lon":0.081089, "limit":4, "radius":2000} + json_data = json.dumps(json_dicc) + r = test(json_data) + if (r.status_code != 200): + return -1 + else: + json_data = r.json() + if("error" in dict.keys(json_data)): + return 1 + else: + return -1 + + +def testCaseSmallRadius(): + json_dicc = { "lat":50.82838, "lon":-0.13947, "limit":4, "radius":2} + json_data = json.dumps(json_dicc) + r = test(json_data) + if (r.status_code != 200): + return -1 + else: + json_data = r.json() + if("error" in dict.keys(json_data)): + return 1 + else: + return -1 + +def testCaseImpossibleLat(): + json_dicc = { "lat":91.0, "lon":-0.13947, "limit":4, "radius":2000} + json_data = json.dumps(json_dicc) + r = test(json_data) + if (r.status_code != 200): + return -1 + else: + json_data = r.json() + if("error" in dict.keys(json_data)): + return 1 + else: + return -1 + +def testCaseImpossibleLon(): + json_dicc = { "lat":50.82838, "lon":-190.5, "limit":4, "radius":2000} + json_data = json.dumps(json_dicc) + r = test(json_data) + if (r.status_code != 200): + return -1 + else: + json_data = r.json() + if("error" in dict.keys(json_data)): + return 1 + else: + return -1 + +def testCaseChangedParameters(): + json_dicc = { "lat":2000 , "lon":-0.13947, "limit":50.82838, "radius":4} + json_data = json.dumps(json_dicc) + r = test(json_data) + if (r.status_code != 200): + return -1 + else: + json_data = r.json() + if("error" in dict.keys(json_data)): + return 1 + else: + return -1 + +def testCasesCall(): + testReport = [] + testReport.append(testCaseCorrectParams()) + testReport.append(testCaseCorrectParams2()) + testReport.append(testCaseLatOut()) + testReport.append(testCaseLonOut()) + testReport.append(testCaseSmallRadius()) + testReport.append(testCaseImpossibleLat()) + testReport.append(testCaseImpossibleLon()) + testReport.append(testCaseChangedParameters()) + + +if __name__ == "__main__": + testCasesCall() From 3c07f133747e96f63d78875d0f9eac786ba40955 Mon Sep 17 00:00:00 2001 From: Mark Said Camilleri Date: Thu, 12 Dec 2019 15:05:48 +0000 Subject: [PATCH 2/5] Made use of the unittest framework for assertions (to run this as a test suite after deploy) --- backend/TestingBackendD/backendTesting.py | 121 ---------------------- backend/test/backendTesting.py | 61 +++++++++++ 2 files changed, 61 insertions(+), 121 deletions(-) delete mode 100644 backend/TestingBackendD/backendTesting.py create mode 100644 backend/test/backendTesting.py diff --git a/backend/TestingBackendD/backendTesting.py b/backend/TestingBackendD/backendTesting.py deleted file mode 100644 index 6d981ec..0000000 --- a/backend/TestingBackendD/backendTesting.py +++ /dev/null @@ -1,121 +0,0 @@ -import requests -import json - -def test(json): - headers = {'Content-type': 'application/json', 'Accept': 'application/json'} - r = requests.post("http://34.89.126.252/getHouses", data = json, headers = headers) - return r - -def testCaseCorrectParams(): - json_dicc = { "lat":50.82838, "lon":-0.13947, "limit":4, "radius":2000} - json_data = json.dumps(json_dicc) - r = test(json_data) - json_data = r.json() - if (r.status_code != 200): - return -1 - else: - return 1 - -def testCaseCorrectParams2(): - json_dicc = { "lat":50.82838, "lon":-0.13947, "limit":6, "radius":4000} - json_data = json.dumps(json_dicc) - r = test(json_data) - json_data = r.json() - if (r.status_code != 200): - return -1 - else: - return 1 - -def testCaseLatOut(): - json_dicc = { "lat":50.0, "lon":-0.13947, "limit":4, "radius":2000} - json_data = json.dumps(json_dicc) - r = test(json_data) - if (r.status_code != 200): - return -1 - else: - json_data = r.json() - if("error" in dict.keys(json_data)): - return 1 - else: - return -1 - -def testCaseLonOut(): - json_dicc = { "lat":50.82838, "lon":0.081089, "limit":4, "radius":2000} - json_data = json.dumps(json_dicc) - r = test(json_data) - if (r.status_code != 200): - return -1 - else: - json_data = r.json() - if("error" in dict.keys(json_data)): - return 1 - else: - return -1 - - -def testCaseSmallRadius(): - json_dicc = { "lat":50.82838, "lon":-0.13947, "limit":4, "radius":2} - json_data = json.dumps(json_dicc) - r = test(json_data) - if (r.status_code != 200): - return -1 - else: - json_data = r.json() - if("error" in dict.keys(json_data)): - return 1 - else: - return -1 - -def testCaseImpossibleLat(): - json_dicc = { "lat":91.0, "lon":-0.13947, "limit":4, "radius":2000} - json_data = json.dumps(json_dicc) - r = test(json_data) - if (r.status_code != 200): - return -1 - else: - json_data = r.json() - if("error" in dict.keys(json_data)): - return 1 - else: - return -1 - -def testCaseImpossibleLon(): - json_dicc = { "lat":50.82838, "lon":-190.5, "limit":4, "radius":2000} - json_data = json.dumps(json_dicc) - r = test(json_data) - if (r.status_code != 200): - return -1 - else: - json_data = r.json() - if("error" in dict.keys(json_data)): - return 1 - else: - return -1 - -def testCaseChangedParameters(): - json_dicc = { "lat":2000 , "lon":-0.13947, "limit":50.82838, "radius":4} - json_data = json.dumps(json_dicc) - r = test(json_data) - if (r.status_code != 200): - return -1 - else: - json_data = r.json() - if("error" in dict.keys(json_data)): - return 1 - else: - return -1 - -def testCasesCall(): - testReport = [] - testReport.append(testCaseCorrectParams()) - testReport.append(testCaseCorrectParams2()) - testReport.append(testCaseLatOut()) - testReport.append(testCaseLonOut()) - testReport.append(testCaseSmallRadius()) - testReport.append(testCaseImpossibleLat()) - testReport.append(testCaseImpossibleLon()) - testReport.append(testCaseChangedParameters()) - - -if __name__ == "__main__": - testCasesCall() diff --git a/backend/test/backendTesting.py b/backend/test/backendTesting.py new file mode 100644 index 0000000..1a32b10 --- /dev/null +++ b/backend/test/backendTesting.py @@ -0,0 +1,61 @@ +import requests +import json +import unittest + + +class TestRunningBackend(unittest.TestCase): + @staticmethod + def get_request(json): + headers = {'Content-type': 'application/json', 'Accept': 'application/json'} + r = requests.post("http://34.89.126.252/getHouses", data=json, headers=headers) + return r + + def testCaseCorrectParams(self): + json_dicc = {"lat": 50.82838, "lon": -0.13947, "limit": 4, "radius": 2000} + json_data = json.dumps(json_dicc) + r = TestRunningBackend.get_request(json_data) + self.assertEquals(r.status_code, 200) + + def testCaseCorrectParams2(self): + json_dicc = {"lat": 50.82838, "lon": -0.13947, "limit": 6, "radius": 4000} + json_data = json.dumps(json_dicc) + r = TestRunningBackend.get_request(json_data) + self.assertEquals(r.status_code, 200) + + def testCaseLatOut(self): + json_dicc = {"lat": 50.0, "lon": -0.13947, "limit": 4, "radius": 2000} + json_data = json.dumps(json_dicc) + r = TestRunningBackend.get_request(json_data) + self.assertTrue(r.status_code == 200 or "error" in r.json().keys()) + + def testCaseLonOut(self): + json_dicc = {"lat": 50.82838, "lon": 0.081089, "limit": 4, "radius": 2000} + json_data = json.dumps(json_dicc) + r = TestRunningBackend.get_request(json_data) + self.assertTrue(r.status_code == 200 or "error" in r.json().keys()) + + def testCaseSmallRadius(self): + json_dicc = {"lat": 50.82838, "lon": -0.13947, "limit": 4, "radius": 2} + json_data = json.dumps(json_dicc) + r = TestRunningBackend.get_request(json_data) + self.assertTrue(r.status_code == 200 or "error" in r.json().keys()) + + + def testCaseImpossibleLat(self): + json_dicc = {"lat": 91.0, "lon": -0.13947, "limit": 4, "radius": 2000} + json_data = json.dumps(json_dicc) + r = TestRunningBackend.get_request(json_data) + self.assertTrue(r.status_code == 200 or "error" in r.json().keys()) + + def testCaseImpossibleLon(self): + json_dicc = {"lat": 50.82838, "lon": -190.5, "limit": 4, "radius": 2000} + json_data = json.dumps(json_dicc) + r = TestRunningBackend.get_request(json_data) + self.assertTrue(r.status_code == 200 or "error" in r.json().keys()) + + def testCaseChangedParameters(self): + json_dicc = {"lat": 2000, "lon": -0.13947, "limit": 50.82838, "radius": 4} + json_data = json.dumps(json_dicc) + r = TestRunningBackend.get_request(json_data) + print(r) + self.assertTrue(r.status_code == 200 or "error" in r.json().keys()) From c954cba3b6fca830f289980b6aa65291b8ee3b01 Mon Sep 17 00:00:00 2001 From: Mark Said Camilleri Date: Thu, 12 Dec 2019 15:42:35 +0000 Subject: [PATCH 3/5] Added some tests and requirements doc --- .travis.yml | 19 +++- backend/__init__.py | 0 backend/requirements.txt | 20 ++++ backend/src/app.py | 174 ++++++++++++++++++++++++++++++++++ backend/src/postcodeIO.py | 55 ----------- backend/test/__init__.py | 0 backend/test/test_SQLQueue.py | 34 +++++++ 7 files changed, 244 insertions(+), 58 deletions(-) create mode 100644 backend/__init__.py create mode 100644 backend/requirements.txt create mode 100644 backend/src/app.py delete mode 100644 backend/src/postcodeIO.py create mode 100644 backend/test/__init__.py create mode 100644 backend/test/test_SQLQueue.py diff --git a/.travis.yml b/.travis.yml index 2da722c..72c9e94 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ cache: before_install: - npm install -g npm@latest - npm install -g expo-cli + stages: - build - test @@ -21,10 +22,22 @@ stages: jobs: include: - stage: test + before_install: + - cd ./backend + - pip install -r requirements.txt + - pip install pytest + - pip install pytest-timeout + - pip install codecov script: - - cd frontend - - npm ci - - npx jest --ci --passWithNoTests + - cd frontend + - npm ci + - npx jest --ci --passWithNoTests + - cd ../backend + - python -m pytest --timeout 600 test/test_SQLQueue.py + after_success: + - codecov # submit coverage + + # - stage: deploy staging # script: # - cd frontend diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..ba963c7 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,20 @@ +certifi==2019.11.28 +chardet==3.0.4 +Click==7.0 +coverage==4.5.4 +deprecation==2.0.7 +Flask==1.1.1 +idna==2.8 +itsdangerous==1.1.0 +janus==0.4.0 +Jinja2==2.10.3 +MarkupSafe==1.1.1 +mock==3.0.5 +mysql-connector==2.2.9 +packaging==19.2 +postcodes-io-api==0.0.4 +pyparsing==2.4.5 +requests==2.22.0 +six==1.13.0 +urllib3==1.25.7 +Werkzeug==0.16.0 diff --git a/backend/src/app.py b/backend/src/app.py new file mode 100644 index 0000000..167aef2 --- /dev/null +++ b/backend/src/app.py @@ -0,0 +1,174 @@ +from flask import Flask, request, jsonify +import postcodes_io_api +import database +import hashlib +import mysql.connector +import secrets + +#gpsDatabase = mysql.connector.connect( # connect to db +# host="34.89.126.252", # won't change +# user="root", # username for rwx access +# password="ASE@group.2=100%", +# database="price_paid_data" # schema name +#) + +#dbex = gpsDatabase.cursor() + + +app = Flask(__name__) + +@app.route('/getHouses', methods=['POST']) +def postcodesIO(): + frontData = request.get_json() + latitude = frontData['lat'] + longitude = frontData['lon'] + radius = frontData['radius'] + houseLimit = frontData['limit'] + listOfPostcodes = callAPI(latitude, longitude, houseLimit, radius) + # houseLimit used here to limit len(listOfPostcodes) amount of values in the SQL WHERE clause. + varseq = ','.join(['%s']*len(listOfPostcodes)) + + statement = ( + f"""SELECT post.id, house.paon, house.saon, post.street, post.postcode, props.initial, trans.price + FROM postcodes AS post + INNER JOIN houses AS house + ON house.postcode_id = post.id + INNER JOIN transactions AS trans + ON trans.house_id = house.id + INNER JOIN property_types AS props + ON props.id = house.property_type_id + WHERE post.postcode IN ({varseq});""" + ) + + print(statement) + print(str(listOfPostcodes)) + + result = db.select(query=statement, parameters=listOfPostcodes) + # --py-pseudo: + # for postcode in listOfPostcodes (not necessarily a for loop) + # --sql-pseudo: + # SELECT (postcode, id, street, county) + # FROM postcodes WHERE postcode IN str(tuple(listOfPostcodes)) + # SELECT (paon, saon) FROM houses WHERE houses.id = postcodes.id (JOIN. DOUBLE CAUTION.) + # SELECT price FROM transactions WHERE transactions.id = houses.id (DOUBLE JOIN. TRIPLE CAUTION.) + # SELECT initial FROM property_types WHERE property_types.id = houses.id (If you don't get it now..) + + print(str(result)) + + return jsonify(result) + + +def callAPI(lat, lon, lim, rad): + api = postcodes_io_api.Api(debug_http=True) + listPostcodes = api.get_nearest_postcodes_for_coordinates( + latitude=lat, longitude=lon, limit=lim, radius=rad) + onlyPostcodes = [] + for i in range(len(listPostcodes["result"])): + print(str(i)) + onlyPostcodes.append(listPostcodes["result"][i]["postcode"]) + return onlyPostcodes + + +# this function is used when creating a new user so a salt can be made +def passwordHash(password): + salt = secrets.token_hex(16) + saltedPass = password + salt + n = hashlib.sha256() + n.update(str.encode(saltedPass)) + hash2 = n.hexdigest() + return {'hash2': hash2, 'salt': salt} + + +def passwordCheckHash(password, salt): # this function is used when checking hash2 + m = hashlib.sha256() + saltedPass = password + salt + m.update(str.encode(saltedPass)) + hash2 = m.hexdigest() + return hash2 + + +def getSalt(username): # get the salt of a password from database + command = "SELECT salt FROM users WHERE username = %s" + result = db.select(query=command, parameters=(username,)) + if not result: + return "EMPTY LIST" + else: + return result[0]['salt'] # TODO use dict cursor or SQLQueue and refer from there + + +# This function should be used to check if a username has been taken or not on signup +def usernameExists(username): + command = "SELECT * FROM users WHERE username = %s" + result = db.select(query=command, parameters=(username,)) + print(result) + if len(result): + return True # username has been taken + else: + return False # username has't been taken + + +def checkLogin(username, password): # this checks for login details in table + command = "SELECT username, hash2 FROM users WHERE username = %s AND hash2 = %s" + result = db.select(query=command, parameters=[username, password]) + if len(result): # username and/or hash2 are correct as found in table + return True + else: # username and/or hash2 are incorrect as not found in table + return False + + +def addNewUser(username, hash2, salt): # adds a new user into the table + command = "INSERT INTO users (username, hash2, salt) VALUES (%s, %s, %s)" + db.execute(query=command, parameters=(username, hash2, salt)) + # newCommand = f"INSERT INTO users (username, hash2, salt) VALUES ('{username}', '{hash2}', '{salt}')" + # dbex.execute(newCommand) + # gpsDatabase.commit() + # command2 = "SELECT username, hash2, salt FROM users WHERE username = %s AND hash2 = %s AND salt = %s" + # result = db.select(query=command2, parameters=(username, hash2, salt)) + # if(len(result)): + # return True + # else: + # return False + + +@app.route('/login', methods=['POST']) +def login(): + data = request.get_json() + username = data['username'] + hash1 = data['hashedPassword'] + salt = getSalt(username) + hash2 = passwordCheckHash(hash1, salt) + if checkLogin(username, hash2): + # user proceeds to next screen as login details are correct + res = jsonify(response="True") # {'response': 'True'} + return res # login successful + else: + # show that the user has used incorrect details and needs to try again + res = jsonify(response="False") # {'response': 'False'} + return res # notification needed saying incorrect login details + + +@app.route('/signup', methods=['POST']) +def signup(): + data = request.get_json() + username = data['username'] + hash1 = data['hashedPassword'] + if usernameExists(username): + # this block shows that the username already exists in the database and the user needs a different one + # ' {'response': 'True'}' # notification needed saying try another username + res = jsonify(response="True") + return res + else: + # this block shows the username hasn't been taken and the new details are being added into the database + hashDict = passwordHash(hash1) + hash2 = hashDict.get('hash2') + salt = hashDict.get('salt') + addNewUser(username, hash2, salt) + # '{'response': 'False'}' # notification needed saying account made + res = jsonify(response="False") + return res + + +if __name__ == '__main__': + db = database.SQLQueue.get_instance( + host="34.89.126.252", user="root", password={change}, database="price_paid_data") + app.run(host='0.0.0.0', port=80) \ No newline at end of file diff --git a/backend/src/postcodeIO.py b/backend/src/postcodeIO.py deleted file mode 100644 index 1aa3ee6..0000000 --- a/backend/src/postcodeIO.py +++ /dev/null @@ -1,55 +0,0 @@ -import logging.config - -import postcodes_io_api -from flask import Flask, request, jsonify - -import database - -logging.config.fileConfig("../conf/logger.conf") -logger = logging.getLogger("root") - -app = Flask(__name__) - - -@app.route('/getHouses', methods=['POST']) -def postcodesIO(): - frontData = request.get_json() - latitude = frontData['lat'] - longitude = frontData['lon'] - radius = frontData['radius'] - houseLimit = frontData['limit'] - listOfPostcodes = callAPI(latitude, longitude, houseLimit, radius) - # houseLimit used here to limit len(listOfPostcodes) amount of values in the SQL WHERE clause. - varseq = ','.join(['%s']*len(listOfPostcodes)) - - statement = ( - f"""SELECT post.id, house.paon, house.saon, post.street, post.postcode, props.initial, trans.price - FROM postcodes AS post - INNER JOIN houses AS house - ON house.postcode_id = post.id - INNER JOIN transactions AS trans - ON trans.house_id = house.id - INNER JOIN property_types AS props - ON props.id = house.property_type_id - WHERE post.postcode IN ({varseq});""" - ) - - result = db.select(query=statement, parameters=listOfPostcodes) - - return jsonify(result) - - -def callAPI(lat, lon, rad, lim): - api = postcodes_io_api.Api(debug_http=True) - listPostcodes = api.get_nearest_postcodes_for_coordinates( - latitude=lat, longitude=lon, limit=lim, radius=rad) - onlyPostcodes = [] - for i in range(len(listPostcodes["result"])): - onlyPostcodes.append(listPostcodes["result"][i]["postcode"]) - return onlyPostcodes - - -if __name__ == '__main__': - db = database.SQLQueue.get_instance( - host="34.89.126.252", user="root", password={change on develop}, database="price_paid_data") - app.run(host='0.0.0.0', port=80) diff --git a/backend/test/__init__.py b/backend/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/test/test_SQLQueue.py b/backend/test/test_SQLQueue.py new file mode 100644 index 0000000..a8d3c8a --- /dev/null +++ b/backend/test/test_SQLQueue.py @@ -0,0 +1,34 @@ +from unittest import TestCase, mock + +from backend.src.database import SQLQueue +from backend.src.exceptions import SingletonException + + +class TestSQLQueue(TestCase): + # @mock.patch('mysql.connector') + # def test_constructor_raises_exception(self, mock_connection): + # sql_queue = SQLQueue(host='localhost') + # self.assertRaises(SingletonException, SQLQueue, host="localhost") + # del sql_queue + + @mock.patch('mysql.connector') + def test_get_instance_first_time_gets_new_instance(self, mock_connection): + sql_queue = SQLQueue.get_instance(host="localhost", user="root", password="root") + self.assertIsInstance(sql_queue, SQLQueue) + del sql_queue + + @mock.patch('mysql.connector') + def test_get_instance_second_time_same_gets_same_instance(self, mock_connection): + sql_queue_1 = SQLQueue.get_instance(host="localhost", user="root", password="root") + sql_queue_2 = SQLQueue.get_instance(host="localhost", user="root", password="root") + self.assertEqual(sql_queue_1, sql_queue_2) + del sql_queue_1 + del sql_queue_2 + + @mock.patch('mysql.connector') + def test_get_instance_second_time_different_gets_new_instance(self, mock_connection): + sql_queue_1 = SQLQueue.get_instance(host="localhost", user="root", password="root") + sql_queue_2 = SQLQueue.get_instance(host="localhost", user="test", password="root") + self.assertNotEqual(sql_queue_1, sql_queue_2) + del sql_queue_1 + del sql_queue_2 From f89e5dc670499051a06ff1ebcbba9864a2df598e Mon Sep 17 00:00:00 2001 From: Mark Said Camilleri Date: Thu, 12 Dec 2019 15:45:22 +0000 Subject: [PATCH 4/5] Updated to python 3 --- .travis.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 72c9e94..5b20324 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,18 +24,19 @@ jobs: - stage: test before_install: - cd ./backend - - pip install -r requirements.txt - - pip install pytest - - pip install pytest-timeout - - pip install codecov + - pip3 install --upgrade pip + - pip3 install -r requirements.txt + - pip3 install pytest + - pip3 install pytest-timeout + - pip3 install codecov script: - cd frontend - npm ci - npx jest --ci --passWithNoTests - cd ../backend - - python -m pytest --timeout 600 test/test_SQLQueue.py + - python3 -m pytest --timeout 600 test/test_SQLQueue.py after_success: - - codecov # submit coverage + - codecov # submit coverage # - stage: deploy staging From 327534cacc61f867482a5618387a74027abce42f Mon Sep 17 00:00:00 2001 From: Mark Said Camilleri Date: Thu, 12 Dec 2019 15:47:41 +0000 Subject: [PATCH 5/5] Specified Python --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5b20324..9d1c2af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: node_js node_js: -- 10.17.0 + - 10.17.0 +python: + - 3.6.7 env: matrix: secure: gSb+5hzjOEsBsrJRIb8x2pQF/Y/yB4b1d1OGW2BkXCLECwFfk/UCCpE1LIWLeK2XJpuF9H/yewZFc2lrgYsM1NI1Wn33BcOFWuvCq3uyLFlpdiB5jKNXvLYA6xmudwbvWPh9AjubN2IJplatF2EAMyf1JjgOX+M3lEWOGqiaOYMLjP+8PphInhbscMDWJb2ni601Q+iBDtEkhwLPWVIzz0gY3EyXta0YbtbiWvjhsFE9NYf2MuB/s9xb3UK09jq2bBtD4M80+ppzYUTmq8s3GzTPqpPm0vww67xkMUPA4tkgygFNwYYETeytLL9bdyDY8PitJfGn26qoZTPSy6z2TOJEmEVhf077Tk25FrJcIjFltQ3Nne/NYczTwFqUrrXNoLPgplm4zMy3LZZKweL11juMmbXmyTZ3fywGqJ8RwKPEKO3Qeuv0xtsPiAc1qmh6bC5Y7E65G5rDj77ei+7BFc12nSjqBCa0rLx1iC47fzeGTwWKCIb6A6tJbr6mRVkRbcP7M9rQ/UMWYskrFLgGbvrgHX0Hn28uHS76CNBAwvh0eQk3iG2iOIZ9GVC32Lh66daLL9noDhTiDGUdmp6q7ofG8jtNDPvnqF6CXgUBgpE8GGDd6paTmHT4rIiKLUI42sQwOzfIhwt7exckspqyZvvZbnkYg/Yk7bR8vTHLZ0M=