update dependencies, rejigged admin script, better structured ci/cd

This commit is contained in:
andy 2021-02-16 22:51:13 +00:00
parent d8a52437fd
commit 7dfc2ec0e1
5 changed files with 831 additions and 731 deletions

View File

@ -16,7 +16,7 @@ jobs:
matrix:
python-version: [3.8]
poetry-version: [1.1.4]
# node: [ '10', '12', '14' ]
node: [14]
os: [ubuntu-20.04, windows-latest]
runs-on: ${{ matrix.os }}
steps:
@ -51,15 +51,19 @@ jobs:
run: poetry run python -m unittest discover -s tests
# JS setup for testing
# - name: Install Node ${{ matrix.node }}
# uses: actions/setup-node@v2
# with:
# node-version: ${{ matrix.node }}
- name: Install Node ${{ matrix.node }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
# JS install from lock.json
# - name: Install Node Packages
# run: npm ci
- name: Install Node Packages
run: npm ci
# JS build for checking errors
- name: Compile Front-end
run: npm run build --if-present
# JS tests
# - name: Run JavaScript Tests
# run: npm test
@ -81,6 +85,10 @@ jobs:
uses: abatilo/actions-poetry@v2.1.0
with:
poetry-version: 1.1.4
# PYTHON Export Poetry dependencies as requirements.txt
- name: Export Poetry Dependencies
run: python admin.py pydepend
# JS setup
- name: Install Node 14
@ -93,7 +101,7 @@ jobs:
run: npm ci
# JS will be built again, for flagging errors
- name: Compile front-end
- name: Compile Front-end
run: npm run build --if-present
# JS for compiling scss
@ -111,10 +119,34 @@ jobs:
service_account_key: ${{ secrets.GCP_SA_KEY }}
export_default_credentials: true
# DEPLOY for deploying app engine API and main functions
- name: Deploy API & Main Functions
run: python admin.py all
# DEPLOY set project
- name: Set GCP Project
run: python admin.py set_project
# DEPLOY for deploying cron functions
- name: Deploy Cron Functions
run: python admin.py all_cron
# DEPLOY app engine service, -nb for skipping compile
- name: Deploy App Engine Service
run: python admin.py app -nb
### MAIN FUNCTIONS
# DEPLOY update_tag function
- name: Deploy update_tag Function
run: python admin.py tag
# DEPLOY run_user_playlist function
- name: Deploy run_user_playlist Function
run: python admin.py playlist
### CRON FUNCTIONS
# DEPLOY run_all_playlists function
- name: Deploy run_all_playlists Function
run: python admin.py playlist_cron
# DEPLOY run_all_playlist_stats function
- name: Deploy run_all_playlist_stats Function
run: python admin.py playlist_stats_cron
# DEPLOY run_all_tags function
- name: Deploy run_all_tags Function
run: python admin.py tags_cron

288
admin.py
View File

@ -13,52 +13,67 @@ css_rel_path = Path('build', 'style.css')
folders_to_ignore = ['venv', 'docs', '.git', '.idea', 'node_modules']
"""
COMPONENTS:
* App Engine
* Cloud Functions:
run_user_playlist
update_tag
run_all_playlists
run_all_playlist_stats
run_all_tags
"""
class Admin(Cmd):
intro = 'Music Tools Admin... ? for help'
prompt = '> '
locals = ['spotframework', 'fmframework', 'spotfm']
def prepare_stage(self):
print('>> freezing dependencies')
self.export_filtered_dependencies()
def do_prepare_local_stage(self, args):
"""
Prepare local working directory for deployment using static sarsoolib injections
"""
# Now done via online repos, not static injection
# print('>> backing up a directory')
# os.chdir(Path(__file__).absolute().parent.parent)
print('>> backing up a directory')
os.chdir(Path(__file__).absolute().parent.parent)
# print('>> deleting old deployment stage')
# shutil.rmtree(stage_dir, ignore_errors=True)
print('>> deleting old deployment stage')
shutil.rmtree(stage_dir, ignore_errors=True)
# print('>> copying main source')
# shutil.copytree('playlist-manager' if Path('playlist-manager').exists() else 'Music-Tools',
# stage_dir,
# ignore=lambda path, contents:
# contents if any(i in Path(path).parts for i in folders_to_ignore) else []
# )
print('>> copying main source')
shutil.copytree('playlist-manager' if Path('playlist-manager').exists() else 'Music-Tools',
stage_dir,
ignore=lambda path, contents:
contents if any(i in Path(path).parts for i in folders_to_ignore) else []
)
# for dependency in Admin.locals:
# print(f'>> injecting {dependency}')
# shutil.copytree(
# Path(dependency, dependency),
# Path(stage_dir, dependency)
# )
for dependency in Admin.locals:
print(f'>> injecting {dependency}')
shutil.copytree(
Path(dependency, dependency),
Path(stage_dir, dependency)
)
# os.chdir(stage_dir)
os.chdir(stage_dir)
# ADMIN
def do_set_project(self, args):
"""
Set project setting in gcloud console
"""
print('>> setting project')
subprocess.check_call('gcloud config set project sarsooxyz', shell=True)
def prepare_frontend(self):
print('>> building css')
subprocess.check_call(f'sass --style=compressed {str(scss_rel_path)} {str(css_rel_path)}', shell=True)
print('>> building javascript')
subprocess.check_call('npm run build', shell=True)
def prepare_main(self, path):
print('>> preparing main.py')
shutil.copy(f'main.{path}.py', 'main.py')
def deploy_function(self, name, timeout: int = 60, region='europe-west2'):
"""
Deploy function with required environment variables
"""
subprocess.check_call(
f'gcloud functions deploy {name} '
f'--region {region} '
@ -68,91 +83,10 @@ class Admin(Cmd):
f'--timeout={timeout}s', shell=True
)
def do_all(self, args):
# self.prepare_frontend()
self.prepare_stage()
self.prepare_main('api')
print('>> deploying api')
subprocess.check_call('gcloud app deploy', shell=True)
self.prepare_main('update_tag')
print('>> deploying tag function')
self.deploy_function('update_tag')
self.prepare_main('run_playlist')
print('>> deploying playlist function')
self.deploy_function('run_user_playlist')
def do_api(self, args):
self.prepare_frontend()
self.prepare_stage()
self.prepare_main('api')
print('>> deploying')
subprocess.check_call('gcloud app deploy', shell=True)
def do_tag(self, args):
self.prepare_stage()
self.prepare_main('update_tag')
print('>> deploying')
self.deploy_function('update_tag')
def do_playlist(self, args):
self.prepare_stage()
self.prepare_main('run_playlist')
print('>> deploying')
self.deploy_function('run_user_playlist')
# all playlists cron job
def do_playlist_cron(self, args):
self.prepare_stage()
self.prepare_main('cron')
print('>> deploying')
self.deploy_function('run_all_playlists')
# all stats refresh cron job
def do_playlist_stats(self, args):
self.prepare_stage()
self.prepare_main('cron')
print('>> deploying')
self.deploy_function('run_all_playlist_stats')
# all tags cron job
def do_tags_cron(self, args):
self.prepare_stage()
self.prepare_main('cron')
print('>> deploying')
self.deploy_function('run_all_tags')
# redeploy all cron job functions
def do_all_cron(self, args):
self.prepare_stage()
self.prepare_main('cron')
print('>> deploying playlists')
self.deploy_function('run_all_playlists')
print('>> deploying stats')
self.deploy_function('run_all_playlist_stats')
print('>> deploying tags')
self.deploy_function('run_all_tags')
def do_exit(self, args):
exit(0)
def do_sass(self, args):
subprocess.check_call(f'sass --style=compressed {str(scss_rel_path)} {str(css_rel_path)}', shell=True)
def do_watchsass(self, args):
subprocess.check_call(f'sass --style=compressed --watch {str(scss_rel_path)} {str(css_rel_path)}', shell=True)
def do_rename(self, args):
"""
Rename playlist in firestore
"""
from music.model.user import User
from music.model.playlist import Playlist
@ -172,10 +106,94 @@ class Admin(Cmd):
playlist.name = new_name
playlist.update()
def do_depend(self, args):
return subprocess.check_output('poetry export -f requirements.txt --output requirements.txt', shell=True, text=True)
# PYTHON
def do_filt_depend(self, args):
def copy_main_file(self, path):
"""
Copy main.{path}.py file corresponding to Python build stage
"""
print('>> preparing main.py')
shutil.copy(f'main.{path}.py', 'main.py')
def do_main_group(self, args):
"""
Compile front-end and deploy to App Engine. Deploy primary functions (run_user_playlist, update_tag)
"""
self.do_set_project(None)
self.export_filtered_dependencies()
self.do_app(args)
self.do_tag(None)
self.do_playlist(None)
def do_app(self, args):
"""
Compile front-end and deploy to App Engine
"""
if not '-nb' in args.strip().split(' '):
print(">> compiling frontend")
self.compile_frontend()
self.copy_main_file('api')
print('>> deploying app engine service')
subprocess.check_call('gcloud app deploy', shell=True)
def function_deploy(self, main, function_id):
self.copy_main_file(main)
print(f'>> deploying {function_id}')
self.deploy_function(function_id)
def do_tag(self, args):
"""
Deploy update_tag function
"""
self.function_deploy('update_tag', 'update_tag')
def do_playlist(self, args):
"""
Deploy run_user_playlist function
"""
self.function_deploy('run_playlist', 'run_user_playlist')
# all playlists cron job
def do_playlist_cron(self, args):
"""
Deploy run_all_playlists function
"""
self.function_deploy('cron', 'run_all_playlists')
# all stats refresh cron job
def do_playlist_stats_cron(self, args):
"""
Deploy run_all_playlist_stats function
"""
self.function_deploy('cron', 'run_all_playlist_stats')
# all tags cron job
def do_tags_cron(self, args):
"""
Deploy run_all_tags function
"""
self.function_deploy('cron', 'run_all_tags')
# redeploy all cron job functions
def do_cron_functions(self, args):
"""
Deploy background functions including cron job scheduling for update actions (run_all_playlists, run_all_playlist_stats, run_all_tags)
"""
self.do_set_project(None)
self.export_filtered_dependencies()
self.do_playlist_cron(None)
self.do_playlist_stats_cron(None)
self.do_tags_cron(None)
def do_pydepend(self, args):
"""
Generate and export requirements.txt from Poetry manifest
"""
self.export_filtered_dependencies()
def export_filtered_dependencies(self):
@ -191,6 +209,38 @@ class Admin(Cmd):
with open('requirements.txt', 'w') as f:
f.write("\n".join(filtered))
# FRONT-END
def compile_frontend(self):
"""
Compile sass to css and run npm build task
"""
print('>> building css')
subprocess.check_call(f'sass --style=compressed {str(scss_rel_path)} {str(css_rel_path)}', shell=True)
print('>> building javascript')
subprocess.check_call('npm run build', shell=True)
def do_sass(self, args):
"""
Compile sass to css
"""
subprocess.check_call(f'sass --style=compressed {str(scss_rel_path)} {str(css_rel_path)}', shell=True)
def do_watchsass(self, args):
"""
Run sass compiler with watch argument to begin watching source folder for changes
"""
subprocess.check_call(f'sass --style=compressed --watch {str(scss_rel_path)} {str(css_rel_path)}', shell=True)
def do_exit(self, args):
"""
Exit script
"""
exit(0)
def test():
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = 'service.json'
subprocess.check_call("python -u -m unittest discover -s tests", shell=True)

1086
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@
},
"homepage": "https://github.com/Sarsoo/Music-Tools#readme",
"dependencies": {
"@material-ui/core": "^4.11.2",
"@material-ui/core": "^4.11.3",
"@material-ui/icons": "^4.11.2",
"axios": "^0.21.1",
"chart.js": "^2.9.4",
@ -28,10 +28,10 @@
"react-router-dom": "^5.2.0"
},
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@babel/cli": "^7.12.16",
"@babel/core": "^7.12.16",
"@babel/preset-env": "^7.12.16",
"@babel/preset-react": "^7.12.13",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^3.6.0",

118
poetry.lock generated
View File

@ -1,15 +1,14 @@
[[package]]
name = "astroid"
version = "2.4.2"
version = "2.5"
description = "An abstract syntax tree for Python with inference support."
category = "dev"
optional = false
python-versions = ">=3.5"
python-versions = ">=3.6"
[package.dependencies]
lazy-object-proxy = ">=1.4.0,<1.5.0"
six = ">=1.12,<2.0"
wrapt = ">=1.11,<2.0"
lazy-object-proxy = ">=1.4.0"
wrapt = ">=1.11,<1.13"
[[package]]
name = "backports-datetime-fromisoformat"
@ -129,7 +128,7 @@ resolved_reference = "bf111d06cb4ffc9dbe0b57fbf83a3c236365c2ad"
[[package]]
name = "google-api-core"
version = "1.25.1"
version = "1.26.0"
description = "Google API client core library"
category = "main"
optional = false
@ -139,6 +138,7 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"
google-auth = ">=1.21.1,<2.0dev"
googleapis-common-protos = ">=1.6.0,<2.0dev"
grpcio = {version = ">=1.29.0,<2.0dev", optional = true, markers = "extra == \"grpc\""}
packaging = ">=14.3"
protobuf = ">=3.12.0"
pytz = "*"
requests = ">=2.18.0,<3.0.0dev"
@ -151,7 +151,7 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2)"]
[[package]]
name = "google-auth"
version = "1.25.0"
version = "1.26.1"
description = "Google Authentication Library"
category = "main"
optional = false
@ -318,11 +318,11 @@ i18n = ["Babel (>=0.8)"]
[[package]]
name = "lazy-object-proxy"
version = "1.4.3"
version = "1.5.2"
description = "A fast and thorough lazy object proxy."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "markupsafe"
@ -340,6 +340,17 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "packaging"
version = "20.9"
description = "Core utilities for Python packages"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
pyparsing = ">=2.0.2"
[[package]]
name = "protobuf"
version = "3.14.0"
@ -385,6 +396,14 @@ isort = ">=4.2.5,<5"
mccabe = ">=0.6,<0.7"
toml = ">=0.7.1"
[[package]]
name = "pyparsing"
version = "2.4.7"
description = "Python parsing module"
category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "pytz"
version = "2021.1"
@ -413,7 +432,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
[[package]]
name = "rsa"
version = "4.7"
version = "4.7.1"
description = "Pure-Python RSA implementation"
category = "main"
optional = false
@ -432,11 +451,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "soupsieve"
version = "2.1"
version = "2.2"
description = "A modern CSS selector implementation for Beautiful Soup."
category = "main"
optional = false
python-versions = ">=3.5"
python-versions = ">=3.6"
[[package]]
name = "spotfm"
@ -475,7 +494,7 @@ tabulate = "^0.8.7"
type = "git"
url = "https://github.com/Sarsoo/spotframework.git"
reference = "master"
resolved_reference = "ac8944764f1ff3c3db40b4bc7a8897a482db9434"
resolved_reference = "1ebd99e99bb52545b9b8c6d4e4fd11b9cb0ba3a8"
[[package]]
name = "tabulate"
@ -536,8 +555,8 @@ content-hash = "943b58ae3ae110c4c990bab3fa72b73b0f8a172905246839c2fe481c73919067
[metadata.files]
astroid = [
{file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"},
{file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"},
{file = "astroid-2.5-py3-none-any.whl", hash = "sha256:87ae7f2398b8a0ae5638ddecf9987f081b756e0e9fc071aeebdca525671fc4dc"},
{file = "astroid-2.5.tar.gz", hash = "sha256:b31c92f545517dcc452f284bc9c044050862fbe6d93d2b3de4a215a6b384bf0d"},
]
backports-datetime-fromisoformat = [
{file = "backports-datetime-fromisoformat-1.0.0.tar.gz", hash = "sha256:9577a2a9486cd7383a5f58b23bb8e81cf0821dbbc0eb7c87d3fa198c1df40f5c"},
@ -576,12 +595,12 @@ flask = [
]
fmframework = []
google-api-core = [
{file = "google-api-core-1.25.1.tar.gz", hash = "sha256:0e152ec37b8481d1be1258d95844a5a7031cd3d83d7c7046d9e9b2d807042440"},
{file = "google_api_core-1.25.1-py2.py3-none-any.whl", hash = "sha256:292dd636ed381098d24b7093ccb826b2278a12d886a3fc982084069aa24a8fbb"},
{file = "google-api-core-1.26.0.tar.gz", hash = "sha256:4230ec764d48ca934fe69b85cc217e31e844e176f68df93e252acd55350e730b"},
{file = "google_api_core-1.26.0-py2.py3-none-any.whl", hash = "sha256:002e44c533299aecd9dd265d200f9eacd9957cddd2c72e2cd1cb5cea127e972d"},
]
google-auth = [
{file = "google-auth-1.25.0.tar.gz", hash = "sha256:514e39f4190ca972200ba33876da5a8857c5665f2b4ccc36c8b8ee21228aae80"},
{file = "google_auth-1.25.0-py2.py3-none-any.whl", hash = "sha256:008e23ed080674f69f9d2d7d80db4c2591b9bb307d136cea7b3bc129771d211d"},
{file = "google-auth-1.26.1.tar.gz", hash = "sha256:1b461d079b5650efe492a7814e95c536ffa9e7a96e39a6d16189c1604f18554f"},
{file = "google_auth-1.26.1-py2.py3-none-any.whl", hash = "sha256:8ce6862cf4e9252de10045f05fa80393fde831da9c2b45c39288edeee3cde7f2"},
]
google-cloud-core = [
{file = "google-cloud-core-1.6.0.tar.gz", hash = "sha256:c6abb18527545379fc82efc4de75ce9a3772ccad2fc645adace593ba097cbb02"},
@ -675,27 +694,30 @@ jinja2 = [
{file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"},
]
lazy-object-proxy = [
{file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"},
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"},
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"},
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"},
{file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"},
{file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"},
{file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"},
{file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"},
{file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"},
{file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"},
{file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"},
{file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"},
{file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"},
{file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"},
{file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"},
{file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"},
{file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"},
{file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"},
{file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"},
{file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"},
{file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"},
{file = "lazy-object-proxy-1.5.2.tar.gz", hash = "sha256:5944a9b95e97de1980c65f03b79b356f30a43de48682b8bdd90aa5089f0ec1f4"},
{file = "lazy_object_proxy-1.5.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:e960e8be509e8d6d618300a6c189555c24efde63e85acaf0b14b2cd1ac743315"},
{file = "lazy_object_proxy-1.5.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:522b7c94b524389f4a4094c4bf04c2b02228454ddd17c1a9b2801fac1d754871"},
{file = "lazy_object_proxy-1.5.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:3782931963dc89e0e9a0ae4348b44762e868ea280e4f8c233b537852a8996ab9"},
{file = "lazy_object_proxy-1.5.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:429c4d1862f3fc37cd56304d880f2eae5bd0da83bdef889f3bd66458aac49128"},
{file = "lazy_object_proxy-1.5.2-cp35-cp35m-win32.whl", hash = "sha256:cd1bdace1a8762534e9a36c073cd54e97d517a17d69a17985961265be6d22847"},
{file = "lazy_object_proxy-1.5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:ddbdcd10eb999d7ab292677f588b658372aadb9a52790f82484a37127a390108"},
{file = "lazy_object_proxy-1.5.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ecb5dd5990cec6e7f5c9c1124a37cb2c710c6d69b0c1a5c4aa4b35eba0ada068"},
{file = "lazy_object_proxy-1.5.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b6577f15d5516d7d209c1a8cde23062c0f10625f19e8dc9fb59268859778d7d7"},
{file = "lazy_object_proxy-1.5.2-cp36-cp36m-win32.whl", hash = "sha256:c8fe2d6ff0ff583784039d0255ea7da076efd08507f2be6f68583b0da32e3afb"},
{file = "lazy_object_proxy-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:fa5b2dee0e231fa4ad117be114251bdfe6afe39213bd629d43deb117b6a6c40a"},
{file = "lazy_object_proxy-1.5.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1d33d6f789697f401b75ce08e73b1de567b947740f768376631079290118ad39"},
{file = "lazy_object_proxy-1.5.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:57fb5c5504ddd45ed420b5b6461a78f58cbb0c1b0cbd9cd5a43ad30a4a3ee4d0"},
{file = "lazy_object_proxy-1.5.2-cp37-cp37m-win32.whl", hash = "sha256:e7273c64bccfd9310e9601b8f4511d84730239516bada26a0c9846c9697617ef"},
{file = "lazy_object_proxy-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f4e5e68b7af950ed7fdb594b3f19a0014a3ace0fedb86acb896e140ffb24302"},
{file = "lazy_object_proxy-1.5.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cadfa2c2cf54d35d13dc8d231253b7985b97d629ab9ca6e7d672c35539d38163"},
{file = "lazy_object_proxy-1.5.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e7428977763150b4cf83255625a80a23dfdc94d43be7791ce90799d446b4e26f"},
{file = "lazy_object_proxy-1.5.2-cp38-cp38-win32.whl", hash = "sha256:2f2de8f8ac0be3e40d17730e0600619d35c78c13a099ea91ef7fb4ad944ce694"},
{file = "lazy_object_proxy-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:38c3865bd220bd983fcaa9aa11462619e84a71233bafd9c880f7b1cb753ca7fa"},
{file = "lazy_object_proxy-1.5.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:8a44e9901c0555f95ac401377032f6e6af66d8fc1fbfad77a7a8b1a826e0b93c"},
{file = "lazy_object_proxy-1.5.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fa7fb7973c622b9e725bee1db569d2c2ee64d2f9a089201c5e8185d482c7352d"},
{file = "lazy_object_proxy-1.5.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:71a1ef23f22fa8437974b2d60fedb947c99a957ad625f83f43fd3de70f77f458"},
{file = "lazy_object_proxy-1.5.2-cp39-cp39-win32.whl", hash = "sha256:ef3f5e288aa57b73b034ce9c1f1ac753d968f9069cd0742d1d69c698a0167166"},
{file = "lazy_object_proxy-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:37d9c34b96cca6787fe014aeb651217944a967a5b165e2cacb6b858d2997ab84"},
]
markupsafe = [
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
@ -736,6 +758,10 @@ mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
]
packaging = [
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
]
protobuf = [
{file = "protobuf-3.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:629b03fd3caae7f815b0c66b41273f6b1900a579e2ccb41ef4493a4f5fb84f3a"},
{file = "protobuf-3.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:5b7a637212cc9b2bcf85dd828b1178d19efdf74dbfe1ddf8cd1b8e01fdaaa7f5"},
@ -790,6 +816,10 @@ pylint = [
{file = "pylint-2.5.3-py3-none-any.whl", hash = "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c"},
{file = "pylint-2.5.3.tar.gz", hash = "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc"},
]
pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
]
pytz = [
{file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
{file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"},
@ -799,16 +829,16 @@ requests = [
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
]
rsa = [
{file = "rsa-4.7-py3-none-any.whl", hash = "sha256:a8774e55b59fd9fc893b0d05e9bfc6f47081f46ff5b46f39ccf24631b7be356b"},
{file = "rsa-4.7.tar.gz", hash = "sha256:69805d6b69f56eb05b62daea3a7dbd7aa44324ad1306445e05da8060232d00f4"},
{file = "rsa-4.7.1-py3-none-any.whl", hash = "sha256:74ba16e7ef58920b80b5c54c1c1066d391a2c1e812c466773f74c634eb12253b"},
{file = "rsa-4.7.1.tar.gz", hash = "sha256:9d74d1ff850745c9802cd6b53382bfeec7f6dbe4e26ee2759241ed1e7b0ecf5d"},
]
six = [
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
]
soupsieve = [
{file = "soupsieve-2.1-py3-none-any.whl", hash = "sha256:4bb21a6ee4707bf43b61230e80740e71bfe56e55d1f1f50924b087bb2975c851"},
{file = "soupsieve-2.1.tar.gz", hash = "sha256:6dc52924dc0bc710a5d16794e6b3480b2c7c08b07729505feab2b2c16661ff6e"},
{file = "soupsieve-2.2-py3-none-any.whl", hash = "sha256:d3a5ea5b350423f47d07639f74475afedad48cf41c0ad7a82ca13a3928af34f6"},
{file = "soupsieve-2.2.tar.gz", hash = "sha256:407fa1e8eb3458d1b5614df51d9651a1180ea5fedf07feb46e45d7e25e6d6cdd"},
]
spotfm = []
spotframework = []