├── .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 |
--------------------------------------------------------------------------------