├── .flake8 ├── .gitignore ├── .isort.cfg ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── hooks.sh ├── hyperglass_agent ├── .gitignore ├── __init__.py ├── api │ ├── __init__.py │ └── web.py ├── cli │ ├── __init__.py │ ├── actions.py │ ├── commands.py │ ├── echo.py │ ├── exceptions.py │ └── static.py ├── config.py ├── console.py ├── constants.py ├── example_config.yaml ├── exceptions.py ├── execute.py ├── log.py ├── models │ ├── __init__.py │ ├── _formatters.py │ ├── _utils.py │ ├── commands.py │ ├── general.py │ └── request.py ├── nos_utils │ ├── __init__.py │ ├── bird.py │ └── frr.py ├── payload.py └── util.py ├── poetry.lock └── pyproject.toml /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=88 3 | count=True 4 | show-source=False 5 | statistics=True 6 | exclude=.git, __pycache__ 7 | filename=*.py 8 | per-file-ignores=hyperglass_agent/models/*.py:N805,E0213,R0903,E501,C0301 9 | ignore=W503,C0330,R504,D202,PIE781 10 | select=B, BLK, C, D, E, F, I, II, N, P, PIE, S, R, W 11 | disable-noqa=False 12 | hang-closing=False 13 | max-complexity=10 14 | # format=${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode* 3 | test.py 4 | .python-version 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | static 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # celery beat schedule file 97 | celerybeat-schedule 98 | 99 | # SageMath parsed files 100 | *.sage.py 101 | 102 | # Environments 103 | .env 104 | .venv 105 | env/ 106 | venv/ 107 | ENV/ 108 | env.bak/ 109 | venv.bak/ 110 | 111 | # Spyder project settings 112 | .spyderproject 113 | .spyproject 114 | 115 | # Rope project settings 116 | .ropeproject 117 | 118 | # mkdocs documentation 119 | /site 120 | 121 | # mypy 122 | .mypy_cache/ 123 | .dmypy.json 124 | dmypy.json 125 | 126 | # Pyre type checker 127 | .pyre/ 128 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length = 88 3 | indent = ' ' 4 | include_trailing_comma = True 5 | multi_line_output = 3 6 | balanced_wrapping = True 7 | length_sort = True 8 | force_single_line = False 9 | import_heading_stdlib = Standard Library 10 | import_heading_thirdparty = Third Party 11 | import_heading_firstparty = Project 12 | known_first_party = hyperglass_agent 13 | known_third_party = starlette,fastapi,inquirer 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: flake8 6 | - repo: local 7 | hooks: 8 | - id: scripts 9 | name: Commit Scripts 10 | entry: hooks.sh 11 | language: script 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## 0.1.5 - 2020-06-28 9 | 10 | ### Fixed 11 | - Incorrect name of systemd service file (`hyperglass-service.service` → `hyperglass-agent.service`) 12 | - Handle empty responses properly 13 | - Format `bgp_aspath` and `bgp_community` commands properly for BIRD ([#4](https://github.com/checktheroads/hyperglass-agent/issues/4)) 14 | 15 | ### Added 16 | - Logging to file -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Clear BSD License 2 | 3 | Copyright (c) 2019 Matthew Love 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted (subject to the limitations in the disclaimer 8 | below) provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, 11 | this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from this 19 | software without specific prior written permission. 20 | 21 | NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY 22 | THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 23 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 25 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 26 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 27 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 28 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 29 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 30 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 31 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATION WARNING 2 | 3 | Starting in [hyperglass](https://github.com/checktheroads/hyperglass) v1.0.0-beta.76, hyperglass-agent is in the process of being deprecated. Moving forward, FRR & BIRD can be interacted with in the same manner as any other device — via SSH. See [here](https://hyperglass.io/docs/platforms#caveats) for details/caveats. Everything in hyperglass-agent will still work as-is for now, but I'd encourage users to try moving to the new transport method. 4 | 5 | 6 |
7 | 8 | 9 | 10 |
11 | 12 | **The hyperglass agent is a RESTful API agent for [hyperglass](https://github.com/checktheroads/hyperglass), currently supporting:** 13 | 14 | ### [Free Range Routing](https://frrouting.org/) 15 | ### [BIRD Routing Daemon](https://bird.network.cz/) 16 | 17 |
18 | 19 | 20 | 21 | # Installation 22 | 23 | ### 📚 [Check out the docs](https://hyperglass.io/docs/agent/installation) 24 | 25 | ### 🛠 [Changelog](https://github.com/checktheroads/hyperglass-agent/blob/master/CHANGELOG.md) 26 | 27 | # License 28 | 29 | [Clear BSD License](https://github.com/checktheroads/hyperglass-agent/master/LICENSE) 30 | -------------------------------------------------------------------------------- /hooks.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # LC=$(./cli.py line-count-badge) 4 | 5 | # echo $LC 6 | 7 | # if [[ ! $? == 0 ]]; then 8 | # exit 1 9 | # fi 10 | exit 0 -------------------------------------------------------------------------------- /hyperglass_agent/.gitignore: -------------------------------------------------------------------------------- 1 | config.yaml -------------------------------------------------------------------------------- /hyperglass_agent/__init__.py: -------------------------------------------------------------------------------- 1 | """The Linux Routing Agent for hyperglass. 2 | 3 | https://github.com/checktheroads/hyperglass-agent 4 | 5 | The Clear BSD License 6 | 7 | Copyright (c) 2020 Matthew Love 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted (subject to the limitations in the disclaimer 12 | below) provided that the following conditions are met: 13 | 14 | * Redistributions of source code must retain the above copyright notice, 15 | this list of conditions and the following disclaimer. 16 | 17 | * Redistributions in binary form must reproduce the above copyright 18 | notice, this list of conditions and the following disclaimer in the 19 | documentation and/or other materials provided with the distribution. 20 | 21 | * Neither the name of the copyright holder nor the names of its 22 | contributors may be used to endorse or promote products derived from this 23 | software without specific prior written permission. 24 | 25 | NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY 26 | THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 27 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 28 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 29 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 30 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 31 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 32 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 33 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 34 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 35 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 36 | POSSIBILITY OF SUCH DAMAGE. 37 | """ 38 | 39 | # Third Party 40 | import uvloop 41 | import stackprinter 42 | 43 | # Project 44 | from hyperglass_agent.util import set_app_path 45 | 46 | stackprinter.set_excepthook() 47 | uvloop.install() 48 | 49 | set_app_path(required=False) 50 | 51 | __name__ = "hyperglass_agent" 52 | __title__ = "Hyperglass Agent" 53 | __description__ = "The Linux Routing Agent for hyperglass" 54 | __version__ = "0.1.6" 55 | -------------------------------------------------------------------------------- /hyperglass_agent/api/__init__.py: -------------------------------------------------------------------------------- 1 | """hyperglass-agent REST API.""" 2 | -------------------------------------------------------------------------------- /hyperglass_agent/api/web.py: -------------------------------------------------------------------------------- 1 | """Web server frontend, passes raw query to backend validation & execution.""" 2 | 3 | # Standard Library 4 | import json 5 | 6 | # Third Party 7 | from fastapi import FastAPI, HTTPException 8 | from pydantic import ValidationError 9 | from fastapi.exceptions import RequestValidationError 10 | from starlette.responses import JSONResponse 11 | from starlette.exceptions import HTTPException as StarletteHTTPException 12 | 13 | # Project 14 | from hyperglass_agent import __title__, __version__, __description__ 15 | from hyperglass_agent.log import log 16 | from hyperglass_agent.config import APP_PATH, params 17 | from hyperglass_agent.execute import run_query 18 | from hyperglass_agent.payload import jwt_decode, jwt_encode 19 | from hyperglass_agent.exceptions import HyperglassAgentError 20 | from hyperglass_agent.models.request import Request, EncodedRequest 21 | 22 | CERT_PATH = APP_PATH / "agent_cert.pem" 23 | KEY_PATH = APP_PATH / "agent_key.pem" 24 | 25 | 26 | API_PARAMS = { 27 | "host": params.listen_address.compressed, 28 | "port": params.port, 29 | "debug": params.debug, 30 | } 31 | 32 | if params.ssl.enable: 33 | API_PARAMS.update({"ssl_certfile": CERT_PATH, "ssl_keyfile": KEY_PATH}) 34 | 35 | if params.debug: 36 | API_PARAMS.update({"log_level": "debug"}) 37 | 38 | api = FastAPI( 39 | title=__title__, 40 | description=__description__, 41 | version=__version__, 42 | docs_url=None, 43 | redoc_url="/docs", 44 | ) 45 | 46 | 47 | @api.exception_handler(StarletteHTTPException) 48 | async def http_exception_handler(request, exc): 49 | """Handle application errors. 50 | 51 | Arguments: 52 | request {object} -- Request object 53 | exc {object} -- Exception object 54 | 55 | Returns: 56 | {str} -- JSON response 57 | """ 58 | log.error(str(exc.detail)) 59 | return JSONResponse(content={"error": str(exc.detail)}, status_code=exc.status_code) 60 | 61 | 62 | @api.exception_handler(RequestValidationError) 63 | async def validation_exception_handler(request, exc): 64 | """Handle validation errors. 65 | 66 | Arguments: 67 | request {object} -- Request object 68 | exc {object} -- Exception object 69 | 70 | Returns: 71 | {str} -- JSON response 72 | """ 73 | log.error(str(exc)) 74 | return JSONResponse(content={"error": str(exc)}, status_code=400) 75 | 76 | 77 | @api.post("/query/", status_code=200, response_model=EncodedRequest) 78 | async def query_entrypoint(query: EncodedRequest): 79 | """Validate and process input request. 80 | 81 | Arguments: 82 | query {dict} -- Encoded JWT 83 | 84 | Returns: 85 | {obj} -- JSON response 86 | """ 87 | try: 88 | log.debug(f"Raw Query JSON: {query.json()}") 89 | 90 | decrypted_query = await jwt_decode(query.encoded) 91 | decrypted_query = json.loads(decrypted_query) 92 | 93 | log.debug(f"Decrypted Query: {decrypted_query}") 94 | 95 | validated_query = Request(**decrypted_query) 96 | query_output = await run_query(validated_query) 97 | 98 | log.debug(f"Query Output:\n{query_output}") 99 | 100 | encoded = await jwt_encode(query_output) 101 | return {"encoded": encoded} 102 | 103 | except ValidationError as err_validation: 104 | raise RequestValidationError(str(err_validation)) 105 | 106 | except HyperglassAgentError as err_agent: 107 | raise HTTPException(status_code=err_agent.code, detail=str(err_agent)) 108 | 109 | 110 | def start(): 111 | """Start the web server with Uvicorn ASGI.""" 112 | import uvicorn 113 | 114 | uvicorn.run(api, **API_PARAMS) 115 | 116 | 117 | if __name__ == "__main__": 118 | start() 119 | -------------------------------------------------------------------------------- /hyperglass_agent/cli/__init__.py: -------------------------------------------------------------------------------- 1 | """hyperglass-agent CLI.""" 2 | -------------------------------------------------------------------------------- /hyperglass_agent/cli/actions.py: -------------------------------------------------------------------------------- 1 | """Actions executed by commands.""" 2 | 3 | # Standard Library 4 | import os 5 | import shutil 6 | from typing import Any, Iterable, Optional, Generator 7 | from pathlib import Path 8 | from datetime import datetime, timedelta 9 | from ipaddress import ip_address 10 | 11 | # Third Party 12 | from click import echo, style, prompt, confirm 13 | from inquirer import List as InquirerList 14 | from inquirer import Checkbox 15 | 16 | # Project 17 | from hyperglass_agent.util import get_addresses 18 | from hyperglass_agent.cli.echo import ( 19 | info, 20 | error, 21 | label, 22 | status, 23 | inquire, 24 | success, 25 | warning, 26 | ) 27 | from hyperglass_agent.cli.static import CL, NL, WS, WARNING, E 28 | 29 | 30 | def create_dir(path: Any, **kwargs: Any) -> bool: 31 | """Validate and attempt to create a directory, if it does not exist.""" 32 | 33 | # If input path is not a path object, try to make it one 34 | if not isinstance(path, Path): 35 | try: 36 | path = Path(path) 37 | except TypeError: 38 | error("{p} is not a valid path", p=path) 39 | 40 | # If path does not exist, try to create it 41 | if not path.exists(): 42 | try: 43 | path.mkdir(**kwargs) 44 | except PermissionError: 45 | error( 46 | "{u} does not have permission to create {p}. Try running with sudo?", 47 | u=os.getlogin(), 48 | p=path, 49 | ) 50 | 51 | # Verify the path was actually created 52 | if path.exists(): 53 | success("Created {p}", p=path) 54 | 55 | # If the path already exists, inform the user 56 | elif path.exists(): 57 | info("{p} already exists", p=path) 58 | 59 | return True 60 | 61 | 62 | def generate_secret(length: int = 32) -> str: 63 | """Generate a secret for JWT encoding.""" 64 | import secrets 65 | 66 | gen_secret = secrets.token_urlsafe(length) 67 | status( 68 | """ 69 | This secret will be used to encrypt & decrypt the communication between 70 | hyperglass and hyperglass-agent. Before proceeding any further, please 71 | add the secret to the `password:` field of the device's configuration in 72 | hyperglass's devices.yaml file, and restart hyperglass. 73 | """ 74 | ) 75 | label("Secret: {s}", s=gen_secret) 76 | done = confirm( 77 | "Press enter once complete...", 78 | default=True, 79 | prompt_suffix="", 80 | show_default=False, 81 | ) 82 | if done: # noqa: R503 83 | return gen_secret 84 | 85 | 86 | def migrate_config(force: bool = False, secret: Optional[str] = None) -> None: 87 | """Copy example config file and remove .example extensions.""" 88 | 89 | app_path = os.environ.get("hyperglass_agent_directory") 90 | 91 | if app_path is None: 92 | app_path = find_app_path() 93 | else: 94 | app_path = Path(app_path) 95 | 96 | example = Path(__file__).parent.parent / "example_config.yaml" 97 | target_file = app_path / "config.yaml" 98 | 99 | def copy(secret): 100 | shutil.copyfile(example, target_file) 101 | if not target_file.exists(): 102 | raise FileNotFoundError(str(target_file) + "does not exist.") 103 | 104 | with target_file.open("r") as f: 105 | data = f.read() 106 | 107 | if secret is None: 108 | secret = generate_secret() 109 | 110 | data = data.replace("secret: null", "secret: '{}'".format(secret)) 111 | 112 | with target_file.open("w") as f: 113 | f.write(data) 114 | 115 | success("Successfully migrated example config file to {t}", t=target_file) 116 | 117 | try: 118 | if target_file.exists(): 119 | if not force: 120 | info("{f} already exists", f=str(target_file)) 121 | else: 122 | copy(secret) 123 | else: 124 | copy(secret) 125 | 126 | except Exception as e: 127 | error("Failed to migrate '{f}': {e}", f=str(target_file), e=e) 128 | 129 | 130 | def find_app_path() -> Path: 131 | """Try to find the app_path, prompt user to set one if it is not found.""" 132 | from hyperglass_agent.util import set_app_path 133 | from hyperglass_agent.constants import APP_PATHS 134 | 135 | try: 136 | set_app_path(required=True) 137 | app_path = Path(os.environ["hyperglass_agent_directory"]) 138 | except RuntimeError: 139 | warning( 140 | "None of the supported paths for hyperglass-agent were found.\n" 141 | + "Checked:\n{one}\n{two}", 142 | one=APP_PATHS[0], 143 | two=APP_PATHS[1], 144 | ) 145 | create = confirm(style("Would you like to create one?", **WARNING)) 146 | if not create: 147 | error( 148 | "hyperglass-agent requires an application path, " 149 | + "but you've chosen not to create one." 150 | ) 151 | elif create: 152 | available_paths = [ 153 | InquirerList( 154 | "selected", 155 | message="Choose a directory for hyperglass-agent", 156 | choices=APP_PATHS, 157 | ) 158 | ] 159 | answer = inquire(available_paths) 160 | if answer is None: 161 | error("A directory for hyperglass-agent is required") 162 | selected = answer["selected"] 163 | 164 | if not selected.exists(): 165 | create_dir(selected) 166 | 167 | app_path = selected 168 | 169 | return app_path 170 | 171 | 172 | def read_cert() -> Generator: 173 | """Read public key attributes.""" 174 | from cryptography import x509 175 | from cryptography.x509.oid import NameOID 176 | from cryptography.x509.extensions import ExtensionOID 177 | from cryptography.hazmat.backends import default_backend 178 | 179 | app_path = find_app_path() 180 | cert_path = app_path / "agent_cert.pem" 181 | 182 | cert = x509.load_pem_x509_certificate(cert_path.read_bytes(), default_backend()) 183 | 184 | for attr in cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME): 185 | yield attr.value 186 | for attr in cert.extensions.get_extension_for_oid( 187 | ExtensionOID.SUBJECT_ALTERNATIVE_NAME 188 | ).value._general_names: 189 | yield attr.value 190 | 191 | 192 | def make_cert( 193 | cn: str, sans: Iterable, o: str, start: datetime, end: datetime, size: int = 2048 194 | ) -> Generator: 195 | """Generate public & private key pair for SSL.""" 196 | from cryptography.hazmat.backends import default_backend 197 | from cryptography.hazmat.primitives import serialization 198 | from cryptography.hazmat.primitives.asymmetric import rsa 199 | from cryptography import x509 200 | from cryptography.x509.oid import NameOID 201 | from cryptography.hazmat.primitives import hashes 202 | 203 | key = rsa.generate_private_key( 204 | public_exponent=65537, key_size=size, backend=default_backend() 205 | ) 206 | subject = issuer = x509.Name( 207 | [ 208 | x509.NameAttribute(NameOID.COMMON_NAME, cn), 209 | x509.NameAttribute(NameOID.ORGANIZATION_NAME, o), 210 | ] 211 | ) 212 | cert = ( 213 | x509.CertificateBuilder() 214 | .subject_name(subject) 215 | .issuer_name(issuer) 216 | .public_key(key.public_key()) 217 | .serial_number(x509.random_serial_number()) 218 | .not_valid_before(start) 219 | .not_valid_after(end) 220 | .add_extension( 221 | x509.SubjectAlternativeName( 222 | [x509.DNSName(cn), *(x509.IPAddress(i) for i in sans)] 223 | ), 224 | critical=False, 225 | ) 226 | .sign(key, hashes.SHA256(), default_backend()) 227 | ) 228 | yield cert.public_bytes(serialization.Encoding.PEM) 229 | yield key.private_bytes( 230 | encoding=serialization.Encoding.PEM, 231 | format=serialization.PrivateFormat.TraditionalOpenSSL, 232 | encryption_algorithm=serialization.NoEncryption(), 233 | ) 234 | 235 | 236 | def write_cert(name: str, org: str, duration: int, size: int, show: bool) -> None: 237 | """Generate SSL certificate keypair.""" 238 | app_path = find_app_path() 239 | cert_path = app_path / "agent_cert.pem" 240 | key_path = app_path / "agent_key.pem" 241 | 242 | start = datetime.now() 243 | end = start + timedelta(days=duration * 365) 244 | 245 | label("Hostname: {cn}", cn=name) 246 | status( 247 | """ 248 | A self-signed certificate with the above hostname as the common name 249 | attribute will be generated. This hostname must be resolvable by 250 | hyperglass via either DNS or a host file, and must match the device's 251 | `address:` field in hyperglass's devices.yaml.""" 252 | ) 253 | use_name = confirm("Is this the correct hostname?", default=True) 254 | 255 | if not use_name: 256 | name = prompt("Please enter the correct hostname", type=str) 257 | 258 | all_ips = [f"{a} [{i}]" for i, a in get_addresses()] 259 | 260 | status( 261 | """ 262 | hyperglass-agent adds any IP addresses reachable by hyperglass as 263 | subject alternative names to the SSL certificate. Please select any IP 264 | addresses over which hyperglass may communicate with hyperglass-agent.""" 265 | ) 266 | 267 | ips = [Checkbox("ips", message="Select IPs", choices=all_ips)] 268 | selected = [i.split("[")[0].strip() for i in inquire(ips)["ips"]] 269 | selected_ips = [ip_address(i) for i in selected] 270 | 271 | cert, key = make_cert( 272 | cn=name, sans=selected_ips, o=org, start=start, end=end, size=size 273 | ) 274 | if show: 275 | info(f'Public Key:\n{cert.decode("utf8")}') 276 | info(f'Private Key:\n{key.decode("utf8")}') 277 | 278 | with cert_path.open("wb") as cf: 279 | cf.write(cert) 280 | 281 | if not cert_path.exists(): 282 | error("Error writing public key to {f}", f=cert_path.absolute()) 283 | 284 | success("Wrote public key to: {f}", f=cert_path.absolute()) 285 | 286 | with key_path.open("wb") as kf: 287 | kf.write(key) 288 | 289 | if not key_path.exists(): 290 | error("Error writing private key to {f}", f=key_path.absolute()) 291 | 292 | success("Wrote private key to: {f}", f=key_path.absolute()) 293 | 294 | 295 | def send_certificate() -> None: 296 | """Send this device's public key to hyperglass.""" 297 | 298 | from hyperglass_agent.config import params 299 | from hyperglass_agent.util import send_public_key 300 | from pydantic import AnyHttpUrl, create_model, ValidationError 301 | 302 | app_path = find_app_path() 303 | cert_file = app_path / "agent_cert.pem" 304 | 305 | device_name = read_cert().send(None) 306 | 307 | if params.ssl is not None and not params.ssl.enable: 308 | confirm( 309 | "SSL is disabled. Proceed with sending certificate to hyperglass?", 310 | default=False, 311 | abort=True, 312 | ) 313 | 314 | if not cert_file.exists(): 315 | error("File {f} does not exist", f=cert_file) 316 | 317 | with cert_file.open("r") as f: 318 | cert = f.read().strip() 319 | 320 | _hg_url = prompt("Enter hyperglass URL (e.g. https://lg.example.com)", type=str) 321 | 322 | url_model = create_model("UrlModel", url=(AnyHttpUrl, ...)) 323 | 324 | try: 325 | hg_url = url_model(url=_hg_url) 326 | except ValidationError as ve: 327 | msg = ve.errors()[0]["msg"] 328 | warning("URL {u} is invalid: {e}", u=_hg_url, e=msg) 329 | _hg_url = prompt("Enter hyperglass URL (e.g. https://lg.example.com)", type=str) 330 | try: 331 | hg_url = url_model(url=_hg_url) 332 | except ValidationError as ve: 333 | msg = ve.errors()[0]["msg"] 334 | error("URL {u} is invalid: {e}", u=_hg_url, e=msg) 335 | 336 | try: 337 | status = send_public_key( 338 | str(hg_url.url), device_name=device_name, certificate=cert, params=params 339 | ) 340 | success(status) 341 | except RuntimeError as re: 342 | error(str(re)) 343 | 344 | 345 | def install_systemd(service_path: Path) -> bool: 346 | """Installs generated systemd file to system's systemd directory.""" 347 | systemd = Path("/etc/systemd/system") 348 | installed = systemd / "hyperglass-agent.service" 349 | 350 | if not systemd.exists(): 351 | error("{e} does not exist. Unable to install systemd service.", e=systemd) 352 | 353 | if installed.is_symlink(): 354 | installed.unlink() 355 | 356 | installed.symlink_to(service_path) 357 | 358 | if not installed.exists(): 359 | error("Unable to symlink {s} to {d}", s=service_path, d=installed) 360 | 361 | success("Symlinked {s} to {d}", s=service_path, d=installed) 362 | return True 363 | 364 | 365 | def make_systemd() -> bool: 366 | """Generate a systemd file based on the local system.""" 367 | from shutil import which 368 | from getpass import getuser 369 | 370 | template = """ 371 | [Unit] 372 | Description=hyperglass-agent 373 | After=network.target 374 | 375 | [Service] 376 | User={user} 377 | Group={group} 378 | ExecStart={bin_path} start 379 | 380 | [Install] 381 | WantedBy=multi-user.target 382 | """ 383 | app_path = find_app_path() 384 | service_path = app_path / "hyperglass-agent.service" 385 | cmd_path = which("hyperglass-agent") 386 | 387 | if not cmd_path: 388 | bin_path = "python3 -m hyperglass_agent.console" 389 | warning("hyperglass executable not found, using {h}", h=bin_path) 390 | else: 391 | bin_path = cmd_path 392 | 393 | if app_path == Path.home(): 394 | user = getuser() 395 | else: 396 | user = "root" 397 | 398 | systemd = template.format(user=user, group=user, bin_path=bin_path) 399 | 400 | info(f"Generated systemd service:\n{systemd}") 401 | 402 | if service_path.exists(): 403 | service_path.unlink() 404 | 405 | with service_path.open("w") as f: 406 | f.write(systemd) 407 | 408 | if not service_path.exists(): 409 | error("Error writing systemd file to {f}", f=service_path) 410 | 411 | install_systemd(service_path) 412 | 413 | return True 414 | 415 | 416 | def start_web_server() -> None: 417 | """Start web server.""" 418 | 419 | find_app_path() 420 | try: 421 | from hyperglass_agent.config import params 422 | from hyperglass_agent.api.web import start 423 | 424 | msg_start = "Starting hyperglass agent web server on" 425 | msg_uri = "http://" 426 | msg_host = str(params.listen_address) 427 | msg_port = str(params.port) 428 | msg_len = len("".join([msg_start, WS[1], msg_uri, msg_host, CL[1], msg_port])) 429 | 430 | echo( 431 | NL[1] 432 | + WS[msg_len + 8] 433 | + E.ROCKET 434 | + NL[1] 435 | + E.CHECK 436 | + style(msg_start, fg="green", bold=True) 437 | + WS[1] 438 | + style(msg_uri, fg="white") 439 | + style(msg_host, fg="blue", bold=True) 440 | + style(CL[1], fg="white") 441 | + style(msg_port, fg="magenta", bold=True) 442 | + WS[1] 443 | + E.ROCKET 444 | + NL[1] 445 | + WS[1] 446 | + NL[1] 447 | ) 448 | start() 449 | 450 | except Exception as e: 451 | error("Failed to start web server: {e}", e=e) 452 | -------------------------------------------------------------------------------- /hyperglass_agent/cli/commands.py: -------------------------------------------------------------------------------- 1 | """hyperglass-agent CLI commands.""" 2 | 3 | # Standard Library 4 | import platform 5 | from pathlib import Path 6 | from datetime import datetime, timedelta 7 | from functools import wraps 8 | 9 | # Third Party 10 | from click import group, style, option, confirm, help_option 11 | 12 | # Project 13 | from hyperglass_agent.util import color_support 14 | from hyperglass_agent.cli.echo import error, label, warning 15 | from hyperglass_agent.cli.static import WARNING 16 | 17 | # Define working directory 18 | WORKING_DIR = Path(__file__).parent 19 | MODULE_DIR = WORKING_DIR / "hyperglass_agent" 20 | 21 | # Certificate parameters 22 | CERT_START = datetime.utcnow() 23 | CERT_END = datetime.utcnow() + timedelta(days=730) 24 | CERT_FILE = MODULE_DIR / "agent_cert.pem" 25 | KEY_FILE = MODULE_DIR / "agent_key.pem" 26 | 27 | DEFAULT_CERT_CN = platform.node() 28 | DEFAULT_CERT_O = "hyperglass" 29 | DEFAULT_CERT_SIZE = 4096 30 | DEFAULT_CERT_DURATION = 2 31 | DEFAULT_CERT_SHOW = False 32 | 33 | supports_color, _ = color_support() 34 | 35 | 36 | def _print_version(ctx, param, value): 37 | from hyperglass_agent import __version__ 38 | 39 | if not value or ctx.resilient_parsing: 40 | return 41 | label("hyperglass-agent version: {v}", v=__version__) 42 | ctx.exit() 43 | 44 | 45 | def catch(func): 46 | """Catch any unhandled exceptions.""" 47 | 48 | @wraps(func) 49 | def wrapper(*args, **kwargs): 50 | try: 51 | val = func(*args, **kwargs) 52 | except BaseException as err: 53 | error(err) 54 | return val 55 | 56 | return wrapper 57 | 58 | 59 | @group( 60 | help="hyperglass agent CLI", 61 | context_settings={"help_option_names": ["-h", "--help"], "color": supports_color}, 62 | ) 63 | @option( 64 | "-v", 65 | "--version", 66 | is_flag=True, 67 | callback=_print_version, 68 | expose_value=False, 69 | is_eager=True, 70 | help="hyperglass version", 71 | ) 72 | @help_option("-h", "--help", help="Show this help message") 73 | def cli(): 74 | """Click command group.""" 75 | pass 76 | 77 | 78 | @cli.command("secret", help="Generate Agent Secret") 79 | @option("-l", "--length", default=32, help="Character Length") 80 | @catch 81 | def _generate_secret(length): 82 | """Generate a secret for JWT encoding.""" 83 | from hyperglass_agent.cli.actions import generate_secret 84 | 85 | generate_secret(length) 86 | 87 | 88 | @cli.command("certificate", help="Generate SSL Certificate Key Pair") 89 | @option( 90 | "-cn", 91 | "--name", 92 | required=False, 93 | type=str, 94 | default=DEFAULT_CERT_CN, 95 | help="Common Name", 96 | ) 97 | @option( 98 | "-o", 99 | "--org", 100 | required=False, 101 | type=str, 102 | default=DEFAULT_CERT_O, 103 | help="Organization Name", 104 | ) 105 | @option( 106 | "-s", "--size", required=False, type=int, default=DEFAULT_CERT_SIZE, help="Key Size" 107 | ) 108 | @option( 109 | "-d", "--duration", required=False, type=int, default=2, help="Validity in Years" 110 | ) 111 | @option("-v", "--view-key", "show", is_flag=True, help="Show Private Key in CLI Output") 112 | @option("--get", is_flag=True, help="Get existing public key") 113 | @catch 114 | def _generate_cert( 115 | name: str, org: str, duration: int, size: int, show: bool, get: bool 116 | ): 117 | """Generate SSL certificate keypair.""" 118 | from hyperglass_agent.cli.actions import write_cert, find_app_path 119 | 120 | if get: 121 | app_path = find_app_path() 122 | cert_path = app_path / "agent_cert.pem" 123 | if not cert_path.exists(): 124 | warning("Certificate & key files have not been generated.") 125 | do_gen = confirm(style("Would you like to generate them now?", **WARNING)) 126 | if not do_gen: 127 | error("Certificate & key files do not yet exist.") 128 | else: 129 | write_cert(name=name, org=org, duration=duration, size=size, show=show) 130 | else: 131 | with cert_path.open("r") as f: 132 | cert = f.read() 133 | 134 | label(f"Public Key:\n\n{cert}") 135 | else: 136 | write_cert(name=name, org=org, duration=duration, size=size, show=show) 137 | 138 | 139 | @cli.command("send-certificate", help="Send this device's public key to hyperglass") 140 | @catch 141 | def _send_certificate(): 142 | """Send this device's public key to hyperglass.""" 143 | from hyperglass_agent.cli.actions import send_certificate 144 | 145 | send_certificate() 146 | 147 | 148 | @cli.command("start", help="Start the Web Server") 149 | def _start_web_server(): 150 | """Start the hyperglass agent.""" 151 | from hyperglass_agent.cli.actions import start_web_server 152 | 153 | start_web_server() 154 | 155 | 156 | @cli.command("setup", help="Run the setup wizard") 157 | @option( 158 | "--no-config", 159 | "config", 160 | is_flag=True, 161 | default=False, 162 | help="Don't regenerate config file", 163 | ) 164 | @option( 165 | "--no-certs", 166 | "certs", 167 | is_flag=True, 168 | default=False, 169 | help="Don't regenerate certificates", 170 | ) 171 | @option( 172 | "--no-systemd", 173 | "systemd", 174 | is_flag=True, 175 | default=False, 176 | help="Don't generate a systemd file", 177 | ) 178 | @option( 179 | "--no-send", 180 | "send", 181 | is_flag=True, 182 | default=False, 183 | help="Don't send the SSL certificate to hyperglass", 184 | ) 185 | @option( 186 | "--force", is_flag=True, default=False, help="Force regeneration of config file" 187 | ) 188 | @catch 189 | def _run_setup(config, certs, systemd, send, force): 190 | """Run setup wizard. 191 | 192 | Checks/creates installation directory, generates and writes 193 | certificates & keys, copies example/default configuration file. 194 | """ 195 | from hyperglass_agent.cli.actions import ( 196 | find_app_path, 197 | migrate_config, 198 | write_cert, 199 | make_systemd, 200 | send_certificate, 201 | generate_secret, 202 | ) 203 | 204 | find_app_path() 205 | 206 | secret = generate_secret() 207 | 208 | if not certs: 209 | write_cert( 210 | name=DEFAULT_CERT_CN, 211 | org=DEFAULT_CERT_O, 212 | duration=DEFAULT_CERT_DURATION, 213 | size=DEFAULT_CERT_SIZE, 214 | show=DEFAULT_CERT_SHOW, 215 | ) 216 | 217 | if not config: 218 | migrate_config(force=force, secret=secret) 219 | 220 | if not systemd: 221 | make_systemd() 222 | 223 | if not send: 224 | send_certificate() 225 | -------------------------------------------------------------------------------- /hyperglass_agent/cli/echo.py: -------------------------------------------------------------------------------- 1 | """Helper functions for CLI message printing.""" 2 | # Standard Library 3 | import re 4 | 5 | # Third Party 6 | from click import echo, style 7 | from inquirer import prompt 8 | from inquirer.themes import load_theme_from_dict 9 | 10 | # Project 11 | from hyperglass_agent.util import color_support 12 | from hyperglass_agent.cli.static import Message 13 | from hyperglass_agent.cli.exceptions import CliError 14 | 15 | 16 | def inquire(questions): 17 | """Run inquire.prompt() with a theme if supported.""" 18 | theme = None 19 | 20 | supports_color, num_colors = color_support() 21 | 22 | if supports_color: 23 | color_themes = { 24 | "8": { 25 | "Question": {"mark_color": "green", "brackets_color": "green"}, 26 | "List": { 27 | "selection_color": "green", 28 | "selection_cursor": ">", 29 | "unselected_color": "white", 30 | }, 31 | "Checkbox": { 32 | "selection_color": "blue", 33 | "selected_color": "green", 34 | "selection_icon": ">", 35 | "selected_icon": "X", 36 | "unselected_icon": "O", 37 | "unselected_color": "white", 38 | }, 39 | }, 40 | "256": { 41 | "Question": {"mark_color": "bright_green", "brackets_color": "green"}, 42 | "List": { 43 | "selection_color": "bold_green", 44 | "selection_cursor": "→", 45 | "unselected_color": "white", 46 | }, 47 | "Checkbox": { 48 | "selection_color": "bold_white", 49 | "selected_color": "bold_green", 50 | "selection_icon": "→", 51 | "selected_icon": "●", 52 | "unselected_icon": "◯", 53 | "unselected_color": "white", 54 | }, 55 | }, 56 | } 57 | color_theme = color_themes.get(num_colors) 58 | if color_theme is not None: 59 | theme = load_theme_from_dict(color_theme) 60 | 61 | return prompt(questions, theme=theme) 62 | 63 | 64 | def _base_formatter(state, text, callback, **kwargs): 65 | """Format text block, replace template strings with keyword arguments. 66 | 67 | Arguments: 68 | state {dict} -- Text format attributes 69 | label {dict} -- Keyword format attributes 70 | text {[type]} -- Text to format 71 | callback {function} -- Callback function 72 | 73 | Returns: 74 | {str|ClickException} -- Formatted output 75 | """ 76 | fmt = Message(state) 77 | 78 | if callback is None: 79 | callback = style 80 | 81 | for k, v in kwargs.items(): 82 | if not isinstance(v, str): 83 | v = str(v) 84 | kwargs[k] = style(v, **fmt.kw) 85 | 86 | if not isinstance(text, str): 87 | text = str(text) 88 | 89 | text_all = re.split(r"(\{\w+\})", text) 90 | text_all = [style(i, **fmt.msg) for i in text_all] 91 | text_all = [i.format(**kwargs) for i in text_all] 92 | 93 | if fmt.emoji: 94 | text_all.insert(0, fmt.emoji) 95 | 96 | text_fmt = "".join(text_all) 97 | 98 | return callback(text_fmt) 99 | 100 | 101 | def info(text, callback=echo, **kwargs): 102 | """Generate formatted informational text. 103 | 104 | Arguments: 105 | text {str} -- Text to format 106 | callback {callable} -- Callback function (default: {echo}) 107 | 108 | Returns: 109 | {str} -- Informational output 110 | """ 111 | return _base_formatter(state="info", text=text, callback=callback, **kwargs) 112 | 113 | 114 | def error(text, callback=CliError, **kwargs): 115 | """Generate formatted exception. 116 | 117 | Arguments: 118 | text {str} -- Text to format 119 | callback {callable} -- Callback function (default: {echo}) 120 | 121 | Raises: 122 | ClickException: Raised after formatting 123 | """ 124 | raise _base_formatter(state="error", text=text, callback=callback, **kwargs) 125 | 126 | 127 | def success(text, callback=echo, **kwargs): 128 | """Generate formatted success text. 129 | 130 | Arguments: 131 | text {str} -- Text to format 132 | callback {callable} -- Callback function (default: {echo}) 133 | 134 | Returns: 135 | {str} -- Success output 136 | """ 137 | return _base_formatter(state="success", text=text, callback=callback, **kwargs) 138 | 139 | 140 | def warning(text, callback=echo, **kwargs): 141 | """Generate formatted warning text. 142 | 143 | Arguments: 144 | text {str} -- Text to format 145 | callback {callable} -- Callback function (default: {echo}) 146 | 147 | Returns: 148 | {str} -- Warning output 149 | """ 150 | return _base_formatter(state="warning", text=text, callback=callback, **kwargs) 151 | 152 | 153 | def label(text, callback=echo, **kwargs): 154 | """Generate formatted info text with accented labels. 155 | 156 | Arguments: 157 | text {str} -- Text to format 158 | callback {callable} -- Callback function (default: {echo}) 159 | 160 | Returns: 161 | {str} -- Label output 162 | """ 163 | return _base_formatter(state="label", text=text, callback=callback, **kwargs) 164 | 165 | 166 | def status(text, callback=echo, **kwargs): 167 | """Generate formatted status text. 168 | 169 | Arguments: 170 | text {str} -- Text to format 171 | callback {callable} -- Callback function (default: {echo}) 172 | 173 | Returns: 174 | {str} -- Status output 175 | """ 176 | return _base_formatter(state="status", text=text, callback=callback, **kwargs) 177 | -------------------------------------------------------------------------------- /hyperglass_agent/cli/exceptions.py: -------------------------------------------------------------------------------- 1 | """CLI custom exceptions.""" 2 | 3 | # Third Party 4 | from click import ClickException, echo 5 | from click._compat import get_text_stderr 6 | 7 | 8 | class CliError(ClickException): 9 | """Custom exception to exclude the 'Error:' prefix from echos.""" 10 | 11 | def show(self, file=None): 12 | """Exclude 'Error:' prefix from raised exceptions.""" 13 | if file is None: 14 | file = get_text_stderr() 15 | echo(self.format_message()) 16 | -------------------------------------------------------------------------------- /hyperglass_agent/cli/static.py: -------------------------------------------------------------------------------- 1 | """Static string definitions.""" 2 | 3 | 4 | class Char: 5 | """Helper class for single-character strings.""" 6 | 7 | def __init__(self, char): 8 | """Set instance character.""" 9 | self.char = char 10 | 11 | def __getitem__(self, i): 12 | """Subscription returns the instance's character * n.""" 13 | return self.char * i 14 | 15 | def __str__(self): 16 | """Stringify the instance character.""" 17 | return str(self.char) 18 | 19 | def __repr__(self): 20 | """Stringify the instance character for representation.""" 21 | return str(self.char) 22 | 23 | def __add__(self, other): 24 | """Addition method for string concatenation.""" 25 | return str(self.char) + str(other) 26 | 27 | 28 | class Emoji: 29 | """Helper class for unicode emoji.""" 30 | 31 | BUTTERFLY = "\U0001F98B " 32 | CHECK = "\U00002705 " 33 | INFO = "\U00002755 " 34 | ERROR = "\U0000274C " 35 | WARNING = "\U000026A0\U0000FE0F " 36 | TOOLBOX = "\U0001F9F0 " 37 | NUMBERS = "\U0001F522 " 38 | FOLDED_HANDS = "\U0001F64F " 39 | ROCKET = "\U0001F680 " 40 | SPARKLES = "\U00002728 " 41 | PAPERCLIP = "\U0001F4CE " 42 | KEY = "\U0001F511 " 43 | LOCK = "\U0001F512 " 44 | CLAMP = "\U0001F5DC " 45 | BOOKS = "\U0001F4DA " 46 | 47 | 48 | WS = Char(" ") 49 | NL = Char("\n") 50 | CL = Char(":") 51 | E = Emoji() 52 | 53 | # Click Style Helpers 54 | SUCCESS = {"fg": "green", "bold": True} 55 | WARNING = {"fg": "yellow"} 56 | ERROR = {"fg": "red", "bold": True} 57 | LABEL = {"fg": "white"} 58 | INFO = {"fg": "blue", "bold": True} 59 | STATUS = {"fg": "black"} 60 | VALUE = {"fg": "magenta", "bold": True} 61 | CMD_HELP = {"fg": "white"} 62 | 63 | 64 | class Message: 65 | """Helper class for single-character strings.""" 66 | 67 | colors = { 68 | "warning": "yellow", 69 | "success": "green", 70 | "error": "red", 71 | "info": "blue", 72 | "status": "white", 73 | "label": "white", 74 | } 75 | label_colors = { 76 | "warning": "yellow", 77 | "success": "green", 78 | "error": "red", 79 | "info": "blue", 80 | "status": "white", 81 | "label": "magenta", 82 | } 83 | emojis = { 84 | "warning": E.WARNING, 85 | "success": E.CHECK, 86 | "error": E.ERROR, 87 | "info": E.INFO, 88 | "status": "", 89 | "label": "", 90 | } 91 | 92 | def __init__(self, state): 93 | """Set instance character.""" 94 | self.state = state 95 | self.color = self.colors[self.state] 96 | self.label_color = self.label_colors[self.state] 97 | 98 | @property 99 | def msg(self): 100 | """Click style attributes for message text.""" 101 | return {"fg": self.color} 102 | 103 | @property 104 | def kw(self): 105 | """Click style attributes for keywords.""" 106 | return {"fg": self.label_color, "bold": True, "underline": True} 107 | 108 | @property 109 | def emoji(self): 110 | """Match emoji from state.""" 111 | return self.emojis[self.state] 112 | 113 | def __repr__(self): 114 | """Stringify the instance character for representation.""" 115 | return "Message(msg={m}, kw={k}, emoji={e})".format( 116 | m=self.msg, k=self.kw, e=self.emoji 117 | ) 118 | -------------------------------------------------------------------------------- /hyperglass_agent/config.py: -------------------------------------------------------------------------------- 1 | """Read YAML config file, validate, and set defaults.""" 2 | 3 | # Standard Library 4 | import os 5 | from pathlib import Path 6 | 7 | # Third Party 8 | import yaml 9 | from pydantic import ValidationError 10 | 11 | # Project 12 | from hyperglass_agent.log import log, set_log_level, enable_file_logging 13 | from hyperglass_agent.util import set_app_path 14 | from hyperglass_agent.exceptions import ConfigError, ConfigInvalid 15 | from hyperglass_agent.models.general import General 16 | from hyperglass_agent.models.commands import Commands 17 | 18 | if os.environ.get("hyperglass_agent_directory") is None: 19 | set_app_path(required=True) 20 | 21 | try: 22 | APP_PATH = Path(os.environ["hyperglass_agent_directory"]) 23 | except KeyError: 24 | raise ConfigError( 25 | "No application path was found. Please consult the setup documentation." 26 | ) 27 | 28 | CONFIG_FILE = APP_PATH / "config.yaml" 29 | 30 | 31 | def _get_config(): 32 | """Read config file & load YAML to dict. 33 | 34 | Raises: 35 | ConfigError: Raised if file is not found. 36 | ConfigError: Raised if there is a YAML parsing error. 37 | 38 | Returns: 39 | {dict} -- Loaded config 40 | """ 41 | try: 42 | with CONFIG_FILE.open("r") as file: 43 | config_file = file.read() 44 | raw_config = yaml.safe_load(config_file) 45 | 46 | except FileNotFoundError: 47 | raise ConfigError("Config file not found.") from None 48 | 49 | except (yaml.YAMLError, yaml.MarkedYAMLError) as yaml_error: 50 | raise ConfigError(yaml_error) from None 51 | return raw_config 52 | 53 | 54 | _raw_config = _get_config() 55 | 56 | # Read raw debug value from config to enable debugging quickly. 57 | set_log_level(logger=log, debug=_raw_config.get("debug", True)) 58 | 59 | try: 60 | _commands = _raw_config.pop("commands", None) 61 | _user_config = General(**_raw_config) 62 | 63 | if _commands is not None: 64 | _user_commands = Commands.import_params(mode=_user_config.mode, **_commands) 65 | else: 66 | _user_commands = Commands.import_params(mode=_user_config.mode) 67 | 68 | except ValidationError as validation_errors: 69 | _errors = validation_errors.errors() 70 | for error in _errors: 71 | raise ConfigInvalid( 72 | field=": ".join([str(item) for item in error["loc"]]), 73 | error_msg=error["msg"], 74 | ) 75 | 76 | params = _user_config 77 | commands = _user_commands 78 | 79 | # Re-evaluate debug state after config is validated 80 | set_log_level(logger=log, debug=params.debug) 81 | 82 | if params.logging is not False: 83 | # Set up file logging once configuration parameters are initialized. 84 | enable_file_logging( 85 | logger=log, 86 | log_directory=params.logging.directory, 87 | log_format=params.logging.format, 88 | log_max_size=params.logging.max_size, 89 | ) 90 | 91 | log.debug(params.json()) 92 | log.debug(commands.json()) 93 | -------------------------------------------------------------------------------- /hyperglass_agent/console.py: -------------------------------------------------------------------------------- 1 | """hyperglass-agent CLI entrypoint.""" 2 | 3 | # Project 4 | from hyperglass_agent.cli.commands import cli 5 | 6 | if __name__ == "__main__": 7 | cli() 8 | -------------------------------------------------------------------------------- /hyperglass_agent/constants.py: -------------------------------------------------------------------------------- 1 | """Constant definitions used throughout the application.""" 2 | 3 | # Standard Library 4 | import sys 5 | from pathlib import Path 6 | 7 | SUPPORTED_NOS = ("frr", "bird") 8 | 9 | DEFAULT_MODE = "frr" 10 | 11 | APP_PATHS = (Path.home() / "hyperglass-agent", Path("/etc/hyperglass-agent")) 12 | 13 | SUPPORTED_QUERY = ("bgp_route", "bgp_aspath", "bgp_community", "ping", "traceroute") 14 | 15 | AGENT_QUERY = ("bgp_route", "bgp_aspath", "bgp_community") 16 | 17 | OS_QUERY = ("ping", "traceroute") 18 | 19 | AFI_DISPLAY_MAP = { 20 | "ipv4_default": "IPv4", 21 | "ipv6_default": "IPv6", 22 | "ipv4_vpn": "IPv4 - {vrf}", 23 | "ipv6_vpn": "IPv6 - {vrf}", 24 | } 25 | 26 | LOG_FMT = ( 27 | "[{level}] {time:YYYYMMDD} | {time:HH:mm:ss} {name} " 28 | "| {function} {message}" 29 | ) 30 | LOG_LEVELS = [ 31 | {"name": "DEBUG", "no": 10, "color": ""}, 32 | {"name": "INFO", "no": 20, "color": ""}, 33 | {"name": "SUCCESS", "no": 25, "color": ""}, 34 | {"name": "WARNING", "no": 30, "color": ""}, 35 | {"name": "ERROR", "no": 40, "color": ""}, 36 | {"name": "CRITICAL", "no": 50, "color": ""}, 37 | ] 38 | 39 | LOG_HANDLER = {"sink": sys.stdout, "format": LOG_FMT, "level": "INFO"} 40 | -------------------------------------------------------------------------------- /hyperglass_agent/example_config.yaml: -------------------------------------------------------------------------------- 1 | # debug: false 2 | # mode: frr 3 | # listen_address: '::1' 4 | # port: 8443 5 | # valid_duration: 60 6 | # not_found_message: "{target} not found. ({afi})" 7 | secret: null 8 | ssl: 9 | enable: true 10 | -------------------------------------------------------------------------------- /hyperglass_agent/exceptions.py: -------------------------------------------------------------------------------- 1 | """Module specific exception classes.""" 2 | 3 | # Standard Library 4 | import json as _json 5 | 6 | # Project 7 | from hyperglass_agent.log import log 8 | 9 | 10 | class HyperglassAgentError(Exception): 11 | """Base exception class for all hyperglass-agent errors.""" 12 | 13 | def __init__(self, message="", code=500, keywords=None): 14 | """Initialize the app's base exception class. 15 | 16 | Keyword Arguments: 17 | message {str} -- Error message (default: {""}) 18 | code {int} -- HTTP Status Code (default: {500}) 19 | keywords {list} -- 'Important' keywords (default: {None}) 20 | """ 21 | self._message = message 22 | self._code = code 23 | self._keywords = keywords or [] 24 | log.critical(self.__repr__()) 25 | 26 | def __str__(self): 27 | """Return the instance's error message. 28 | 29 | Returns: 30 | {str} -- Error Message 31 | """ 32 | return self._message 33 | 34 | def __repr__(self): 35 | """Return the instance's code & error message in a string. 36 | 37 | Returns: 38 | {str} -- Error message with code 39 | """ 40 | return f"{self._code}: {self._message}" 41 | 42 | def __dict__(self): 43 | """Return the instance's attributes as a dictionary. 44 | 45 | Returns: 46 | {dict} -- Exception attributes in dict 47 | """ 48 | return { 49 | "message": self._message, 50 | "code": self._code, 51 | "keywords": self._keywords, 52 | } 53 | 54 | def json(self): 55 | """Return the instance's attributes as a JSON object. 56 | 57 | Returns: 58 | {str} -- Exception attributes as JSON 59 | """ 60 | return _json.dumps( 61 | {"message": self._message, "code": self._code, "keywords": self._keywords} 62 | ) 63 | 64 | @property 65 | def code(self): 66 | """Return the instance's `code` attribute. 67 | 68 | Returns: 69 | {int} -- HTTP Status Code 70 | """ 71 | return self._code 72 | 73 | @property 74 | def message(self): 75 | """Return the instance's `message` attribute. 76 | 77 | Returns: 78 | {str} -- Error Message 79 | """ 80 | return self._message 81 | 82 | @property 83 | def keywords(self): 84 | """Return the instance's `keywords` attribute. 85 | 86 | Returns: 87 | {list} -- Keywords List 88 | """ 89 | return self._keywords 90 | 91 | 92 | class ConfigInvalid(HyperglassAgentError): 93 | """Raised when a config item fails type or option validation.""" 94 | 95 | def __init__(self, **kwargs): 96 | """Format a pre-defined message with passed keyword arguments.""" 97 | self._message = 'The value field "{field}" is invalid: {error_msg}'.format( 98 | **kwargs 99 | ) 100 | self._keywords = list(kwargs.values()) 101 | super().__init__(message=self._message, keywords=self._keywords) 102 | 103 | 104 | class _UnformattedHyperglassError(HyperglassAgentError): 105 | """Base exception class for freeform error messages.""" 106 | 107 | _code = 500 108 | 109 | def __init__(self, unformatted_msg="undefined error", code=None, **kwargs): 110 | """Format error message with keyword arguments. 111 | 112 | Keyword Arguments: 113 | message {str} -- Error message (default: {""}) 114 | alert {str} -- Error severity (default: {"warning"}) 115 | keywords {list} -- 'Important' keywords (default: {None}) 116 | """ 117 | self._message = unformatted_msg.format(**kwargs) 118 | self._alert = code or self._code 119 | self._keywords = list(kwargs.values()) 120 | super().__init__( 121 | message=self._message, code=self._code, keywords=self._keywords 122 | ) 123 | 124 | 125 | class ConfigError(_UnformattedHyperglassError): 126 | """Raised for generic user-config issues.""" 127 | 128 | 129 | class QueryError(_UnformattedHyperglassError): 130 | """Raised when a received query is invalid according to the query model.""" 131 | 132 | _code = 400 133 | 134 | 135 | class ResponseEmpty(_UnformattedHyperglassError): 136 | """Raised when a received query is valid, but the response is empty.""" 137 | 138 | _code = 204 139 | 140 | 141 | class ExecutionError(_UnformattedHyperglassError): 142 | """Raised when an error occurs during shell command execution.""" 143 | 144 | _code = 503 145 | 146 | 147 | class SecurityError(_UnformattedHyperglassError): 148 | """Raised when a JWT decoding error occurs.""" 149 | 150 | _code = 500 151 | -------------------------------------------------------------------------------- /hyperglass_agent/execute.py: -------------------------------------------------------------------------------- 1 | """Construct, execute, parse, and return the requested query.""" 2 | 3 | # Standard Library 4 | import asyncio 5 | import operator 6 | 7 | # Project 8 | from hyperglass_agent.log import log 9 | from hyperglass_agent.config import params, commands 10 | from hyperglass_agent.exceptions import ResponseEmpty, ExecutionError 11 | from hyperglass_agent.nos_utils.frr import parse_frr_output 12 | from hyperglass_agent.nos_utils.bird import ( 13 | parse_bird_output, 14 | format_bird_bgp_aspath, 15 | format_bird_bgp_community, 16 | ) 17 | 18 | target_format_map = { 19 | "bird": { 20 | "bgp_community": format_bird_bgp_community, 21 | "bgp_aspath": format_bird_bgp_aspath, 22 | }, 23 | "frr": {}, 24 | } 25 | parser_map = {"bird": parse_bird_output, "frr": parse_frr_output} 26 | 27 | 28 | async def run_query(query): 29 | """Execute validated query & parse the results. 30 | 31 | Arguments: 32 | query {object} -- Validated query object 33 | 34 | Raises: 35 | ExecutionError: If stderr exists 36 | 37 | Returns: 38 | {str} -- Parsed output string 39 | """ 40 | log.debug(f"Query: {query}") 41 | 42 | parser = parser_map[params.mode] 43 | 44 | target_formatter = target_format_map[params.mode].get(query.query_type) 45 | 46 | if target_formatter is not None: 47 | query.target = target_formatter(query.target) 48 | 49 | command_raw = operator.attrgetter( 50 | ".".join([params.mode, query.afi, query.query_type]) 51 | )(commands) 52 | 53 | command = command_raw.format(**query.dict()) 54 | 55 | log.debug(f"Formatted Command: {command}") 56 | 57 | proc = await asyncio.create_subprocess_shell( 58 | command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE 59 | ) 60 | stdout, stderr = await proc.communicate() 61 | 62 | if stderr: 63 | err_output = stderr.decode() 64 | log.error(err_output) 65 | raise ExecutionError(err_output) 66 | 67 | output = "" 68 | 69 | if stdout: 70 | log.debug(f"Parser: {parser.__name__}") 71 | 72 | raw_output = stdout.decode() 73 | output += await parser( 74 | raw=raw_output, query_data=query, not_found=params.not_found_message 75 | ) 76 | return output 77 | 78 | if not output and proc.returncode == 0: 79 | raise ResponseEmpty("Command ran successfully, but the response was empty.") 80 | 81 | return output 82 | -------------------------------------------------------------------------------- /hyperglass_agent/log.py: -------------------------------------------------------------------------------- 1 | """Logging instance setup & configuration.""" 2 | 3 | # Standard Library 4 | import os 5 | import sys 6 | from datetime import datetime 7 | 8 | # Third Party 9 | from loguru import logger as _loguru_logger 10 | 11 | _LOG_FMT = ( 12 | "[{level}] {time:YYYYMMDD} {time:HH:mm:ss} | {name}:" 13 | "{line} | {function} {message}" 14 | ) 15 | _LOG_LEVELS = [ 16 | {"name": "TRACE", "no": 5, "color": ""}, 17 | {"name": "DEBUG", "no": 10, "color": ""}, 18 | {"name": "INFO", "no": 20, "color": ""}, 19 | {"name": "SUCCESS", "no": 25, "color": ""}, 20 | {"name": "WARNING", "no": 30, "color": ""}, 21 | {"name": "ERROR", "no": 40, "color": ""}, 22 | {"name": "CRITICAL", "no": 50, "color": ""}, 23 | ] 24 | 25 | 26 | def base_logger(): 27 | """Initialize hyperglass logging instance.""" 28 | _loguru_logger.remove() 29 | _loguru_logger.add(sys.stdout, format=_LOG_FMT, level="INFO", enqueue=True) 30 | _loguru_logger.configure(levels=_LOG_LEVELS) 31 | return _loguru_logger 32 | 33 | 34 | log = base_logger() 35 | 36 | 37 | def set_log_level(logger, debug): 38 | """Set log level based on debug state.""" 39 | if debug: 40 | os.environ["HYPERGLASS_AGENT_LOG_LEVEL"] = "DEBUG" 41 | logger.remove() 42 | logger.add(sys.stdout, format=_LOG_FMT, level="DEBUG", enqueue=True) 43 | logger.configure(levels=_LOG_LEVELS) 44 | 45 | if debug: 46 | logger.debug("Debugging enabled") 47 | return True 48 | 49 | 50 | def enable_file_logging(logger, log_directory, log_format, log_max_size): 51 | """Set up file-based logging from configuration parameters.""" 52 | 53 | if log_format == "json": 54 | log_file_name = "hyperglass-agent.log.json" 55 | structured = True 56 | else: 57 | log_file_name = "hyperglass-agent.log" 58 | structured = False 59 | 60 | log_file = log_directory / log_file_name 61 | 62 | if log_format == "text": 63 | now_str = "hyperglass-agent logs for " + datetime.utcnow().strftime( 64 | "%B %d, %Y beginning at %H:%M:%S UTC" 65 | ) 66 | now_str_y = len(now_str) + 6 67 | now_str_x = len(now_str) + 4 68 | log_break = ( 69 | "#" * now_str_y, 70 | "\n#" + " " * now_str_x + "#\n", 71 | "# ", 72 | now_str, 73 | " #", 74 | "\n#" + " " * now_str_x + "#\n", 75 | "#" * now_str_y, 76 | ) 77 | 78 | with log_file.open("a+") as lf: 79 | lf.write(f'\n\n{"".join(log_break)}\n\n') 80 | 81 | logger.add(log_file, rotation=log_max_size, serialize=structured, enqueue=True) 82 | 83 | logger.debug("Logging to file enabled") 84 | 85 | return True 86 | -------------------------------------------------------------------------------- /hyperglass_agent/models/__init__.py: -------------------------------------------------------------------------------- 1 | """Pydantic config models.""" 2 | -------------------------------------------------------------------------------- /hyperglass_agent/models/_formatters.py: -------------------------------------------------------------------------------- 1 | """Various formatting functions for supported platforms.""" 2 | 3 | 4 | def format_bird(ip_version, bird_version, cmd): 5 | """Prefixes BIRD command with the appropriate BIRD CLI command. 6 | 7 | Arguments: 8 | ip_version {int} -- IPv4/IPv6 9 | bird_version {int} -- BIRD version 10 | cmd {str} -- Unprefixed command 11 | 12 | Returns: 13 | {str} -- Prefixed command 14 | """ 15 | cmd_prefix = "birdc" 16 | 17 | if bird_version == 1 and ip_version == 6: 18 | cmd_prefix = "birdc6" 19 | 20 | command = f'{cmd_prefix} "{cmd}"' 21 | 22 | return command 23 | 24 | 25 | def format_frr(cmd): 26 | """Prefixes FRR command with the appropriate vtysh prefix. 27 | 28 | Arguments: 29 | cmd {str} -- Unprefixed command 30 | 31 | Returns: 32 | {str} -- Prefixed command 33 | """ 34 | return f'vtysh -uc "{cmd}"' 35 | -------------------------------------------------------------------------------- /hyperglass_agent/models/_utils.py: -------------------------------------------------------------------------------- 1 | """Utility Functions for Pydantic Models.""" 2 | 3 | # Standard Library 4 | import re 5 | 6 | # Third Party 7 | import pydantic 8 | 9 | 10 | class StrictBytes(bytes): 11 | """Custom data type for a strict byte string. 12 | 13 | Used for validatnig the encoded JWT request payload. 14 | """ 15 | 16 | @classmethod 17 | def __get_validators__(cls): 18 | """Yield Pydantic validator function. 19 | 20 | See: https://pydantic-docs.helpmanual.io/usage/types/#custom-data-types 21 | 22 | Yields: 23 | {function} -- Validator 24 | """ 25 | yield cls.validate 26 | 27 | @classmethod 28 | def validate(cls, value): 29 | """Validate type. 30 | 31 | Arguments: 32 | value {Any} -- Pre-validated input 33 | 34 | Raises: 35 | TypeError: Raised if value is not bytes 36 | 37 | Returns: 38 | {object} -- Instantiated class 39 | """ 40 | if not isinstance(value, bytes): 41 | raise TypeError("bytes required") 42 | return cls() 43 | 44 | def __repr__(self): 45 | """Return representation of object. 46 | 47 | Returns: 48 | {str} -- Representation 49 | """ 50 | return f"StrictBytes({super().__repr__()})" 51 | 52 | 53 | def clean_name(_name): 54 | """Remove unsupported characters from field names. 55 | 56 | Converts any "desirable" seperators to underscore, then removes all 57 | characters that are unsupported in Python class variable names. 58 | Also removes leading numbers underscores. 59 | 60 | Arguments: 61 | _name {str} -- Initial field name 62 | 63 | Returns: 64 | {str} -- Cleaned field name 65 | """ 66 | _replaced = re.sub(r"[\-|\.|\@|\~|\:\/|\s]", "_", _name) 67 | _scrubbed = "".join(re.findall(r"([a-zA-Z]\w+|\_+)", _replaced)) 68 | return _scrubbed.lower() 69 | 70 | 71 | class HyperglassModel(pydantic.BaseModel): 72 | """Base model for all hyperglass configuration models.""" 73 | 74 | pass 75 | 76 | class Config: 77 | """Default Pydantic configuration. 78 | 79 | See https://pydantic-docs.helpmanual.io/usage/model_config 80 | """ 81 | 82 | validate_all = True 83 | extra = "forbid" 84 | validate_assignment = True 85 | -------------------------------------------------------------------------------- /hyperglass_agent/models/commands.py: -------------------------------------------------------------------------------- 1 | """Command definitions for supported platforms.""" 2 | 3 | # Third Party 4 | from pydantic import conint, root_validator 5 | 6 | # Project 7 | from hyperglass_agent.constants import AGENT_QUERY 8 | from hyperglass_agent.models._utils import HyperglassModel 9 | from hyperglass_agent.nos_utils.bird import get_bird_version 10 | from hyperglass_agent.models._formatters import format_frr, format_bird 11 | 12 | 13 | class Command(HyperglassModel): 14 | """Class model for non-default dual afi commands.""" 15 | 16 | bgp_route: str = "" 17 | bgp_aspath: str = "" 18 | bgp_community: str = "" 19 | ping: str = "" 20 | traceroute: str = "" 21 | 22 | 23 | class FRRCommand(Command): 24 | """Class model for FRRouting commands.""" 25 | 26 | @root_validator 27 | def prefix_frr(cls, values): 28 | """Prefix command if needed. 29 | 30 | Returns: 31 | {str} -- Prefixed command 32 | """ 33 | for cmd in AGENT_QUERY: 34 | if "vtysh" not in values[cmd]: 35 | values[cmd] = format_frr(values[cmd]) 36 | return values 37 | 38 | 39 | class FRR(HyperglassModel): 40 | """Class model for default FRRouting commands.""" 41 | 42 | class VPNIPv4(FRRCommand): 43 | """Default commands for dual afi commands.""" 44 | 45 | bgp_community: str = "show bgp vrf {vrf} ipv4 unicast community {target}" 46 | bgp_aspath: str = "show bgp vrf {vrf} ipv4 unicast regexp {target}" 47 | bgp_route: str = "show bgp vrf {vrf} ipv4 unicast {target}" 48 | ping: str = "ping -4 -c 5 -I {source} {target}" 49 | traceroute: str = "traceroute -4 -w 1 -q 1 -s {source} {target}" 50 | 51 | class VPNIPv6(FRRCommand): 52 | """Default commands for dual afi commands.""" 53 | 54 | bgp_community: str = "show bgp vrf {vrf} ipv6 unicast community {target}" 55 | bgp_aspath: str = "show bgp vrf {vrf} ipv6 unicast regexp {target}" 56 | bgp_route: str = "show bgp vrf {vrf} ipv6 unicast {target}" 57 | ping: str = "ping -6 -c 5 -I {source} {target}" 58 | traceroute: str = "traceroute -6 -w 1 -q 1 -s {source} {target}" 59 | 60 | class IPv4(FRRCommand): 61 | """Default commands for ipv4 commands.""" 62 | 63 | bgp_community: str = "show bgp ipv4 unicast community {target}" 64 | bgp_aspath: str = "show bgp ipv4 unicast regexp {target}" 65 | bgp_route: str = "show bgp ipv4 unicast {target}" 66 | ping: str = "ping -4 -c 5 -I {source} {target}" 67 | traceroute: str = "traceroute -4 -w 1 -q 1 -s {source} {target}" 68 | 69 | class IPv6(FRRCommand): 70 | """Default commands for ipv6 commands.""" 71 | 72 | bgp_community: str = "show bgp ipv6 unicast community {target}" 73 | bgp_aspath: str = "show bgp ipv6 unicast regexp {target}" 74 | bgp_route: str = "show bgp ipv6 unicast {target}" 75 | ping: str = "ping -6 -c 5 -I {source} {target}" 76 | traceroute: str = "traceroute -6 -w 1 -q 1 -s {source} {target}" 77 | 78 | ipv4_default: IPv4 = IPv4() 79 | ipv6_default: IPv6 = IPv6() 80 | ipv4_vpn: VPNIPv4 = VPNIPv4() 81 | ipv6_vpn: VPNIPv6 = VPNIPv6() 82 | 83 | 84 | class BIRDCommand(Command): 85 | """Class model for BIRD commands.""" 86 | 87 | bird_version: int 88 | ip_version: int 89 | 90 | @root_validator 91 | def prefix_bird(cls, values): 92 | """Prefix command if needed. 93 | 94 | Returns: 95 | {str} -- Prefixed command 96 | """ 97 | for cmd in AGENT_QUERY: 98 | if "birdc" not in values[cmd]: 99 | values[cmd] = format_bird( 100 | values["ip_version"], values["bird_version"], values[cmd], 101 | ) 102 | return values 103 | 104 | 105 | class BIRD(HyperglassModel): 106 | """Class model for default BIRD commands.""" 107 | 108 | class VPNIPv4(BIRDCommand): 109 | """Default dual AFI commands.""" 110 | 111 | bgp_community: str = "show route all where {target} ~ bgp_community" 112 | bgp_aspath: str = "show route all where bgp_path ~ {target}" 113 | bgp_route: str = "show route all where {target} ~ net" 114 | ping: str = "ping -4 -c 5 -I {source} {target}" 115 | traceroute: str = "traceroute -4 -w 1 -q 1 -s {source} {target}" 116 | 117 | class VPNIPv6(BIRDCommand): 118 | """Default dual AFI commands.""" 119 | 120 | bgp_community: str = "show route all where {target} ~ bgp_community" 121 | bgp_aspath: str = "show route all where bgp_path ~ {target}" 122 | bgp_route: str = "show route all where {target} ~ net" 123 | ping: str = "ping -6 -c 5 -I {source} {target}" 124 | traceroute: str = "traceroute -6 -w 1 -q 1 -s {source} {target}" 125 | 126 | class IPv4(BIRDCommand): 127 | """Default IPv4 commands.""" 128 | 129 | bgp_community: str = "show route all where {target} ~ bgp_community" 130 | bgp_aspath: str = "show route all where bgp_path ~ {target}" 131 | bgp_route: str = "show route all where {target} ~ net" 132 | ping: str = "ping -4 -c 5 -I {source} {target}" 133 | traceroute: str = "traceroute -4 -w 1 -q 1 -s {source} {target}" 134 | 135 | class IPv6(BIRDCommand): 136 | """Default IPv6 commands.""" 137 | 138 | bgp_community: str = "show route all where {target} ~ bgp_community" 139 | bgp_aspath: str = "show route all where bgp_path ~ {target}" 140 | bgp_route: str = "show route all where {target} ~ net" 141 | ping: str = "ping -6 -c 5 -I {source} {target}" 142 | traceroute: str = "traceroute -6 -w 1 -q 1 -s {source} {target}" 143 | 144 | bird_version: conint(ge=1, le=2) = 2 145 | ipv4_default: IPv4 = IPv4(ip_version=4, bird_version=bird_version) 146 | ipv6_default: IPv6 = IPv6(ip_version=6, bird_version=bird_version) 147 | ipv4_vpn: VPNIPv4 = VPNIPv4(ip_version=4, bird_version=bird_version) 148 | ipv6_vpn: VPNIPv6 = VPNIPv6(ip_version=6, bird_version=bird_version) 149 | 150 | 151 | class Commands(HyperglassModel): 152 | """Base class for all commands.""" 153 | 154 | @classmethod 155 | def import_params(cls, mode, input_params=None): 156 | """Import YAML config, dynamically set attributes for each NOS class. 157 | 158 | Arguments: 159 | mode {str} -- Agent mode 160 | 161 | Keyword Arguments: 162 | input_params {dict} -- Overidden commands (default: {None}) 163 | 164 | Returns: 165 | {object} -- Validated command object 166 | """ 167 | cmd_kwargs = {} 168 | if mode == "bird": 169 | bird_version = get_bird_version() 170 | cmd_kwargs.update({"bird_version": bird_version}) 171 | 172 | obj = Commands() 173 | 174 | if input_params is not None: 175 | for (nos, cmds) in input_params.items(): 176 | setattr(Commands, nos, Command(**cmd_kwargs, **cmds)) 177 | return obj 178 | 179 | bird: BIRD = BIRD() 180 | frr: FRR = FRR() 181 | -------------------------------------------------------------------------------- /hyperglass_agent/models/general.py: -------------------------------------------------------------------------------- 1 | """Validate application config parameters.""" 2 | 3 | # Standard Library 4 | import os 5 | from typing import Union, Optional 6 | from pathlib import Path 7 | 8 | # Third Party 9 | from pydantic import ( 10 | ByteSize, 11 | FilePath, 12 | SecretStr, 13 | StrictInt, 14 | StrictStr, 15 | StrictBool, 16 | DirectoryPath, 17 | IPvAnyAddress, 18 | constr, 19 | validator, 20 | ) 21 | 22 | # Project 23 | from hyperglass_agent.constants import DEFAULT_MODE, SUPPORTED_NOS 24 | from hyperglass_agent.exceptions import ConfigError 25 | from hyperglass_agent.models._utils import HyperglassModel 26 | 27 | APP_PATH = Path(os.environ["hyperglass_agent_directory"]) 28 | DEFAULT_LOG_DIR = Path("/tmp/") # noqa: S108 29 | 30 | 31 | class Ssl(HyperglassModel): 32 | """Validate SSL config parameters.""" 33 | 34 | enable: StrictBool = True 35 | cert: Optional[FilePath] 36 | key: Optional[FilePath] 37 | 38 | @validator("cert") 39 | def validate_cert(cls, value, values): 40 | """Pydantic validator: set default cert path if ssl is enabled. 41 | 42 | Arguments: 43 | value {Path|None} -- Path to cert file 44 | values {dict} -- Other values 45 | 46 | Returns: 47 | {Path} -- Path to cert file 48 | """ 49 | cert_path = APP_PATH / "agent_cert.pem" 50 | if values["enable"] and value is None: 51 | if not cert_path.exists(): 52 | raise ValueError(f"{str(cert_path)} does not exist.") 53 | value = cert_path 54 | return value 55 | 56 | @validator("key") 57 | def validate_key(cls, value, values): 58 | """Pydantic validator: set default key path if ssl is enabled. 59 | 60 | Arguments: 61 | value {Path|None} -- Path to key file 62 | values {dict} -- Other values 63 | 64 | Returns: 65 | {Path} -- Path to key file 66 | """ 67 | key_path = APP_PATH / "agent_key.pem" 68 | 69 | if values["enable"] and value is None: 70 | if not key_path.exists(): 71 | raise ValueError(f"{str(key_path)} does not exist.") 72 | value = key_path 73 | return value 74 | 75 | 76 | class Logging(HyperglassModel): 77 | """Logging configuration.""" 78 | 79 | directory: Union[DirectoryPath, StrictBool] = DEFAULT_LOG_DIR 80 | format: constr(regex=r"(text|json)") = "text" 81 | max_size: ByteSize = "50MB" 82 | 83 | @validator("directory") 84 | def validate_directory(cls, value): 85 | """Require the only boolean value to be false.""" 86 | if value is True: 87 | value = DEFAULT_LOG_DIR 88 | return value 89 | 90 | 91 | class General(HyperglassModel): 92 | """Validate config parameters.""" 93 | 94 | debug: StrictBool = False 95 | listen_address: IPvAnyAddress = "0.0.0.0" # noqa: S104 96 | ssl: Ssl = Ssl() 97 | logging: Logging = Logging() 98 | port: StrictInt = None 99 | mode: StrictStr = DEFAULT_MODE 100 | secret: SecretStr 101 | valid_duration: StrictInt = 60 102 | not_found_message: StrictStr = "{target} not found. ({afi})" 103 | 104 | @validator("port", pre=True, always=True) 105 | def validate_port(cls, value, values): 106 | """Pydantic validator: set default port based on SSL state. 107 | 108 | Arguments: 109 | value {int|None} -- Port 110 | values {dict} -- Other values 111 | 112 | Returns: 113 | {int} -- Port 114 | """ 115 | if value is None and values["ssl"].enable: 116 | value = 8443 117 | elif value is None and not values["ssl"].enable: 118 | value = 8080 119 | return value 120 | 121 | @validator("mode") 122 | def validate_mode(cls, value): 123 | """Pydantic validator: validate mode is supported. 124 | 125 | Raises: 126 | ConfigError: Raised if mode is not supported. 127 | 128 | Returns: 129 | {str} -- Sets mode attribute if valid. 130 | """ 131 | if value not in SUPPORTED_NOS: 132 | raise ConfigError( 133 | f"mode must be one of '{', '.join(SUPPORTED_NOS)}'. Received '{value}'" 134 | ) 135 | return value 136 | -------------------------------------------------------------------------------- /hyperglass_agent/models/request.py: -------------------------------------------------------------------------------- 1 | """Validate the raw JSON request data.""" 2 | 3 | # Standard Library 4 | from typing import Union, Optional 5 | 6 | # Third Party 7 | from pydantic import BaseModel, StrictStr, IPvAnyAddress, validator 8 | 9 | # Project 10 | from hyperglass_agent.constants import SUPPORTED_QUERY 11 | from hyperglass_agent.exceptions import QueryError 12 | from hyperglass_agent.models._utils import StrictBytes 13 | 14 | 15 | class Request(BaseModel): 16 | """Validate and serialize raw request.""" 17 | 18 | query_type: str 19 | vrf: str 20 | afi: str 21 | source: Optional[IPvAnyAddress] 22 | target: str 23 | 24 | @validator("query_type") 25 | def validate_query_type(cls, value): # noqa: N805 26 | """Pydantic validator: validate that query type is supported. 27 | 28 | Raises: 29 | QueryError: Raised if the received query type is not supported. 30 | 31 | Returns: 32 | {str} -- Set query_type attribute if valid. 33 | """ 34 | if value not in SUPPORTED_QUERY: 35 | raise QueryError("Query Type '{query_type}' is Invalid", query_type=value) 36 | return value 37 | 38 | 39 | class EncodedRequest(BaseModel): 40 | """Validate encoded request.""" 41 | 42 | encoded: Union[StrictStr, StrictBytes] 43 | -------------------------------------------------------------------------------- /hyperglass_agent/nos_utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Utilities for supported platforms.""" 2 | -------------------------------------------------------------------------------- /hyperglass_agent/nos_utils/bird.py: -------------------------------------------------------------------------------- 1 | """Various BIRD Internet Routing Daemon (BIRD) utilities.""" 2 | 3 | # Standard Library 4 | import re 5 | import asyncio 6 | 7 | # Project 8 | from hyperglass_agent.log import log 9 | from hyperglass_agent.util import top_level_async 10 | from hyperglass_agent.constants import AFI_DISPLAY_MAP 11 | from hyperglass_agent.exceptions import ExecutionError 12 | 13 | 14 | @top_level_async 15 | async def get_bird_version(): 16 | """Get BIRD version from command line. 17 | 18 | Raises: 19 | ExecutionError: Raised when `birdc` is not found on the system. 20 | ExecutionError: Raised when the output is unreadable or contains errors. 21 | 22 | Returns: 23 | {int} -- Major BIRD version. 24 | """ 25 | proc = await asyncio.create_subprocess_shell( 26 | cmd="bird --version", 27 | stdout=asyncio.subprocess.PIPE, 28 | stderr=asyncio.subprocess.PIPE, 29 | ) 30 | stdout, stderr = await proc.communicate() 31 | 32 | if stdout: 33 | raw_output = stdout.decode("utf-8") 34 | 35 | if stderr and b"BIRD version" in stderr: 36 | raw_output = stderr.decode("utf-8") 37 | 38 | elif stderr and b"command not found" in stderr: 39 | raise ExecutionError( 40 | ( 41 | "BIRD mode is configured, but bird does not appear to be " 42 | f'installed: {stderr.decode("utf-8")}' 43 | ) 44 | ) 45 | 46 | elif stderr and b"BIRD version" not in stderr: 47 | raise ExecutionError(stderr.decode("utf-8")) 48 | 49 | # Extract numbers from string as list of numbers 50 | version_str = re.findall(r"\d+", raw_output) 51 | 52 | # Filter major release number & convert to int 53 | version = int(version_str[0]) 54 | 55 | log.debug(f"BIRD Major Version: {version_str[0]}") 56 | return version 57 | 58 | 59 | async def parse_bird_output(raw, query_data, not_found): 60 | """Parse raw BIRD output and return parsed output. 61 | 62 | Arguments: 63 | raw {str} -- Raw BIRD output 64 | query_data {object} -- Validated query object 65 | not_found {str} -- Lookup not found message template 66 | 67 | Returns: 68 | str -- Parsed output 69 | """ 70 | 71 | def remove_ready(lines): 72 | for line in lines: 73 | if not re.match(r".*(BIRD \d+\.\d+\.?\d* ready\.).*", line): 74 | yield line.strip() 75 | 76 | raw_split = re.split(r"(Table)", raw.strip()) 77 | 78 | if not raw_split: 79 | notfound_message = not_found.format( 80 | target=query_data.target, afi=AFI_DISPLAY_MAP[query_data.afi] 81 | ) 82 | lines = notfound_message 83 | else: 84 | lines = raw_split 85 | 86 | output = "\n".join(remove_ready(lines)) 87 | log.debug(f"Parsed output:\n{output}") 88 | return output 89 | 90 | 91 | def format_bird_bgp_community(target): 92 | """Convert from standard community format to BIRD format. 93 | 94 | Args: 95 | target {str} -- Query Target 96 | 97 | Returns: 98 | {str} -- Formatted target 99 | """ 100 | parts = target.split(":") 101 | return f'({",".join(parts)})' 102 | 103 | 104 | def format_bird_bgp_aspath(target): 105 | """Convert from Cisco AS_PATH format to BIRD format. 106 | 107 | Args: 108 | target {str} -- Query Target 109 | 110 | Returns: 111 | {str} -- Formatted query target 112 | """ 113 | 114 | # Extract ASNs from query target string 115 | asns = re.findall(r"\d+", target) 116 | 117 | if bool(re.match(r"^\_", target)): 118 | # Replace `_65000` with `.* 65000` 119 | asns.insert(0, "*") 120 | 121 | if bool(re.match(r".*(\_)$", target)): 122 | # Replace `65000_` with `65000 .*` 123 | asns.append("*") 124 | 125 | asns.insert(0, "[=") 126 | asns.append("=]") 127 | 128 | return " ".join(asns) 129 | -------------------------------------------------------------------------------- /hyperglass_agent/nos_utils/frr.py: -------------------------------------------------------------------------------- 1 | """Various Free Range Routing (FRR) utilities.""" 2 | 3 | # Project 4 | from hyperglass_agent.log import log 5 | from hyperglass_agent.constants import AFI_DISPLAY_MAP 6 | 7 | 8 | async def parse_frr_output(raw, query_data, not_found): 9 | """Parse raw CLI output from FRR (vtysh) and return parsed output. 10 | 11 | Arguments: 12 | raw {str} -- Raw output from vtysh 13 | query_data {object} -- Validated query object 14 | not_found {str} -- Lookup not found message template 15 | 16 | Returns: 17 | {str} -- Parsed output 18 | """ 19 | raw_split = raw.strip() 20 | if not raw_split: 21 | notfound_message = not_found.format( 22 | target=query_data.target, afi=AFI_DISPLAY_MAP[query_data.afi] 23 | ) 24 | output = notfound_message 25 | else: 26 | output = raw_split 27 | 28 | log.debug(f"Parsed output:\n{output}") 29 | return output 30 | -------------------------------------------------------------------------------- /hyperglass_agent/payload.py: -------------------------------------------------------------------------------- 1 | """Handle JSON Web Token Encoding & Decoding.""" 2 | 3 | # Standard Library 4 | import datetime 5 | 6 | # Third Party 7 | import jwt 8 | 9 | # Project 10 | from hyperglass_agent.config import params 11 | from hyperglass_agent.exceptions import SecurityError 12 | 13 | 14 | async def jwt_decode(*args, **kwargs): 15 | """Decode the request claim.""" 16 | return _jwt_decode(*args, **kwargs) 17 | 18 | 19 | async def jwt_encode(*args, **kwargs): 20 | """Encode the response claim.""" 21 | return _jwt_encode(*args, **kwargs) 22 | 23 | 24 | def _jwt_decode(payload): 25 | try: 26 | decoded = jwt.decode( 27 | payload, params.secret.get_secret_value(), algorithm="HS256" 28 | ) 29 | decoded = decoded["payload"] 30 | return decoded 31 | except (KeyError, jwt.PyJWTError) as exp: 32 | raise SecurityError(str(exp)) from None 33 | 34 | 35 | def _jwt_encode(response): 36 | payload = { 37 | "payload": response, 38 | "nbf": datetime.datetime.utcnow(), 39 | "iat": datetime.datetime.utcnow(), 40 | "exp": datetime.datetime.utcnow() 41 | + datetime.timedelta(seconds=params.valid_duration), 42 | } 43 | encoded = jwt.encode( 44 | payload, params.secret.get_secret_value(), algorithm="HS256" 45 | ).decode("utf-8") 46 | return encoded 47 | -------------------------------------------------------------------------------- /hyperglass_agent/util.py: -------------------------------------------------------------------------------- 1 | """Agent utility functions.""" 2 | 3 | # Standard Library 4 | import os 5 | import re 6 | from typing import Generator 7 | 8 | # Project 9 | from hyperglass_agent.constants import APP_PATHS 10 | 11 | 12 | def top_level_async(func): 13 | """Allow async functions to be executed synchronously. 14 | 15 | Arguments: 16 | func {function} -- Asynchronous function 17 | 18 | Returns: 19 | {function} -- Synchronous function 20 | """ 21 | import asyncio 22 | from functools import update_wrapper 23 | 24 | func = asyncio.coroutine(func) 25 | 26 | def _wrapper(*args, **kwargs): 27 | loop = asyncio.get_event_loop() 28 | return loop.run_until_complete(func(*args, **kwargs)) 29 | 30 | return update_wrapper(_wrapper, func) 31 | 32 | 33 | def find_app_path(): 34 | """Verify the supported app paths exist, return the first found path. 35 | 36 | Raises: 37 | ConfigError: Raised if no path is found. 38 | 39 | Returns: 40 | {Path} -- Matched app path 41 | """ 42 | app_path = None 43 | 44 | for path in APP_PATHS: 45 | if path.exists(): 46 | app_path = path 47 | break 48 | if app_path is None: 49 | raise RuntimeError( 50 | "None of the supported paths for hyperglass-agent were found.\n" 51 | + "Checked:\n{one}\n{two}".format(one=APP_PATHS[0], two=APP_PATHS[1]) 52 | ) 53 | os.environ["hyperglass_agent_directory"] = str(app_path) 54 | return app_path 55 | 56 | 57 | def set_app_path(required=False): 58 | """Find app directory and set value to environment variable.""" 59 | import os 60 | from pathlib import Path 61 | from getpass import getuser 62 | 63 | matched_path = None 64 | 65 | config_paths = (Path.home() / "hyperglass-agent", Path("/etc/hyperglass-agent/")) 66 | 67 | for path in config_paths: 68 | try: 69 | if path.exists(): 70 | tmp = path / "test.tmp" 71 | tmp.touch() 72 | if tmp.exists(): 73 | matched_path = path 74 | tmp.unlink() 75 | break 76 | except Exception: 77 | matched_path = None 78 | 79 | if required and matched_path is None: 80 | # Only raise an error if required is True 81 | raise RuntimeError( 82 | """ 83 | No configuration directories were determined to both exist and be readable 84 | by hyperglass. hyperglass is running as user '{un}' (UID '{uid}'), and tried 85 | to access the following directories: 86 | {dir}""".format( 87 | un=getuser(), 88 | uid=os.getuid(), 89 | dir="\n".join([" - " + str(p) for p in config_paths]), 90 | ) 91 | ) 92 | 93 | if matched_path is not None: 94 | os.environ["hyperglass_agent_directory"] = str(matched_path) 95 | 96 | return True 97 | 98 | 99 | def send_public_key(hyperglass_url, device_name, certificate, params): 100 | """Send this device's public key to hyperglass. 101 | 102 | Arguments: 103 | hyperglass_url {str} -- URL to hyperglass 104 | device_name {str} -- This device's hostname 105 | certificate {str} -- Public key string 106 | params {object} -- Configuration object 107 | 108 | Returns: 109 | {str} -- Response 110 | """ 111 | import httpx 112 | from hyperglass_agent.payload import _jwt_encode as jwt_encode 113 | from json import JSONDecodeError 114 | 115 | payload = jwt_encode(certificate.strip()) 116 | 117 | hyperglass_url = hyperglass_url.rstrip("/") 118 | 119 | try: 120 | response = httpx.post( 121 | hyperglass_url + "/api/import-agent-certificate/", 122 | json={"device": device_name, "encoded": payload}, 123 | ) 124 | except httpx.HTTPError as http_error: 125 | raise RuntimeError(str(http_error)) 126 | try: 127 | data = response.json() 128 | 129 | if response.status_code != 200: 130 | raise RuntimeError(data.get("output", "An error occurred")) 131 | 132 | return data.get("output", "An error occurred") 133 | 134 | except JSONDecodeError: 135 | if response.status_code != 200: 136 | raise RuntimeError(response.text) 137 | return response.text 138 | 139 | 140 | def get_addresses() -> Generator: 141 | """Get IPv4/IPv6 addresses associated with each interface.""" 142 | import psutil 143 | from ipaddress import ip_address 144 | 145 | interfaces = psutil.net_if_addrs() 146 | 147 | for iface, addrs in interfaces.items(): 148 | for addr in addrs: 149 | if addr.family.numerator in (2, 30): 150 | try: 151 | re.sub 152 | ip = ip_address(addr.address.split("%")[0]) 153 | if not any((ip.is_link_local, ip.is_multicast, ip.is_loopback)): 154 | yield iface, ip 155 | except ValueError: 156 | pass 157 | 158 | 159 | def color_support() -> Generator: 160 | """Determine if the terminal supports color.""" 161 | import sys 162 | import shutil 163 | import subprocess # noqa: S404 164 | 165 | supported_platform = sys.platform != "win32" or "ANSICON" in os.environ 166 | is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty() 167 | 168 | color = supported_platform and is_a_tty 169 | yield color 170 | 171 | has_tput = shutil.which("tput") 172 | num_colors = "" 173 | 174 | if color and has_tput: 175 | num_colors = ( 176 | subprocess.check_output([has_tput, "colors"]).decode().strip() # noqa: S603 177 | ) 178 | yield num_colors 179 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "main" 3 | description = "Asyncio support for PEP-567 contextvars backport." 4 | marker = "python_version < \"3.7\"" 5 | name = "aiocontextvars" 6 | optional = false 7 | python-versions = ">=3.5" 8 | version = "0.2.2" 9 | 10 | [package.dependencies] 11 | [package.dependencies.contextvars] 12 | python = "<3.7" 13 | version = "2.4" 14 | 15 | [[package]] 16 | category = "dev" 17 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 18 | name = "appdirs" 19 | optional = false 20 | python-versions = "*" 21 | version = "1.4.3" 22 | 23 | [[package]] 24 | category = "dev" 25 | description = "A few extensions to pyyaml." 26 | name = "aspy.yaml" 27 | optional = false 28 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 29 | version = "1.3.0" 30 | 31 | [package.dependencies] 32 | pyyaml = "*" 33 | 34 | [[package]] 35 | category = "dev" 36 | description = "Classes Without Boilerplate" 37 | name = "attrs" 38 | optional = false 39 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 40 | version = "19.3.0" 41 | 42 | [package.extras] 43 | azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] 44 | dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] 45 | docs = ["sphinx", "zope.interface"] 46 | tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 47 | 48 | [[package]] 49 | category = "dev" 50 | description = "Security oriented static analyser for python code." 51 | name = "bandit" 52 | optional = false 53 | python-versions = "*" 54 | version = "1.6.2" 55 | 56 | [package.dependencies] 57 | GitPython = ">=1.0.1" 58 | PyYAML = ">=3.13" 59 | colorama = ">=0.3.9" 60 | six = ">=1.10.0" 61 | stevedore = ">=1.20.0" 62 | 63 | [[package]] 64 | category = "dev" 65 | description = "The uncompromising code formatter." 66 | name = "black" 67 | optional = false 68 | python-versions = ">=3.6" 69 | version = "19.10b0" 70 | 71 | [package.dependencies] 72 | appdirs = "*" 73 | attrs = ">=18.1.0" 74 | click = ">=6.5" 75 | pathspec = ">=0.6,<1" 76 | regex = "*" 77 | toml = ">=0.9.4" 78 | typed-ast = ">=1.4.0" 79 | 80 | [package.extras] 81 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 82 | 83 | [[package]] 84 | category = "main" 85 | description = "A thin, practical wrapper around terminal coloring, styling, and positioning" 86 | name = "blessings" 87 | optional = false 88 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 89 | version = "1.7" 90 | 91 | [package.dependencies] 92 | six = "*" 93 | 94 | [[package]] 95 | category = "main" 96 | description = "Python package for providing Mozilla's CA Bundle." 97 | name = "certifi" 98 | optional = false 99 | python-versions = "*" 100 | version = "2019.11.28" 101 | 102 | [[package]] 103 | category = "main" 104 | description = "Foreign Function Interface for Python calling C code." 105 | name = "cffi" 106 | optional = false 107 | python-versions = "*" 108 | version = "1.14.0" 109 | 110 | [package.dependencies] 111 | pycparser = "*" 112 | 113 | [[package]] 114 | category = "dev" 115 | description = "Validate configuration and produce human readable error messages." 116 | name = "cfgv" 117 | optional = false 118 | python-versions = ">=3.6" 119 | version = "3.0.0" 120 | 121 | [[package]] 122 | category = "main" 123 | description = "Universal encoding detector for Python 2 and 3" 124 | name = "chardet" 125 | optional = false 126 | python-versions = "*" 127 | version = "3.0.4" 128 | 129 | [[package]] 130 | category = "main" 131 | description = "Composable command line interface toolkit" 132 | name = "click" 133 | optional = false 134 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 135 | version = "7.0" 136 | 137 | [[package]] 138 | category = "main" 139 | description = "Cross-platform colored terminal text." 140 | marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" 141 | name = "colorama" 142 | optional = false 143 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 144 | version = "0.4.3" 145 | 146 | [[package]] 147 | category = "main" 148 | description = "PEP 567 Backport" 149 | marker = "python_version < \"3.7\"" 150 | name = "contextvars" 151 | optional = false 152 | python-versions = "*" 153 | version = "2.4" 154 | 155 | [package.dependencies] 156 | immutables = ">=0.9" 157 | 158 | [[package]] 159 | category = "main" 160 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 161 | name = "cryptography" 162 | optional = false 163 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" 164 | version = "2.8" 165 | 166 | [package.dependencies] 167 | cffi = ">=1.8,<1.11.3 || >1.11.3" 168 | six = ">=1.4.1" 169 | 170 | [package.extras] 171 | docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0)", "sphinx-rtd-theme"] 172 | docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] 173 | idna = ["idna (>=2.1)"] 174 | pep8test = ["flake8", "flake8-import-order", "pep8-naming"] 175 | test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"] 176 | 177 | [[package]] 178 | category = "main" 179 | description = "A backport of the dataclasses module for Python 3.6" 180 | marker = "python_version < \"3.7\"" 181 | name = "dataclasses" 182 | optional = false 183 | python-versions = "*" 184 | version = "0.6" 185 | 186 | [[package]] 187 | category = "dev" 188 | description = "Distribution utilities" 189 | name = "distlib" 190 | optional = false 191 | python-versions = "*" 192 | version = "0.3.0" 193 | 194 | [[package]] 195 | category = "dev" 196 | description = "Discover and load entry points from installed packages." 197 | name = "entrypoints" 198 | optional = false 199 | python-versions = ">=2.7" 200 | version = "0.3" 201 | 202 | [[package]] 203 | category = "dev" 204 | description = "Removes commented-out code." 205 | name = "eradicate" 206 | optional = false 207 | python-versions = "*" 208 | version = "1.0" 209 | 210 | [[package]] 211 | category = "main" 212 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 213 | name = "fastapi" 214 | optional = false 215 | python-versions = ">=3.6" 216 | version = "0.45.0" 217 | 218 | [package.dependencies] 219 | pydantic = ">=0.32.2,<2.0.0" 220 | starlette = "0.12.9" 221 | 222 | [package.extras] 223 | all = ["requests", "aiofiles", "jinja2", "python-multipart", "itsdangerous", "pyyaml", "graphene", "ujson", "email-validator", "uvicorn", "async-exit-stack", "async-generator"] 224 | dev = ["pyjwt", "passlib"] 225 | doc = ["mkdocs", "mkdocs-material", "markdown-include"] 226 | test = ["pytest (>=4.0.0)", "pytest-cov", "mypy", "black", "isort", "requests", "email-validator", "sqlalchemy", "databases", "orjson", "async-exit-stack", "async-generator"] 227 | 228 | [[package]] 229 | category = "dev" 230 | description = "A platform independent file lock." 231 | name = "filelock" 232 | optional = false 233 | python-versions = "*" 234 | version = "3.0.12" 235 | 236 | [[package]] 237 | category = "dev" 238 | description = "the modular source code checker: pep8, pyflakes and co" 239 | name = "flake8" 240 | optional = false 241 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 242 | version = "3.7.9" 243 | 244 | [package.dependencies] 245 | entrypoints = ">=0.3.0,<0.4.0" 246 | mccabe = ">=0.6.0,<0.7.0" 247 | pycodestyle = ">=2.5.0,<2.6.0" 248 | pyflakes = ">=2.1.0,<2.2.0" 249 | 250 | [[package]] 251 | category = "dev" 252 | description = "Automated security testing with bandit and flake8." 253 | name = "flake8-bandit" 254 | optional = false 255 | python-versions = "*" 256 | version = "2.1.2" 257 | 258 | [package.dependencies] 259 | bandit = "*" 260 | flake8 = "*" 261 | flake8-polyfill = "*" 262 | pycodestyle = "*" 263 | 264 | [[package]] 265 | category = "dev" 266 | description = "flake8 plugin to call black as a code style validator" 267 | name = "flake8-black" 268 | optional = false 269 | python-versions = "*" 270 | version = "0.1.1" 271 | 272 | [package.dependencies] 273 | black = ">=19.3b0" 274 | flake8 = ">=3.0.0" 275 | 276 | [[package]] 277 | category = "dev" 278 | description = "Flake8 plugin that check forgotten breakpoints" 279 | name = "flake8-breakpoint" 280 | optional = false 281 | python-versions = ">=3.6,<4.0" 282 | version = "1.1.0" 283 | 284 | [package.dependencies] 285 | flake8-plugin-utils = ">=1.0,<2.0" 286 | 287 | [[package]] 288 | category = "dev" 289 | description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." 290 | name = "flake8-bugbear" 291 | optional = false 292 | python-versions = ">=3.6" 293 | version = "20.1.4" 294 | 295 | [package.dependencies] 296 | attrs = ">=19.2.0" 297 | flake8 = ">=3.0.0" 298 | 299 | [[package]] 300 | category = "dev" 301 | description = "Check for python builtins being used as variables or parameters." 302 | name = "flake8-builtins" 303 | optional = false 304 | python-versions = "*" 305 | version = "1.4.2" 306 | 307 | [package.dependencies] 308 | flake8 = "*" 309 | 310 | [package.extras] 311 | test = ["coverage", "coveralls", "mock", "pytest", "pytest-cov"] 312 | 313 | [[package]] 314 | category = "dev" 315 | description = "A flake8 plugin to help you write better list/set/dict comprehensions." 316 | name = "flake8-comprehensions" 317 | optional = false 318 | python-versions = ">=3.5" 319 | version = "3.2.2" 320 | 321 | [package.dependencies] 322 | flake8 = ">=3.0,<3.2.0 || >3.2.0,<4" 323 | 324 | [package.dependencies.importlib-metadata] 325 | python = "<3.8" 326 | version = "*" 327 | 328 | [[package]] 329 | category = "dev" 330 | description = "Warns about deprecated method calls." 331 | name = "flake8-deprecated" 332 | optional = false 333 | python-versions = "*" 334 | version = "1.3" 335 | 336 | [package.dependencies] 337 | flake8 = ">=3.0.0" 338 | 339 | [[package]] 340 | category = "dev" 341 | description = "Extension for flake8 which uses pydocstyle to check docstrings" 342 | name = "flake8-docstrings" 343 | optional = false 344 | python-versions = "*" 345 | version = "1.5.0" 346 | 347 | [package.dependencies] 348 | flake8 = ">=3" 349 | pydocstyle = ">=2.1" 350 | 351 | [[package]] 352 | category = "dev" 353 | description = "Flake8 plugin to find commented out code" 354 | name = "flake8-eradicate" 355 | optional = false 356 | python-versions = ">=3.6,<4.0" 357 | version = "0.2.4" 358 | 359 | [package.dependencies] 360 | attrs = ">=18.2,<20.0" 361 | eradicate = ">=0.2.1,<1.1.0" 362 | flake8 = ">=3.5,<4.0" 363 | 364 | [[package]] 365 | category = "dev" 366 | description = "The plugin checks `if expressions` (ternary operator)" 367 | name = "flake8-if-expr" 368 | optional = false 369 | python-versions = ">=3.6,<4.0" 370 | version = "1.0.0" 371 | 372 | [package.dependencies] 373 | flake8-plugin-utils = ">=1.0,<2.0" 374 | 375 | [[package]] 376 | category = "dev" 377 | description = "flake8 plugin that integrates isort ." 378 | name = "flake8-isort" 379 | optional = false 380 | python-versions = "*" 381 | version = "2.8.0" 382 | 383 | [package.dependencies] 384 | flake8 = ">=3.2.1" 385 | testfixtures = "*" 386 | 387 | [package.dependencies.isort] 388 | extras = ["pyproject"] 389 | version = ">=4.3.0" 390 | 391 | [package.extras] 392 | test = ["pytest"] 393 | 394 | [[package]] 395 | category = "dev" 396 | description = "A flake8 extension that implements misc. lints" 397 | name = "flake8-pie" 398 | optional = false 399 | python-versions = ">=3.6" 400 | version = "0.4.2" 401 | 402 | [[package]] 403 | category = "dev" 404 | description = "The package provides base classes and utils for flake8 plugin writing" 405 | name = "flake8-plugin-utils" 406 | optional = false 407 | python-versions = ">=3.6,<4.0" 408 | version = "1.0.0" 409 | 410 | [[package]] 411 | category = "dev" 412 | description = "Polyfill package for Flake8 plugins" 413 | name = "flake8-polyfill" 414 | optional = false 415 | python-versions = "*" 416 | version = "1.0.2" 417 | 418 | [package.dependencies] 419 | flake8 = "*" 420 | 421 | [[package]] 422 | category = "dev" 423 | description = "print statement checker plugin for flake8" 424 | name = "flake8-print" 425 | optional = false 426 | python-versions = "*" 427 | version = "3.1.4" 428 | 429 | [package.dependencies] 430 | flake8 = ">=1.5" 431 | pycodestyle = "*" 432 | six = "*" 433 | 434 | [[package]] 435 | category = "dev" 436 | description = "Flake8 plugin that checks return values" 437 | name = "flake8-return" 438 | optional = false 439 | python-versions = ">=3.6,<4.0" 440 | version = "1.1.1" 441 | 442 | [package.dependencies] 443 | flake8-plugin-utils = ">=1.0,<2.0" 444 | 445 | [[package]] 446 | category = "dev" 447 | description = "Git Object Database" 448 | name = "gitdb" 449 | optional = false 450 | python-versions = ">=3.4" 451 | version = "4.0.2" 452 | 453 | [package.dependencies] 454 | smmap = ">=3.0.1,<4" 455 | 456 | [[package]] 457 | category = "dev" 458 | description = "Python Git Library" 459 | name = "gitpython" 460 | optional = false 461 | python-versions = ">=3.4" 462 | version = "3.1.0" 463 | 464 | [package.dependencies] 465 | gitdb = ">=4.0.1,<5" 466 | 467 | [[package]] 468 | category = "main" 469 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 470 | name = "h11" 471 | optional = false 472 | python-versions = "*" 473 | version = "0.9.0" 474 | 475 | [[package]] 476 | category = "main" 477 | description = "HTTP/2 State-Machine based protocol implementation" 478 | name = "h2" 479 | optional = false 480 | python-versions = "*" 481 | version = "3.2.0" 482 | 483 | [package.dependencies] 484 | hpack = ">=3.0,<4" 485 | hyperframe = ">=5.2.0,<6" 486 | 487 | [[package]] 488 | category = "main" 489 | description = "Pure-Python HPACK header compression" 490 | name = "hpack" 491 | optional = false 492 | python-versions = "*" 493 | version = "3.0.0" 494 | 495 | [[package]] 496 | category = "main" 497 | description = "Chromium HSTS Preload list as a Python package and updated daily" 498 | name = "hstspreload" 499 | optional = false 500 | python-versions = ">=3.6" 501 | version = "2020.2.29" 502 | 503 | [[package]] 504 | category = "main" 505 | description = "A collection of framework independent HTTP protocol utils." 506 | marker = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"" 507 | name = "httptools" 508 | optional = false 509 | python-versions = "*" 510 | version = "0.1.1" 511 | 512 | [package.extras] 513 | test = ["Cython (0.29.14)"] 514 | 515 | [[package]] 516 | category = "main" 517 | description = "The next generation HTTP client." 518 | name = "httpx" 519 | optional = false 520 | python-versions = ">=3.6" 521 | version = "0.11.1" 522 | 523 | [package.dependencies] 524 | certifi = "*" 525 | chardet = ">=3.0.0,<4.0.0" 526 | h11 = ">=0.8,<0.10" 527 | h2 = ">=3.0.0,<4.0.0" 528 | hstspreload = "*" 529 | idna = ">=2.0.0,<3.0.0" 530 | rfc3986 = ">=1.3,<2" 531 | sniffio = ">=1.0.0,<2.0.0" 532 | urllib3 = ">=1.0.0,<2.0.0" 533 | 534 | [[package]] 535 | category = "main" 536 | description = "HTTP/2 framing layer for Python" 537 | name = "hyperframe" 538 | optional = false 539 | python-versions = "*" 540 | version = "5.2.0" 541 | 542 | [[package]] 543 | category = "dev" 544 | description = "File identification library for Python" 545 | name = "identify" 546 | optional = false 547 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 548 | version = "1.4.11" 549 | 550 | [package.extras] 551 | license = ["editdistance"] 552 | 553 | [[package]] 554 | category = "main" 555 | description = "Internationalized Domain Names in Applications (IDNA)" 556 | name = "idna" 557 | optional = false 558 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 559 | version = "2.9" 560 | 561 | [[package]] 562 | category = "main" 563 | description = "Immutable Collections" 564 | marker = "python_version < \"3.7\"" 565 | name = "immutables" 566 | optional = false 567 | python-versions = "*" 568 | version = "0.11" 569 | 570 | [[package]] 571 | category = "dev" 572 | description = "Read metadata from Python packages" 573 | marker = "python_version < \"3.8\"" 574 | name = "importlib-metadata" 575 | optional = false 576 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 577 | version = "1.5.0" 578 | 579 | [package.dependencies] 580 | zipp = ">=0.5" 581 | 582 | [package.extras] 583 | docs = ["sphinx", "rst.linker"] 584 | testing = ["packaging", "importlib-resources"] 585 | 586 | [[package]] 587 | category = "dev" 588 | description = "Read resources from Python packages" 589 | marker = "python_version < \"3.7\"" 590 | name = "importlib-resources" 591 | optional = false 592 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 593 | version = "1.1.0" 594 | 595 | [package.dependencies] 596 | [package.dependencies.zipp] 597 | python = "<3.8" 598 | version = ">=0.4" 599 | 600 | [package.extras] 601 | docs = ["sphinx", "docutils (0.12)"] 602 | 603 | [[package]] 604 | category = "main" 605 | description = "Collection of common interactive command line user interfaces, based on Inquirer.js" 606 | name = "inquirer" 607 | optional = false 608 | python-versions = "*" 609 | version = "2.6.3" 610 | 611 | [package.dependencies] 612 | blessings = "1.7" 613 | python-editor = "1.0.4" 614 | readchar = "2.0.1" 615 | 616 | [[package]] 617 | category = "dev" 618 | description = "A Python utility / library to sort Python imports." 619 | name = "isort" 620 | optional = false 621 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 622 | version = "4.3.21" 623 | 624 | [package.extras] 625 | pipfile = ["pipreqs", "requirementslib"] 626 | pyproject = ["toml"] 627 | requirements = ["pipreqs", "pip-api"] 628 | xdg_home = ["appdirs (>=1.4.0)"] 629 | 630 | [[package]] 631 | category = "main" 632 | description = "Python logging made (stupidly) simple" 633 | name = "loguru" 634 | optional = false 635 | python-versions = ">=3.5" 636 | version = "0.4.1" 637 | 638 | [package.dependencies] 639 | colorama = ">=0.3.4" 640 | win32-setctime = ">=1.0.0" 641 | 642 | [package.dependencies.aiocontextvars] 643 | python = "<3.7" 644 | version = ">=0.2.0" 645 | 646 | [package.extras] 647 | dev = ["codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "isort (>=4.3.20)", "tox (>=3.9.0)", "tox-travis (>=0.12)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "Sphinx (>=2.2.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "black (>=19.3b0)"] 648 | 649 | [[package]] 650 | category = "dev" 651 | description = "McCabe checker, plugin for flake8" 652 | name = "mccabe" 653 | optional = false 654 | python-versions = "*" 655 | version = "0.6.1" 656 | 657 | [[package]] 658 | category = "dev" 659 | description = "Node.js virtual environment builder" 660 | name = "nodeenv" 661 | optional = false 662 | python-versions = "*" 663 | version = "1.3.5" 664 | 665 | [[package]] 666 | category = "dev" 667 | description = "Utility library for gitignore style pattern matching of file paths." 668 | name = "pathspec" 669 | optional = false 670 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 671 | version = "0.7.0" 672 | 673 | [[package]] 674 | category = "dev" 675 | description = "Python Build Reasonableness" 676 | name = "pbr" 677 | optional = false 678 | python-versions = "*" 679 | version = "5.4.4" 680 | 681 | [[package]] 682 | category = "dev" 683 | description = "Check PEP-8 naming conventions, plugin for flake8" 684 | name = "pep8-naming" 685 | optional = false 686 | python-versions = "*" 687 | version = "0.9.1" 688 | 689 | [package.dependencies] 690 | flake8-polyfill = ">=1.0.2,<2" 691 | 692 | [[package]] 693 | category = "dev" 694 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 695 | name = "pre-commit" 696 | optional = false 697 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 698 | version = "1.21.0" 699 | 700 | [package.dependencies] 701 | "aspy.yaml" = "*" 702 | cfgv = ">=2.0.0" 703 | identify = ">=1.0.0" 704 | nodeenv = ">=0.11.1" 705 | pyyaml = "*" 706 | six = "*" 707 | toml = "*" 708 | virtualenv = ">=15.2" 709 | 710 | [package.dependencies.importlib-metadata] 711 | python = "<3.8" 712 | version = "*" 713 | 714 | [package.dependencies.importlib-resources] 715 | python = "<3.7" 716 | version = "*" 717 | 718 | [[package]] 719 | category = "main" 720 | description = "Cross-platform lib for process and system monitoring in Python." 721 | name = "psutil" 722 | optional = false 723 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 724 | version = "5.7.2" 725 | 726 | [package.extras] 727 | test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"] 728 | 729 | [[package]] 730 | category = "dev" 731 | description = "Python style guide checker" 732 | name = "pycodestyle" 733 | optional = false 734 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 735 | version = "2.5.0" 736 | 737 | [[package]] 738 | category = "main" 739 | description = "C parser in Python" 740 | name = "pycparser" 741 | optional = false 742 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 743 | version = "2.19" 744 | 745 | [[package]] 746 | category = "main" 747 | description = "Data validation and settings management using python 3.6 type hinting" 748 | name = "pydantic" 749 | optional = false 750 | python-versions = ">=3.6" 751 | version = "1.4" 752 | 753 | [package.dependencies] 754 | [package.dependencies.dataclasses] 755 | python = "<3.7" 756 | version = ">=0.6" 757 | 758 | [package.extras] 759 | dotenv = ["python-dotenv (>=0.10.4)"] 760 | email = ["email-validator (>=1.0.3)"] 761 | typing_extensions = ["typing-extensions (>=3.7.2)"] 762 | 763 | [[package]] 764 | category = "dev" 765 | description = "Python docstring style checker" 766 | name = "pydocstyle" 767 | optional = false 768 | python-versions = ">=3.5" 769 | version = "5.0.2" 770 | 771 | [package.dependencies] 772 | snowballstemmer = "*" 773 | 774 | [[package]] 775 | category = "dev" 776 | description = "passive checker of Python programs" 777 | name = "pyflakes" 778 | optional = false 779 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 780 | version = "2.1.1" 781 | 782 | [[package]] 783 | category = "main" 784 | description = "JSON Web Token implementation in Python" 785 | name = "pyjwt" 786 | optional = false 787 | python-versions = "*" 788 | version = "1.7.1" 789 | 790 | [package.extras] 791 | crypto = ["cryptography (>=1.4)"] 792 | flake8 = ["flake8", "flake8-import-order", "pep8-naming"] 793 | test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner (>=4.2,<5.0.0)"] 794 | 795 | [[package]] 796 | category = "main" 797 | description = "Programmatically open an editor, capture the result." 798 | name = "python-editor" 799 | optional = false 800 | python-versions = "*" 801 | version = "1.0.4" 802 | 803 | [[package]] 804 | category = "main" 805 | description = "YAML parser and emitter for Python" 806 | name = "pyyaml" 807 | optional = false 808 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 809 | version = "5.3" 810 | 811 | [[package]] 812 | category = "main" 813 | description = "Utilities to read single characters and key-strokes" 814 | name = "readchar" 815 | optional = false 816 | python-versions = "*" 817 | version = "2.0.1" 818 | 819 | [[package]] 820 | category = "dev" 821 | description = "Alternative regular expression module, to replace re." 822 | name = "regex" 823 | optional = false 824 | python-versions = "*" 825 | version = "2020.2.20" 826 | 827 | [[package]] 828 | category = "main" 829 | description = "Validating URI References per RFC 3986" 830 | name = "rfc3986" 831 | optional = false 832 | python-versions = "*" 833 | version = "1.3.2" 834 | 835 | [package.extras] 836 | idna2008 = ["idna"] 837 | 838 | [[package]] 839 | category = "main" 840 | description = "Python 2 and 3 compatibility utilities" 841 | name = "six" 842 | optional = false 843 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 844 | version = "1.14.0" 845 | 846 | [[package]] 847 | category = "dev" 848 | description = "A pure Python implementation of a sliding window memory map manager" 849 | name = "smmap" 850 | optional = false 851 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 852 | version = "3.0.1" 853 | 854 | [[package]] 855 | category = "main" 856 | description = "Sniff out which async library your code is running under" 857 | name = "sniffio" 858 | optional = false 859 | python-versions = ">=3.5" 860 | version = "1.1.0" 861 | 862 | [package.dependencies] 863 | [package.dependencies.contextvars] 864 | python = "<3.7" 865 | version = ">=2.1" 866 | 867 | [[package]] 868 | category = "dev" 869 | description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." 870 | name = "snowballstemmer" 871 | optional = false 872 | python-versions = "*" 873 | version = "2.0.0" 874 | 875 | [[package]] 876 | category = "main" 877 | description = "Debug-friendly stack traces, with variable values and semantic highlighting" 878 | name = "stackprinter" 879 | optional = false 880 | python-versions = ">=3.4" 881 | version = "0.2.3" 882 | 883 | [[package]] 884 | category = "main" 885 | description = "The little ASGI library that shines." 886 | name = "starlette" 887 | optional = false 888 | python-versions = ">=3.6" 889 | version = "0.12.9" 890 | 891 | [package.extras] 892 | full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"] 893 | 894 | [[package]] 895 | category = "dev" 896 | description = "Manage dynamic plugins for Python applications" 897 | name = "stevedore" 898 | optional = false 899 | python-versions = "*" 900 | version = "1.32.0" 901 | 902 | [package.dependencies] 903 | pbr = ">=2.0.0,<2.1.0 || >2.1.0" 904 | six = ">=1.10.0" 905 | 906 | [[package]] 907 | category = "dev" 908 | description = "A collection of helpers and mock objects for unit tests and doc tests." 909 | name = "testfixtures" 910 | optional = false 911 | python-versions = "*" 912 | version = "6.14.0" 913 | 914 | [package.extras] 915 | build = ["setuptools-git", "wheel", "twine"] 916 | docs = ["sphinx", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] 917 | test = ["pytest (>=3.6)", "pytest-cov", "pytest-django", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] 918 | 919 | [[package]] 920 | category = "dev" 921 | description = "Python Library for Tom's Obvious, Minimal Language" 922 | name = "toml" 923 | optional = false 924 | python-versions = "*" 925 | version = "0.10.0" 926 | 927 | [[package]] 928 | category = "dev" 929 | description = "a fork of Python 2 and 3 ast modules with type comment support" 930 | name = "typed-ast" 931 | optional = false 932 | python-versions = "*" 933 | version = "1.4.1" 934 | 935 | [[package]] 936 | category = "main" 937 | description = "HTTP library with thread-safe connection pooling, file post, and more." 938 | name = "urllib3" 939 | optional = false 940 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 941 | version = "1.25.8" 942 | 943 | [package.extras] 944 | brotli = ["brotlipy (>=0.6.0)"] 945 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 946 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] 947 | 948 | [[package]] 949 | category = "main" 950 | description = "The lightning-fast ASGI server." 951 | name = "uvicorn" 952 | optional = false 953 | python-versions = "*" 954 | version = "0.11.3" 955 | 956 | [package.dependencies] 957 | click = ">=7.0.0,<8.0.0" 958 | h11 = ">=0.8,<0.10" 959 | httptools = ">=0.1.0,<0.2.0" 960 | uvloop = ">=0.14.0" 961 | websockets = ">=8.0.0,<9.0.0" 962 | 963 | [[package]] 964 | category = "main" 965 | description = "Fast implementation of asyncio event loop on top of libuv" 966 | name = "uvloop" 967 | optional = false 968 | python-versions = "*" 969 | version = "0.14.0" 970 | 971 | [[package]] 972 | category = "dev" 973 | description = "Virtual Python Environment builder" 974 | name = "virtualenv" 975 | optional = false 976 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 977 | version = "20.0.7" 978 | 979 | [package.dependencies] 980 | appdirs = ">=1.4.3,<2" 981 | distlib = ">=0.3.0,<1" 982 | filelock = ">=3.0.0,<4" 983 | six = ">=1.9.0,<2" 984 | 985 | [package.dependencies.importlib-metadata] 986 | python = "<3.8" 987 | version = ">=0.12,<2" 988 | 989 | [package.dependencies.importlib-resources] 990 | python = "<3.7" 991 | version = ">=1.0,<2" 992 | 993 | [package.extras] 994 | docs = ["sphinx (>=2.0.0,<3)", "sphinx-argparse (>=0.2.5,<1)", "sphinx-rtd-theme (>=0.4.3,<1)", "towncrier (>=19.9.0rc1)", "proselint (>=0.10.2,<1)"] 995 | testing = ["pytest (>=4.0.0,<6)", "coverage (>=4.5.1,<6)", "pytest-mock (>=2.0.0,<3)", "pytest-env (>=0.6.2,<1)", "packaging (>=20.0)", "xonsh (>=0.9.13,<1)"] 996 | 997 | [[package]] 998 | category = "main" 999 | description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" 1000 | name = "websockets" 1001 | optional = false 1002 | python-versions = ">=3.6" 1003 | version = "8.0.2" 1004 | 1005 | [[package]] 1006 | category = "main" 1007 | description = "A small Python utility to set file creation time on Windows" 1008 | marker = "sys_platform == \"win32\"" 1009 | name = "win32-setctime" 1010 | optional = false 1011 | python-versions = ">=3.5" 1012 | version = "1.0.1" 1013 | 1014 | [package.extras] 1015 | dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] 1016 | 1017 | [[package]] 1018 | category = "dev" 1019 | description = "Backport of pathlib-compatible object wrapper for zip files" 1020 | marker = "python_version < \"3.8\"" 1021 | name = "zipp" 1022 | optional = false 1023 | python-versions = ">=3.6" 1024 | version = "3.0.0" 1025 | 1026 | [package.extras] 1027 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 1028 | testing = ["jaraco.itertools", "func-timeout"] 1029 | 1030 | [metadata] 1031 | content-hash = "c35093fe38aabcd1f044801dbd97f4d73b5e226395f60de8f9c6d24fbbe0bc13" 1032 | lock-version = "1.0" 1033 | python-versions = "^3.6" 1034 | 1035 | [metadata.files] 1036 | aiocontextvars = [ 1037 | {file = "aiocontextvars-0.2.2-py2.py3-none-any.whl", hash = "sha256:885daf8261818767d8f7cbd79f9d4482d118f024b6586ef6e67980236a27bfa3"}, 1038 | {file = "aiocontextvars-0.2.2.tar.gz", hash = "sha256:f027372dc48641f683c559f247bd84962becaacdc9ba711d583c3871fb5652aa"}, 1039 | ] 1040 | appdirs = [ 1041 | {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, 1042 | {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, 1043 | ] 1044 | "aspy.yaml" = [ 1045 | {file = "aspy.yaml-1.3.0-py2.py3-none-any.whl", hash = "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc"}, 1046 | {file = "aspy.yaml-1.3.0.tar.gz", hash = "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45"}, 1047 | ] 1048 | attrs = [ 1049 | {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, 1050 | {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, 1051 | ] 1052 | bandit = [ 1053 | {file = "bandit-1.6.2-py2.py3-none-any.whl", hash = "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952"}, 1054 | {file = "bandit-1.6.2.tar.gz", hash = "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"}, 1055 | ] 1056 | black = [ 1057 | {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, 1058 | {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, 1059 | ] 1060 | blessings = [ 1061 | {file = "blessings-1.7-py2-none-any.whl", hash = "sha256:caad5211e7ba5afe04367cdd4cfc68fa886e2e08f6f35e76b7387d2109ccea6e"}, 1062 | {file = "blessings-1.7-py3-none-any.whl", hash = "sha256:b1fdd7e7a675295630f9ae71527a8ebc10bfefa236b3d6aa4932ee4462c17ba3"}, 1063 | {file = "blessings-1.7.tar.gz", hash = "sha256:98e5854d805f50a5b58ac2333411b0482516a8210f23f43308baeb58d77c157d"}, 1064 | ] 1065 | certifi = [ 1066 | {file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"}, 1067 | {file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"}, 1068 | ] 1069 | cffi = [ 1070 | {file = "cffi-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384"}, 1071 | {file = "cffi-1.14.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30"}, 1072 | {file = "cffi-1.14.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"}, 1073 | {file = "cffi-1.14.0-cp27-cp27m-win32.whl", hash = "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78"}, 1074 | {file = "cffi-1.14.0-cp27-cp27m-win_amd64.whl", hash = "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793"}, 1075 | {file = "cffi-1.14.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e"}, 1076 | {file = "cffi-1.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a"}, 1077 | {file = "cffi-1.14.0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff"}, 1078 | {file = "cffi-1.14.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f"}, 1079 | {file = "cffi-1.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa"}, 1080 | {file = "cffi-1.14.0-cp35-cp35m-win32.whl", hash = "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5"}, 1081 | {file = "cffi-1.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4"}, 1082 | {file = "cffi-1.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d"}, 1083 | {file = "cffi-1.14.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc"}, 1084 | {file = "cffi-1.14.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac"}, 1085 | {file = "cffi-1.14.0-cp36-cp36m-win32.whl", hash = "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f"}, 1086 | {file = "cffi-1.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b"}, 1087 | {file = "cffi-1.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3"}, 1088 | {file = "cffi-1.14.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66"}, 1089 | {file = "cffi-1.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0"}, 1090 | {file = "cffi-1.14.0-cp37-cp37m-win32.whl", hash = "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f"}, 1091 | {file = "cffi-1.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26"}, 1092 | {file = "cffi-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd"}, 1093 | {file = "cffi-1.14.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55"}, 1094 | {file = "cffi-1.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2"}, 1095 | {file = "cffi-1.14.0-cp38-cp38-win32.whl", hash = "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8"}, 1096 | {file = "cffi-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b"}, 1097 | {file = "cffi-1.14.0.tar.gz", hash = "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6"}, 1098 | ] 1099 | cfgv = [ 1100 | {file = "cfgv-3.0.0-py2.py3-none-any.whl", hash = "sha256:f22b426ed59cd2ab2b54ff96608d846c33dfb8766a67f0b4a6ce130ce244414f"}, 1101 | {file = "cfgv-3.0.0.tar.gz", hash = "sha256:04b093b14ddf9fd4d17c53ebfd55582d27b76ed30050193c14e560770c5360eb"}, 1102 | ] 1103 | chardet = [ 1104 | {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, 1105 | {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, 1106 | ] 1107 | click = [ 1108 | {file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"}, 1109 | {file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"}, 1110 | ] 1111 | colorama = [ 1112 | {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, 1113 | {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, 1114 | ] 1115 | contextvars = [ 1116 | {file = "contextvars-2.4.tar.gz", hash = "sha256:f38c908aaa59c14335eeea12abea5f443646216c4e29380d7bf34d2018e2c39e"}, 1117 | ] 1118 | cryptography = [ 1119 | {file = "cryptography-2.8-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"}, 1120 | {file = "cryptography-2.8-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2"}, 1121 | {file = "cryptography-2.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad"}, 1122 | {file = "cryptography-2.8-cp27-cp27m-win32.whl", hash = "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2"}, 1123 | {file = "cryptography-2.8-cp27-cp27m-win_amd64.whl", hash = "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912"}, 1124 | {file = "cryptography-2.8-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d"}, 1125 | {file = "cryptography-2.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42"}, 1126 | {file = "cryptography-2.8-cp34-abi3-macosx_10_6_intel.whl", hash = "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879"}, 1127 | {file = "cryptography-2.8-cp34-abi3-manylinux1_x86_64.whl", hash = "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d"}, 1128 | {file = "cryptography-2.8-cp34-abi3-manylinux2010_x86_64.whl", hash = "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9"}, 1129 | {file = "cryptography-2.8-cp34-cp34m-win32.whl", hash = "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c"}, 1130 | {file = "cryptography-2.8-cp34-cp34m-win_amd64.whl", hash = "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0"}, 1131 | {file = "cryptography-2.8-cp35-cp35m-win32.whl", hash = "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf"}, 1132 | {file = "cryptography-2.8-cp35-cp35m-win_amd64.whl", hash = "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793"}, 1133 | {file = "cryptography-2.8-cp36-cp36m-win32.whl", hash = "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595"}, 1134 | {file = "cryptography-2.8-cp36-cp36m-win_amd64.whl", hash = "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7"}, 1135 | {file = "cryptography-2.8-cp37-cp37m-win32.whl", hash = "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff"}, 1136 | {file = "cryptography-2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f"}, 1137 | {file = "cryptography-2.8-cp38-cp38-win32.whl", hash = "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e"}, 1138 | {file = "cryptography-2.8-cp38-cp38-win_amd64.whl", hash = "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13"}, 1139 | {file = "cryptography-2.8.tar.gz", hash = "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651"}, 1140 | ] 1141 | dataclasses = [ 1142 | {file = "dataclasses-0.6-py3-none-any.whl", hash = "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f"}, 1143 | {file = "dataclasses-0.6.tar.gz", hash = "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"}, 1144 | ] 1145 | distlib = [ 1146 | {file = "distlib-0.3.0.zip", hash = "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"}, 1147 | ] 1148 | entrypoints = [ 1149 | {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, 1150 | {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, 1151 | ] 1152 | eradicate = [ 1153 | {file = "eradicate-1.0.tar.gz", hash = "sha256:4ffda82aae6fd49dfffa777a857cb758d77502a1f2e0f54c9ac5155a39d2d01a"}, 1154 | ] 1155 | fastapi = [ 1156 | {file = "fastapi-0.45.0-py3-none-any.whl", hash = "sha256:3f626eda9b6edaa17c90c21a4d0d1a97a2a2fcba43a55f8c425b6b37e832c8bb"}, 1157 | {file = "fastapi-0.45.0.tar.gz", hash = "sha256:44712863ca3899eb812a6869a2efe02d6be6ae972968c76a43d82ec472788f17"}, 1158 | ] 1159 | filelock = [ 1160 | {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, 1161 | {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, 1162 | ] 1163 | flake8 = [ 1164 | {file = "flake8-3.7.9-py2.py3-none-any.whl", hash = "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"}, 1165 | {file = "flake8-3.7.9.tar.gz", hash = "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb"}, 1166 | ] 1167 | flake8-bandit = [ 1168 | {file = "flake8_bandit-2.1.2.tar.gz", hash = "sha256:687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b"}, 1169 | ] 1170 | flake8-black = [ 1171 | {file = "flake8-black-0.1.1.tar.gz", hash = "sha256:56f85aaa5a83f06a3f61e680e3b50f156b5e557ebdcb964d823d86f4c108b0c8"}, 1172 | ] 1173 | flake8-breakpoint = [ 1174 | {file = "flake8-breakpoint-1.1.0.tar.gz", hash = "sha256:5bc70d478f0437a3655d094e1d2fca81ddacabaa84d99db45ad3630bf2004064"}, 1175 | {file = "flake8_breakpoint-1.1.0-py3-none-any.whl", hash = "sha256:27e0cb132647f9ef348b4a3c3126e7350bedbb22e8e221cd11712a223855ea0b"}, 1176 | ] 1177 | flake8-bugbear = [ 1178 | {file = "flake8-bugbear-20.1.4.tar.gz", hash = "sha256:bd02e4b009fb153fe6072c31c52aeab5b133d508095befb2ffcf3b41c4823162"}, 1179 | {file = "flake8_bugbear-20.1.4-py36.py37.py38-none-any.whl", hash = "sha256:a3ddc03ec28ba2296fc6f89444d1c946a6b76460f859795b35b77d4920a51b63"}, 1180 | ] 1181 | flake8-builtins = [ 1182 | {file = "flake8-builtins-1.4.2.tar.gz", hash = "sha256:c44415fb19162ef3737056e700d5b99d48c3612a533943b4e16419a5d3de3a64"}, 1183 | {file = "flake8_builtins-1.4.2-py2.py3-none-any.whl", hash = "sha256:29bc0f7e68af481d088f5c96f8aeb02520abdfc900500484e3af969f42a38a5f"}, 1184 | ] 1185 | flake8-comprehensions = [ 1186 | {file = "flake8-comprehensions-3.2.2.tar.gz", hash = "sha256:e7db586bb6eb95afdfd87ed244c90e57ae1352db8ef0ad3012fca0200421e5df"}, 1187 | {file = "flake8_comprehensions-3.2.2-py3-none-any.whl", hash = "sha256:d08323aa801aef33477cd33f2f5ce3acb1aafd26803ab0d171d85d514c1273a2"}, 1188 | ] 1189 | flake8-deprecated = [ 1190 | {file = "flake8-deprecated-1.3.tar.gz", hash = "sha256:9fa5a0c5c81fb3b34c53a0e4f16cd3f0a3395078cfd4988011cbab5fb0afa7f7"}, 1191 | {file = "flake8_deprecated-1.3-py2.py3-none-any.whl", hash = "sha256:211951854837ced9ec997a75c6e5b957f3536a735538ee0620b76539fd3706cd"}, 1192 | ] 1193 | flake8-docstrings = [ 1194 | {file = "flake8-docstrings-1.5.0.tar.gz", hash = "sha256:3d5a31c7ec6b7367ea6506a87ec293b94a0a46c0bce2bb4975b7f1d09b6f3717"}, 1195 | {file = "flake8_docstrings-1.5.0-py2.py3-none-any.whl", hash = "sha256:a256ba91bc52307bef1de59e2a009c3cf61c3d0952dbe035d6ff7208940c2edc"}, 1196 | ] 1197 | flake8-eradicate = [ 1198 | {file = "flake8-eradicate-0.2.4.tar.gz", hash = "sha256:b693e9dfe6da42dbc7fb75af8486495b9414d1ab0372d15efcf85a2ac85fd368"}, 1199 | {file = "flake8_eradicate-0.2.4-py3-none-any.whl", hash = "sha256:b0bcdbb70a489fb799f9ee11fefc57bd0d3251e1ea9bdc5bf454443cccfd620c"}, 1200 | ] 1201 | flake8-if-expr = [ 1202 | {file = "flake8-if-expr-1.0.0.tar.gz", hash = "sha256:173f6ceefdecbff532180aafe0360f6d1dd4da8b4a9b10193ddc1781291d580e"}, 1203 | {file = "flake8_if_expr-1.0.0-py3-none-any.whl", hash = "sha256:890c5bd0103c864492e7088bfaf4f9f5a987c336b03b2b285178456d08db3025"}, 1204 | ] 1205 | flake8-isort = [ 1206 | {file = "flake8-isort-2.8.0.tar.gz", hash = "sha256:64454d1f154a303cfe23ee715aca37271d4f1d299b2f2663f45b73bff14e36a9"}, 1207 | {file = "flake8_isort-2.8.0-py2.py3-none-any.whl", hash = "sha256:aa0c4d004e6be47e74f122f5b7f36554d0d78ad8bf99b497a460dedccaa7cce9"}, 1208 | ] 1209 | flake8-pie = [ 1210 | {file = "flake8-pie-0.4.2.tar.gz", hash = "sha256:7e24f7749cc701b0842901b676ca5a4b1c4e3719bcad1192c873dea70b6dee86"}, 1211 | {file = "flake8_pie-0.4.2-py3-none-any.whl", hash = "sha256:f7a821815e60023b9d50d437b97835f44ec15c11a75b20f7f832b7e6e67a7f90"}, 1212 | ] 1213 | flake8-plugin-utils = [ 1214 | {file = "flake8-plugin-utils-1.0.0.tar.gz", hash = "sha256:1ac5eb19773d5c7fdde60b0d901ae86be9c751bf697c61fdb6609b86872f3c6e"}, 1215 | {file = "flake8_plugin_utils-1.0.0-py3-none-any.whl", hash = "sha256:24b4a3b216ad588951d3d7adef4645dcb3b32a33b878e03baa790b5a66bf3a73"}, 1216 | ] 1217 | flake8-polyfill = [ 1218 | {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, 1219 | {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, 1220 | ] 1221 | flake8-print = [ 1222 | {file = "flake8-print-3.1.4.tar.gz", hash = "sha256:324f9e59a522518daa2461bacd7f82da3c34eb26a4314c2a54bd493f8b394a68"}, 1223 | ] 1224 | flake8-return = [ 1225 | {file = "flake8-return-1.1.1.tar.gz", hash = "sha256:03b920cf2784370af4447a754fb7133ce165a6ecf6d4f506a95c4032ece48d8a"}, 1226 | {file = "flake8_return-1.1.1-py3-none-any.whl", hash = "sha256:a219b619cdca3cd07dae150772f21083a11ce5280e2198acbac82bd9be0f574f"}, 1227 | ] 1228 | gitdb = [ 1229 | {file = "gitdb-4.0.2-py3-none-any.whl", hash = "sha256:284a6a4554f954d6e737cddcff946404393e030b76a282c6640df8efd6b3da5e"}, 1230 | {file = "gitdb-4.0.2.tar.gz", hash = "sha256:598e0096bb3175a0aab3a0b5aedaa18a9a25c6707e0eca0695ba1a0baf1b2150"}, 1231 | ] 1232 | gitpython = [ 1233 | {file = "GitPython-3.1.0-py3-none-any.whl", hash = "sha256:43da89427bdf18bf07f1164c6d415750693b4d50e28fc9b68de706245147b9dd"}, 1234 | {file = "GitPython-3.1.0.tar.gz", hash = "sha256:e426c3b587bd58c482f0b7fe6145ff4ac7ae6c82673fc656f489719abca6f4cb"}, 1235 | ] 1236 | h11 = [ 1237 | {file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"}, 1238 | {file = "h11-0.9.0.tar.gz", hash = "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1"}, 1239 | ] 1240 | h2 = [ 1241 | {file = "h2-3.2.0-py2.py3-none-any.whl", hash = "sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5"}, 1242 | {file = "h2-3.2.0.tar.gz", hash = "sha256:875f41ebd6f2c44781259005b157faed1a5031df3ae5aa7bcb4628a6c0782f14"}, 1243 | ] 1244 | hpack = [ 1245 | {file = "hpack-3.0.0-py2.py3-none-any.whl", hash = "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89"}, 1246 | {file = "hpack-3.0.0.tar.gz", hash = "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"}, 1247 | ] 1248 | hstspreload = [ 1249 | {file = "hstspreload-2020.2.29.tar.gz", hash = "sha256:519feba70b0d490ac161d60120923e5d1d2abf23e72eeac4ac8622f6f518c50d"}, 1250 | ] 1251 | httptools = [ 1252 | {file = "httptools-0.1.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce"}, 1253 | {file = "httptools-0.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4"}, 1254 | {file = "httptools-0.1.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6"}, 1255 | {file = "httptools-0.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c"}, 1256 | {file = "httptools-0.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a"}, 1257 | {file = "httptools-0.1.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f"}, 1258 | {file = "httptools-0.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2"}, 1259 | {file = "httptools-0.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009"}, 1260 | {file = "httptools-0.1.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"}, 1261 | {file = "httptools-0.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d"}, 1262 | {file = "httptools-0.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be"}, 1263 | {file = "httptools-0.1.1.tar.gz", hash = "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce"}, 1264 | ] 1265 | httpx = [ 1266 | {file = "httpx-0.11.1-py2.py3-none-any.whl", hash = "sha256:1d3893d3e4244c569764a6bae5c5a9fbbc4a6ec3825450b5696602af7a275576"}, 1267 | {file = "httpx-0.11.1.tar.gz", hash = "sha256:7d2bfb726eeed717953d15dddb22da9c2fcf48a4d70ba1456aa0a7faeda33cf7"}, 1268 | ] 1269 | hyperframe = [ 1270 | {file = "hyperframe-5.2.0-py2.py3-none-any.whl", hash = "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40"}, 1271 | {file = "hyperframe-5.2.0.tar.gz", hash = "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"}, 1272 | ] 1273 | identify = [ 1274 | {file = "identify-1.4.11-py2.py3-none-any.whl", hash = "sha256:1222b648251bdcb8deb240b294f450fbf704c7984e08baa92507e4ea10b436d5"}, 1275 | {file = "identify-1.4.11.tar.gz", hash = "sha256:d824ebe21f38325c771c41b08a95a761db1982f1fc0eee37c6c97df3f1636b96"}, 1276 | ] 1277 | idna = [ 1278 | {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, 1279 | {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, 1280 | ] 1281 | immutables = [ 1282 | {file = "immutables-0.11-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:bce27277a2fe91509cca69181971ab509c2ee862e8b37b09f26b64f90e8fe8fb"}, 1283 | {file = "immutables-0.11-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c7eb2d15c35c73bb168c002c6ea145b65f40131e10dede54b39db0b72849b280"}, 1284 | {file = "immutables-0.11-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2de2ec8dde1ca154f811776a8cbbeaea515c3b226c26036eab6484530eea28e0"}, 1285 | {file = "immutables-0.11-cp35-cp35m-win32.whl", hash = "sha256:e87bd941cb4dfa35f16e1ff4b2d99a2931452dcc9cfd788dc8fe513f3d38551e"}, 1286 | {file = "immutables-0.11-cp35-cp35m-win_amd64.whl", hash = "sha256:0aa055c745510238cbad2f1f709a37a1c9e30a38594de3b385e9876c48a25633"}, 1287 | {file = "immutables-0.11-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:422c7d4c75c88057c625e32992248329507bca180b48cfb702b4ef608f581b50"}, 1288 | {file = "immutables-0.11-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:f5b93248552c9e7198558776da21c9157d3f70649905d7fdc083c2ab2fbc6088"}, 1289 | {file = "immutables-0.11-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:b268422a5802fbf934152b835329ac0d23b80b558eaee68034d45718edab4a11"}, 1290 | {file = "immutables-0.11-cp36-cp36m-win32.whl", hash = "sha256:0f07c58122e1ce70a7165e68e18e795ac5fe94d7fee3e045ffcf6432602026df"}, 1291 | {file = "immutables-0.11-cp36-cp36m-win_amd64.whl", hash = "sha256:b8fed714f1c84a3242c7184838f5e9889139a22bbdd701a182b7fdc237ca3cbb"}, 1292 | {file = "immutables-0.11-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:518f20945c1f600b618fb691922c2ab43b193f04dd2d4d2823220d0202014670"}, 1293 | {file = "immutables-0.11-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2c536ff2bafeeff9a7865ea10a17a50f90b80b585e31396c349e8f57b0075bd4"}, 1294 | {file = "immutables-0.11-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1c2e729aab250be0de0c13fa833241a778b51390ee2650e0457d1e45b318c441"}, 1295 | {file = "immutables-0.11-cp37-cp37m-win32.whl", hash = "sha256:545186faab9237c102b8bcffd36d71f0b382174c93c501e061de239753cff694"}, 1296 | {file = "immutables-0.11-cp37-cp37m-win_amd64.whl", hash = "sha256:6b6d8d035e5888baad3db61dfb167476838a63afccecd927c365f228bb55754c"}, 1297 | {file = "immutables-0.11.tar.gz", hash = "sha256:d6850578a0dc6530ac19113cfe4ddc13903df635212d498f176fe601a8a5a4a3"}, 1298 | ] 1299 | importlib-metadata = [ 1300 | {file = "importlib_metadata-1.5.0-py2.py3-none-any.whl", hash = "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b"}, 1301 | {file = "importlib_metadata-1.5.0.tar.gz", hash = "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302"}, 1302 | ] 1303 | importlib-resources = [ 1304 | {file = "importlib_resources-1.1.0-py2.py3-none-any.whl", hash = "sha256:57919feb41464d207fd44cbb9359bd2d9ee7fb3ab327e1686662f3f5a973412f"}, 1305 | {file = "importlib_resources-1.1.0.tar.gz", hash = "sha256:44bbe129a4ff27fcc0bae81f10f411bb011015b9afb1f0dde6234724d96966ae"}, 1306 | ] 1307 | inquirer = [ 1308 | {file = "inquirer-2.6.3-py2.py3-none-any.whl", hash = "sha256:c77fd8c3c053e1b4aa7ac1e0300cbdec5fe887e144d7bdb40f9f97f96a0eb909"}, 1309 | {file = "inquirer-2.6.3.tar.gz", hash = "sha256:5f6e5dcbc881f43554b6fdfea245e417c6ed05c930cdb6e09b5df7357c288e06"}, 1310 | ] 1311 | isort = [ 1312 | {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, 1313 | {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, 1314 | ] 1315 | loguru = [ 1316 | {file = "loguru-0.4.1-py3-none-any.whl", hash = "sha256:074b3caa6748452c1e4f2b302093c94b65d5a4c5a4d7743636b4121e06437b0e"}, 1317 | {file = "loguru-0.4.1.tar.gz", hash = "sha256:a6101fd435ac89ba5205a105a26a6ede9e4ddbb4408a6e167852efca47806d11"}, 1318 | ] 1319 | mccabe = [ 1320 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 1321 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 1322 | ] 1323 | nodeenv = [ 1324 | {file = "nodeenv-1.3.5-py2.py3-none-any.whl", hash = "sha256:5b2438f2e42af54ca968dd1b374d14a1194848955187b0e5e4be1f73813a5212"}, 1325 | ] 1326 | pathspec = [ 1327 | {file = "pathspec-0.7.0-py2.py3-none-any.whl", hash = "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424"}, 1328 | {file = "pathspec-0.7.0.tar.gz", hash = "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"}, 1329 | ] 1330 | pbr = [ 1331 | {file = "pbr-5.4.4-py2.py3-none-any.whl", hash = "sha256:61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488"}, 1332 | {file = "pbr-5.4.4.tar.gz", hash = "sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b"}, 1333 | ] 1334 | pep8-naming = [ 1335 | {file = "pep8-naming-0.9.1.tar.gz", hash = "sha256:a33d38177056321a167decd6ba70b890856ba5025f0a8eca6a3eda607da93caf"}, 1336 | {file = "pep8_naming-0.9.1-py2.py3-none-any.whl", hash = "sha256:45f330db8fcfb0fba57458c77385e288e7a3be1d01e8ea4268263ef677ceea5f"}, 1337 | ] 1338 | pre-commit = [ 1339 | {file = "pre_commit-1.21.0-py2.py3-none-any.whl", hash = "sha256:f92a359477f3252452ae2e8d3029de77aec59415c16ae4189bcfba40b757e029"}, 1340 | {file = "pre_commit-1.21.0.tar.gz", hash = "sha256:8f48d8637bdae6fa70cc97db9c1dd5aa7c5c8bf71968932a380628c25978b850"}, 1341 | ] 1342 | psutil = [ 1343 | {file = "psutil-5.7.2-cp27-none-win32.whl", hash = "sha256:f2018461733b23f308c298653c8903d32aaad7873d25e1d228765e91ae42c3f2"}, 1344 | {file = "psutil-5.7.2-cp27-none-win_amd64.whl", hash = "sha256:66c18ca7680a31bf16ee22b1d21b6397869dda8059dbdb57d9f27efa6615f195"}, 1345 | {file = "psutil-5.7.2-cp35-cp35m-win32.whl", hash = "sha256:5e9d0f26d4194479a13d5f4b3798260c20cecf9ac9a461e718eb59ea520a360c"}, 1346 | {file = "psutil-5.7.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4080869ed93cce662905b029a1770fe89c98787e543fa7347f075ade761b19d6"}, 1347 | {file = "psutil-5.7.2-cp36-cp36m-win32.whl", hash = "sha256:d8a82162f23c53b8525cf5f14a355f5d1eea86fa8edde27287dd3a98399e4fdf"}, 1348 | {file = "psutil-5.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:0ee3c36428f160d2d8fce3c583a0353e848abb7de9732c50cf3356dd49ad63f8"}, 1349 | {file = "psutil-5.7.2-cp37-cp37m-win32.whl", hash = "sha256:ff1977ba1a5f71f89166d5145c3da1cea89a0fdb044075a12c720ee9123ec818"}, 1350 | {file = "psutil-5.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a5b120bb3c0c71dfe27551f9da2f3209a8257a178ed6c628a819037a8df487f1"}, 1351 | {file = "psutil-5.7.2-cp38-cp38-win32.whl", hash = "sha256:10512b46c95b02842c225f58fa00385c08fa00c68bac7da2d9a58ebe2c517498"}, 1352 | {file = "psutil-5.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:68d36986ded5dac7c2dcd42f2682af1db80d4bce3faa126a6145c1637e1b559f"}, 1353 | {file = "psutil-5.7.2.tar.gz", hash = "sha256:90990af1c3c67195c44c9a889184f84f5b2320dce3ee3acbd054e3ba0b4a7beb"}, 1354 | ] 1355 | pycodestyle = [ 1356 | {file = "pycodestyle-2.5.0-py2.py3-none-any.whl", hash = "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56"}, 1357 | {file = "pycodestyle-2.5.0.tar.gz", hash = "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"}, 1358 | ] 1359 | pycparser = [ 1360 | {file = "pycparser-2.19.tar.gz", hash = "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"}, 1361 | ] 1362 | pydantic = [ 1363 | {file = "pydantic-1.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:07911aab70f3bc52bb845ce1748569c5e70478ac977e106a150dd9d0465ebf04"}, 1364 | {file = "pydantic-1.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:012c422859bac2e03ab3151ea6624fecf0e249486be7eb8c6ee69c91740c6752"}, 1365 | {file = "pydantic-1.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:61d22d36808087d3184ed6ac0d91dd71c533b66addb02e4a9930e1e30833202f"}, 1366 | {file = "pydantic-1.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f863456d3d4bf817f2e5248553dee3974c5dc796f48e6ddb599383570f4215ac"}, 1367 | {file = "pydantic-1.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:bbbed364376f4a0aebb9ea452ff7968b306499a9e74f4db69b28ff2cd4043a11"}, 1368 | {file = "pydantic-1.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e27559cedbd7f59d2375bfd6eea29a330ea1a5b0589c34d6b4e0d7bec6027bbf"}, 1369 | {file = "pydantic-1.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:50e4e948892a6815649ad5a9a9379ad1e5f090f17842ac206535dfaed75c6f2f"}, 1370 | {file = "pydantic-1.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:8848b4eb458469739126e4c1a202d723dd092e087f8dbe3104371335f87ba5df"}, 1371 | {file = "pydantic-1.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:831a0265a9e3933b3d0f04d1a81bba543bafbe4119c183ff2771871db70524ab"}, 1372 | {file = "pydantic-1.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:47b8db7024ba3d46c3d4768535e1cf87b6c8cf92ccd81e76f4e1cb8ee47688b3"}, 1373 | {file = "pydantic-1.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:51f11c8bbf794a68086540da099aae4a9107447c7a9d63151edbb7d50110cf21"}, 1374 | {file = "pydantic-1.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:6100d7862371115c40be55cc4b8d766a74b1d0dbaf99dbfe72bb4bac0faf89ed"}, 1375 | {file = "pydantic-1.4-py36.py37.py38-none-any.whl", hash = "sha256:72184c1421103cca128300120f8f1185fb42a9ea73a1c9845b1c53db8c026a7d"}, 1376 | {file = "pydantic-1.4.tar.gz", hash = "sha256:f17ec336e64d4583311249fb179528e9a2c27c8a2eaf590ec6ec2c6dece7cb3f"}, 1377 | ] 1378 | pydocstyle = [ 1379 | {file = "pydocstyle-5.0.2-py3-none-any.whl", hash = "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586"}, 1380 | {file = "pydocstyle-5.0.2.tar.gz", hash = "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5"}, 1381 | ] 1382 | pyflakes = [ 1383 | {file = "pyflakes-2.1.1-py2.py3-none-any.whl", hash = "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0"}, 1384 | {file = "pyflakes-2.1.1.tar.gz", hash = "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"}, 1385 | ] 1386 | pyjwt = [ 1387 | {file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"}, 1388 | {file = "PyJWT-1.7.1.tar.gz", hash = "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"}, 1389 | ] 1390 | python-editor = [ 1391 | {file = "python-editor-1.0.4.tar.gz", hash = "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b"}, 1392 | {file = "python_editor-1.0.4-py2-none-any.whl", hash = "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"}, 1393 | {file = "python_editor-1.0.4-py2.7.egg", hash = "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522"}, 1394 | {file = "python_editor-1.0.4-py3-none-any.whl", hash = "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d"}, 1395 | {file = "python_editor-1.0.4-py3.5.egg", hash = "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77"}, 1396 | ] 1397 | pyyaml = [ 1398 | {file = "PyYAML-5.3-cp27-cp27m-win32.whl", hash = "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d"}, 1399 | {file = "PyYAML-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6"}, 1400 | {file = "PyYAML-5.3-cp35-cp35m-win32.whl", hash = "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e"}, 1401 | {file = "PyYAML-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689"}, 1402 | {file = "PyYAML-5.3-cp36-cp36m-win32.whl", hash = "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994"}, 1403 | {file = "PyYAML-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e"}, 1404 | {file = "PyYAML-5.3-cp37-cp37m-win32.whl", hash = "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5"}, 1405 | {file = "PyYAML-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf"}, 1406 | {file = "PyYAML-5.3-cp38-cp38-win32.whl", hash = "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811"}, 1407 | {file = "PyYAML-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20"}, 1408 | {file = "PyYAML-5.3.tar.gz", hash = "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"}, 1409 | ] 1410 | readchar = [ 1411 | {file = "readchar-2.0.1-py2-none-any.whl", hash = "sha256:ed00b7a49bb12f345319d9fa393f289f03670310ada2beb55e8c3f017c648f1e"}, 1412 | {file = "readchar-2.0.1-py3-none-any.whl", hash = "sha256:3ac34aab28563bc895f73233d5c08b28f951ca190d5850b8d4bec973132a8dca"}, 1413 | ] 1414 | regex = [ 1415 | {file = "regex-2020.2.20-cp27-cp27m-win32.whl", hash = "sha256:99272d6b6a68c7ae4391908fc15f6b8c9a6c345a46b632d7fdb7ef6c883a2bbb"}, 1416 | {file = "regex-2020.2.20-cp27-cp27m-win_amd64.whl", hash = "sha256:974535648f31c2b712a6b2595969f8ab370834080e00ab24e5dbb9d19b8bfb74"}, 1417 | {file = "regex-2020.2.20-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5de40649d4f88a15c9489ed37f88f053c15400257eeb18425ac7ed0a4e119400"}, 1418 | {file = "regex-2020.2.20-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:82469a0c1330a4beb3d42568f82dffa32226ced006e0b063719468dcd40ffdf0"}, 1419 | {file = "regex-2020.2.20-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d58a4fa7910102500722defbde6e2816b0372a4fcc85c7e239323767c74f5cbc"}, 1420 | {file = "regex-2020.2.20-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f1ac2dc65105a53c1c2d72b1d3e98c2464a133b4067a51a3d2477b28449709a0"}, 1421 | {file = "regex-2020.2.20-cp36-cp36m-win32.whl", hash = "sha256:8c2b7fa4d72781577ac45ab658da44c7518e6d96e2a50d04ecb0fd8f28b21d69"}, 1422 | {file = "regex-2020.2.20-cp36-cp36m-win_amd64.whl", hash = "sha256:269f0c5ff23639316b29f31df199f401e4cb87529eafff0c76828071635d417b"}, 1423 | {file = "regex-2020.2.20-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:bed7986547ce54d230fd8721aba6fd19459cdc6d315497b98686d0416efaff4e"}, 1424 | {file = "regex-2020.2.20-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:046e83a8b160aff37e7034139a336b660b01dbfe58706f9d73f5cdc6b3460242"}, 1425 | {file = "regex-2020.2.20-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:b33ebcd0222c1d77e61dbcd04a9fd139359bded86803063d3d2d197b796c63ce"}, 1426 | {file = "regex-2020.2.20-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bba52d72e16a554d1894a0cc74041da50eea99a8483e591a9edf1025a66843ab"}, 1427 | {file = "regex-2020.2.20-cp37-cp37m-win32.whl", hash = "sha256:01b2d70cbaed11f72e57c1cfbaca71b02e3b98f739ce33f5f26f71859ad90431"}, 1428 | {file = "regex-2020.2.20-cp37-cp37m-win_amd64.whl", hash = "sha256:113309e819634f499d0006f6200700c8209a2a8bf6bd1bdc863a4d9d6776a5d1"}, 1429 | {file = "regex-2020.2.20-cp38-cp38-manylinux1_i686.whl", hash = "sha256:25f4ce26b68425b80a233ce7b6218743c71cf7297dbe02feab1d711a2bf90045"}, 1430 | {file = "regex-2020.2.20-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9b64a4cc825ec4df262050c17e18f60252cdd94742b4ba1286bcfe481f1c0f26"}, 1431 | {file = "regex-2020.2.20-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:9ff16d994309b26a1cdf666a6309c1ef51ad4f72f99d3392bcd7b7139577a1f2"}, 1432 | {file = "regex-2020.2.20-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c7f58a0e0e13fb44623b65b01052dae8e820ed9b8b654bb6296bc9c41f571b70"}, 1433 | {file = "regex-2020.2.20-cp38-cp38-win32.whl", hash = "sha256:200539b5124bc4721247a823a47d116a7a23e62cc6695744e3eb5454a8888e6d"}, 1434 | {file = "regex-2020.2.20-cp38-cp38-win_amd64.whl", hash = "sha256:7f78f963e62a61e294adb6ff5db901b629ef78cb2a1cfce3cf4eeba80c1c67aa"}, 1435 | {file = "regex-2020.2.20.tar.gz", hash = "sha256:9e9624440d754733eddbcd4614378c18713d2d9d0dc647cf9c72f64e39671be5"}, 1436 | ] 1437 | rfc3986 = [ 1438 | {file = "rfc3986-1.3.2-py2.py3-none-any.whl", hash = "sha256:df4eba676077cefb86450c8f60121b9ae04b94f65f85b69f3f731af0516b7b18"}, 1439 | {file = "rfc3986-1.3.2.tar.gz", hash = "sha256:0344d0bd428126ce554e7ca2b61787b6a28d2bbd19fc70ed2dd85efe31176405"}, 1440 | ] 1441 | six = [ 1442 | {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, 1443 | {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, 1444 | ] 1445 | smmap = [ 1446 | {file = "smmap-3.0.1-py2.py3-none-any.whl", hash = "sha256:5fead614cf2de17ee0707a8c6a5f2aa5a2fc6c698c70993ba42f515485ffda78"}, 1447 | {file = "smmap-3.0.1.tar.gz", hash = "sha256:171484fe62793e3626c8b05dd752eb2ca01854b0c55a1efc0dc4210fccb65446"}, 1448 | ] 1449 | sniffio = [ 1450 | {file = "sniffio-1.1.0-py3-none-any.whl", hash = "sha256:20ed6d5b46f8ae136d00b9dcb807615d83ed82ceea6b2058cecb696765246da5"}, 1451 | {file = "sniffio-1.1.0.tar.gz", hash = "sha256:8e3810100f69fe0edd463d02ad407112542a11ffdc29f67db2bf3771afb87a21"}, 1452 | ] 1453 | snowballstemmer = [ 1454 | {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, 1455 | {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, 1456 | ] 1457 | stackprinter = [ 1458 | {file = "stackprinter-0.2.3-py3-none-any.whl", hash = "sha256:a21590e1c8fc4aad1e97e89df2bcf86dcaf55f47b1bbb4dfd209361d28fd9d68"}, 1459 | {file = "stackprinter-0.2.3.tar.gz", hash = "sha256:8d050d86f98d1a054da125733c998fed6020c1e078628d616c75701496ebd0b8"}, 1460 | ] 1461 | starlette = [ 1462 | {file = "starlette-0.12.9.tar.gz", hash = "sha256:c2ac9a42e0e0328ad20fe444115ac5e3760c1ee2ac1ff8cdb5ec915c4a453411"}, 1463 | ] 1464 | stevedore = [ 1465 | {file = "stevedore-1.32.0-py2.py3-none-any.whl", hash = "sha256:a4e7dc759fb0f2e3e2f7d8ffe2358c19d45b9b8297f393ef1256858d82f69c9b"}, 1466 | {file = "stevedore-1.32.0.tar.gz", hash = "sha256:18afaf1d623af5950cc0f7e75e70f917784c73b652a34a12d90b309451b5500b"}, 1467 | ] 1468 | testfixtures = [ 1469 | {file = "testfixtures-6.14.0-py2.py3-none-any.whl", hash = "sha256:799144b3cbef7b072452d9c36cbd024fef415ab42924b96aad49dfd9c763de66"}, 1470 | {file = "testfixtures-6.14.0.tar.gz", hash = "sha256:cdfc3d73cb6d3d4dc3c67af84d912e86bf117d30ae25f02fe823382ef99383d2"}, 1471 | ] 1472 | toml = [ 1473 | {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, 1474 | {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, 1475 | {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, 1476 | ] 1477 | typed-ast = [ 1478 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, 1479 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, 1480 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, 1481 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, 1482 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, 1483 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, 1484 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, 1485 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, 1486 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, 1487 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, 1488 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, 1489 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, 1490 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, 1491 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, 1492 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, 1493 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, 1494 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, 1495 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, 1496 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, 1497 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, 1498 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, 1499 | ] 1500 | urllib3 = [ 1501 | {file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"}, 1502 | {file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"}, 1503 | ] 1504 | uvicorn = [ 1505 | {file = "uvicorn-0.11.3-py3-none-any.whl", hash = "sha256:0f58170165c4495f563d8224b2f415a0829af0412baa034d6f777904613087fd"}, 1506 | {file = "uvicorn-0.11.3.tar.gz", hash = "sha256:6fdaf8e53bf1b2ddf0fe9ed06079b5348d7d1d87b3365fe2549e6de0d49e631c"}, 1507 | ] 1508 | uvloop = [ 1509 | {file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"}, 1510 | {file = "uvloop-0.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726"}, 1511 | {file = "uvloop-0.14.0-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7"}, 1512 | {file = "uvloop-0.14.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"}, 1513 | {file = "uvloop-0.14.0-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891"}, 1514 | {file = "uvloop-0.14.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95"}, 1515 | {file = "uvloop-0.14.0-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5"}, 1516 | {file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"}, 1517 | {file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"}, 1518 | ] 1519 | virtualenv = [ 1520 | {file = "virtualenv-20.0.7-py2.py3-none-any.whl", hash = "sha256:30ea90b21dabd11da5f509710ad3be2ae47d40ccbc717dfdd2efe4367c10f598"}, 1521 | {file = "virtualenv-20.0.7.tar.gz", hash = "sha256:4a36a96d785428278edd389d9c36d763c5755844beb7509279194647b1ef47f1"}, 1522 | ] 1523 | websockets = [ 1524 | {file = "websockets-8.0.2-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:e906128532a14b9d264a43eb48f9b3080d53a9bda819ab45bf56b8039dc606ac"}, 1525 | {file = "websockets-8.0.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:83e63aa73331b9ca21af61df8f115fb5fbcba3f281bee650a4ad16a40cd1ef15"}, 1526 | {file = "websockets-8.0.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e9102043a81cdc8b7c8032ff4bce39f6229e4ac39cb2010946c912eeb84e2cb6"}, 1527 | {file = "websockets-8.0.2-cp36-cp36m-win32.whl", hash = "sha256:8d7a20a2f97f1e98c765651d9fb9437201a9ccc2c70e94b0270f1c5ef29667a3"}, 1528 | {file = "websockets-8.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:c82e286555f839846ef4f0fdd6910769a577952e1e26aa8ee7a6f45f040e3c2b"}, 1529 | {file = "websockets-8.0.2-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:73ce69217e4655783ec72ce11c151053fcbd5b837cc39de7999e19605182e28a"}, 1530 | {file = "websockets-8.0.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8c77f7d182a6ea2a9d09c2612059f3ad859a90243e899617137ee3f6b7f2b584"}, 1531 | {file = "websockets-8.0.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a7affaeffbc5d55681934c16bb6b8fc82bb75b175e7fd4dcca798c938bde8dda"}, 1532 | {file = "websockets-8.0.2-cp37-cp37m-win32.whl", hash = "sha256:f5cb2683367e32da6a256b60929a3af9c29c212b5091cf5bace9358d03011bf5"}, 1533 | {file = "websockets-8.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:049e694abe33f8a1d99969fee7bfc0ae6761f7fd5f297c58ea933b27dd6805f2"}, 1534 | {file = "websockets-8.0.2.tar.gz", hash = "sha256:882a7266fa867a2ebb2c0baaa0f9159cabf131cf18c1b4270d79ad42f9208dc5"}, 1535 | ] 1536 | win32-setctime = [ 1537 | {file = "win32_setctime-1.0.1-py3-none-any.whl", hash = "sha256:568fd636c68350bcc54755213fe01966fe0a6c90b386c0776425944a0382abef"}, 1538 | {file = "win32_setctime-1.0.1.tar.gz", hash = "sha256:b47e5023ec7f0b4962950902b15bc56464a380d869f59d27dbf9ab423b23e8f9"}, 1539 | ] 1540 | zipp = [ 1541 | {file = "zipp-3.0.0-py3-none-any.whl", hash = "sha256:12248a63bbdf7548f89cb4c7cda4681e537031eda29c02ea29674bc6854460c2"}, 1542 | {file = "zipp-3.0.0.tar.gz", hash = "sha256:7c0f8e91abc0dc07a5068f315c52cb30c66bfbc581e5b50704c8a2f6ebae794a"}, 1543 | ] 1544 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "hyperglass-agent" 3 | version = "0.1.6" 4 | description = "The Linux Routing Agent for hyperglass" 5 | authors = ["Matt Love "] 6 | readme = "README.md" 7 | homepage = "https://hyperglass.io" 8 | repository = "https://github.com/checktheroads/hyperglass-agent" 9 | license = "BSD-3-Clause-Clear" 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.6" 13 | click = "^7.0" 14 | cryptography = "^2.8" 15 | fastapi = "^0.45.0" 16 | httpx = "^0.11" 17 | loguru = "^0.4.0" 18 | pydantic = "^1.3" 19 | pyjwt = "^1.7.1" 20 | pyyaml = "^5.2" 21 | stackprinter = "^0.2.3" 22 | uvloop = "^0.14.0" 23 | uvicorn = "^0.11.1" 24 | inquirer = "^2.6.3" 25 | psutil = "^5.7.2" 26 | 27 | [tool.poetry.dev-dependencies] 28 | black = "^19.10b0" 29 | isort = "^4.3.21" 30 | bandit = "^1.6.2" 31 | flake8 = "^3.7.9" 32 | flake8-bandit = "^2.1.2" 33 | flake8-black = "^0.1.1" 34 | flake8-breakpoint = "^1.1.0" 35 | flake8-bugbear = "^20.1.0" 36 | flake8-builtins = "^1.4.2" 37 | flake8-comprehensions = "^3.1.4" 38 | flake8-deprecated = "^1.3" 39 | flake8-eradicate = "^0.2.4" 40 | flake8-if-expr = "^1.0.0" 41 | flake8-isort = "^2.8.0" 42 | flake8-pie = "^0.4.2" 43 | flake8-plugin-utils = "^1.0.0" 44 | flake8-polyfill = "^1.0.2" 45 | flake8-print = "^3.1.4" 46 | flake8-return = "^1.1.1" 47 | pep8-naming = "^0.9.1" 48 | flake8-docstrings = "^1.5.0" 49 | pre-commit = "^1.21.0" 50 | mccabe = "^0.6.1" 51 | 52 | [tool.poetry.scripts] 53 | hyperglass-agent = "hyperglass_agent.console:cli" 54 | 55 | [tool.black] 56 | line-length = 88 57 | 58 | [build-system] 59 | requires = ["poetry>=0.12"] 60 | build-backend = "poetry.masonry.api" 61 | 62 | --------------------------------------------------------------------------------