├── mepe ├── __init__.py ├── exceptions.py ├── main.py ├── scrape.py └── display.py ├── docs └── mepe.png ├── Makefile ├── .bumpversion.cfg ├── README.md ├── pyproject.toml ├── LICENSE ├── .gitignore └── poetry.lock /mepe/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = "0.1.4" 2 | -------------------------------------------------------------------------------- /docs/mepe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laixintao/mepe/HEAD/docs/mepe.png -------------------------------------------------------------------------------- /mepe/exceptions.py: -------------------------------------------------------------------------------- 1 | class MepeException(Exception): 2 | pass 3 | 4 | 5 | class MetricsFetchException(MepeException): 6 | """Can't fetch metrics""" 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | bump_patch: 2 | bumpversion patch 3 | 4 | bump_minor: 5 | bumpversion minor 6 | 7 | patch: bump_patch 8 | rm -rf dist 9 | poetry build 10 | poetry publish 11 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.4 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:mepe/__init__.py] 7 | 8 | [bumpversion:file:pyproject.toml] 9 | search = version = "{current_version}" 10 | replace = version = "{new_version}" 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prometheus Metrics Explorer 2 | 3 | Cli Prometheus metrics viewer. 4 | 5 | Summaries metrics, useful when you configure a new Grafana dashboard for a new component, and want to check what metrics does it have. 6 | 7 | ## Installation 8 | 9 | ```shell 10 | pip install mepe 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```shell 16 | mepe http://127.0.0.1:9100/metrics 17 | ``` 18 | 19 | 20 | ![](./docs/mepe.png) 21 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "mepe" 3 | version = "0.1.4" 4 | description = "" 5 | authors = ["laixintao "] 6 | readme = "README.md" 7 | packages = [{include = "mepe"}] 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.8" 11 | click = "^8.1.3" 12 | rich = "^12.6.0" 13 | prometheus-client = "^0.15.0" 14 | requests = "^2.28.1" 15 | 16 | 17 | [tool.poetry.group.dev.dependencies] 18 | ipdb = "^0.13.9" 19 | 20 | [build-system] 21 | requires = ["poetry-core"] 22 | build-backend = "poetry.core.masonry.api" 23 | 24 | [tool.poetry.scripts] 25 | mepe = 'mepe.main:main' 26 | -------------------------------------------------------------------------------- /mepe/main.py: -------------------------------------------------------------------------------- 1 | import logging, os 2 | import click 3 | from .scrape import fetch_metrics_url 4 | from .display import display 5 | 6 | 7 | def config_log(level, location): 8 | logging.basicConfig( 9 | level=level, 10 | filename=os.path.expanduser(location), 11 | filemode="a", 12 | format="[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s", 13 | ) 14 | 15 | 16 | @click.command() 17 | @click.argument("metrics-url") 18 | @click.option( 19 | "--log-level", help="CRITICAL ERROR WARNING INFO DEBUG NOTSET", default="DEBUG" 20 | ) 21 | @click.option("--log-file", help="File location to log to", default=None) 22 | def main(metrics_url, log_level, log_file): 23 | if log_file: 24 | config_log(logging.getLevelName(log_level), log_file) 25 | logger = logging.getLogger(__name__) 26 | logger.debug(f"Logging configured with level={log_level}") 27 | logger.debug(f"Now fetching metrics url={metrics_url}") 28 | metrics = fetch_metrics_url(metrics_url) 29 | display(metrics) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 laixintao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mepe/scrape.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import requests 3 | from . import exceptions 4 | import prometheus_client 5 | from prometheus_client.parser import text_string_to_metric_families 6 | from typing import List 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def fetch_metrics_url(url) -> List["prometheus_client.metrics_core.Metric"]: 12 | resp = requests.get(url) 13 | 14 | if resp.status_code != 200: 15 | raise exceptions.MetricsFetchException( 16 | f"Can not fetch exceptions, status_code={resp.status_code}," 17 | f" error={resp.text}" 18 | ) 19 | 20 | metrics_text = resp.text 21 | 22 | logger.info(f"Successfully fetch the metrics, {len(metrics_text)=}, parsing...") 23 | 24 | metrics = text_string_to_metric_families(metrics_text) 25 | # merge metrics with the same name, it seems that prometheus_client didn't 26 | # merge them correctly 27 | 28 | merge_dict = {} 29 | for m in metrics: 30 | merge_dict.setdefault(m.name, []).append(m) 31 | 32 | unique_metrics = list() 33 | for values in merge_dict.values(): 34 | samples = [] 35 | for m in values: 36 | samples.extend(m.samples) 37 | 38 | selected_metric = _select_metric(values) 39 | selected_metric.samples = samples 40 | unique_metrics.append(selected_metric) 41 | return unique_metrics 42 | 43 | 44 | def _select_metric(values): 45 | for v in values: 46 | if v.type != "unknown": 47 | return v 48 | return values[0] 49 | -------------------------------------------------------------------------------- /mepe/display.py: -------------------------------------------------------------------------------- 1 | import re 2 | import logging 3 | from typing import List 4 | import prometheus_client 5 | from rich.console import Console 6 | from rich.text import Text 7 | 8 | console = Console() 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def display(metrics: List["prometheus_client.metrics_core.Metric"]) -> None: 13 | metrics = sorted(metrics, key=lambda m: m.name) 14 | 15 | for metric in metrics: 16 | display_metric(metric) 17 | 18 | 19 | def display_metric(metric: "prometheus_client.metrics_core.Metric") -> None: 20 | 21 | metric_name = metric.samples[0].name 22 | 23 | name = Text(metric_name) 24 | 25 | # bold the common prefix which indicates the metrics group 26 | 27 | name.stylize("yellow") 28 | 29 | matcher = re.match(r"(.*?)_", metric.name) 30 | if matcher: 31 | prefix = len(matcher.group(1)) 32 | name.stylize("bold", 0, prefix) 33 | 34 | _type = Text(metric.type) 35 | _type.stylize("blue") 36 | 37 | unit = Text(metric.unit) 38 | unit.stylize("blue") 39 | 40 | total_samples = Text(f"{len(metric.samples)} total samples") 41 | total_samples.stylize("red") 42 | 43 | console.print(name, _type, unit, total_samples) 44 | 45 | if metric.documentation: 46 | doc = Text(metric.documentation) 47 | doc.stylize("green") 48 | console.print(" ", doc) 49 | 50 | labels = set() 51 | for sample in metric.samples: 52 | labels.update(sample.labels.keys()) 53 | labels_text = Text(",".join(labels)) 54 | if labels_text: 55 | labels_text.stylize("grey54") 56 | console.print(" ", labels_text) 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appnope" 3 | version = "0.1.3" 4 | description = "Disable App Nap on macOS >= 10.9" 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "asttokens" 11 | version = "2.2.1" 12 | description = "Annotate AST trees with source code positions" 13 | category = "dev" 14 | optional = false 15 | python-versions = "*" 16 | 17 | [package.dependencies] 18 | six = "*" 19 | 20 | [package.extras] 21 | test = ["astroid", "pytest"] 22 | 23 | [[package]] 24 | name = "backcall" 25 | version = "0.2.0" 26 | description = "Specifications for callback functions passed in to an API" 27 | category = "dev" 28 | optional = false 29 | python-versions = "*" 30 | 31 | [[package]] 32 | name = "certifi" 33 | version = "2022.12.7" 34 | description = "Python package for providing Mozilla's CA Bundle." 35 | category = "main" 36 | optional = false 37 | python-versions = ">=3.6" 38 | 39 | [[package]] 40 | name = "charset-normalizer" 41 | version = "2.1.1" 42 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 43 | category = "main" 44 | optional = false 45 | python-versions = ">=3.6.0" 46 | 47 | [package.extras] 48 | unicode-backport = ["unicodedata2"] 49 | 50 | [[package]] 51 | name = "click" 52 | version = "8.1.3" 53 | description = "Composable command line interface toolkit" 54 | category = "main" 55 | optional = false 56 | python-versions = ">=3.7" 57 | 58 | [package.dependencies] 59 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 60 | 61 | [[package]] 62 | name = "colorama" 63 | version = "0.4.6" 64 | description = "Cross-platform colored terminal text." 65 | category = "main" 66 | optional = false 67 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 68 | 69 | [[package]] 70 | name = "commonmark" 71 | version = "0.9.1" 72 | description = "Python parser for the CommonMark Markdown spec" 73 | category = "main" 74 | optional = false 75 | python-versions = "*" 76 | 77 | [package.extras] 78 | test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] 79 | 80 | [[package]] 81 | name = "decorator" 82 | version = "5.1.1" 83 | description = "Decorators for Humans" 84 | category = "dev" 85 | optional = false 86 | python-versions = ">=3.5" 87 | 88 | [[package]] 89 | name = "executing" 90 | version = "1.2.0" 91 | description = "Get the currently executing AST node of a frame, and other information" 92 | category = "dev" 93 | optional = false 94 | python-versions = "*" 95 | 96 | [package.extras] 97 | tests = ["asttokens", "littleutils", "pytest", "rich"] 98 | 99 | [[package]] 100 | name = "idna" 101 | version = "3.4" 102 | description = "Internationalized Domain Names in Applications (IDNA)" 103 | category = "main" 104 | optional = false 105 | python-versions = ">=3.5" 106 | 107 | [[package]] 108 | name = "ipdb" 109 | version = "0.13.9" 110 | description = "IPython-enabled pdb" 111 | category = "dev" 112 | optional = false 113 | python-versions = ">=2.7" 114 | 115 | [package.dependencies] 116 | decorator = {version = "*", markers = "python_version > \"3.6\""} 117 | ipython = {version = ">=7.17.0", markers = "python_version > \"3.6\""} 118 | setuptools = "*" 119 | toml = {version = ">=0.10.2", markers = "python_version > \"3.6\""} 120 | 121 | [[package]] 122 | name = "ipython" 123 | version = "8.7.0" 124 | description = "IPython: Productive Interactive Computing" 125 | category = "dev" 126 | optional = false 127 | python-versions = ">=3.8" 128 | 129 | [package.dependencies] 130 | appnope = {version = "*", markers = "sys_platform == \"darwin\""} 131 | backcall = "*" 132 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 133 | decorator = "*" 134 | jedi = ">=0.16" 135 | matplotlib-inline = "*" 136 | pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} 137 | pickleshare = "*" 138 | prompt-toolkit = ">=3.0.11,<3.1.0" 139 | pygments = ">=2.4.0" 140 | stack-data = "*" 141 | traitlets = ">=5" 142 | 143 | [package.extras] 144 | all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.20)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] 145 | black = ["black"] 146 | doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] 147 | kernel = ["ipykernel"] 148 | nbconvert = ["nbconvert"] 149 | nbformat = ["nbformat"] 150 | notebook = ["ipywidgets", "notebook"] 151 | parallel = ["ipyparallel"] 152 | qtconsole = ["qtconsole"] 153 | test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] 154 | test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.20)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] 155 | 156 | [[package]] 157 | name = "jedi" 158 | version = "0.18.2" 159 | description = "An autocompletion tool for Python that can be used for text editors." 160 | category = "dev" 161 | optional = false 162 | python-versions = ">=3.6" 163 | 164 | [package.dependencies] 165 | parso = ">=0.8.0,<0.9.0" 166 | 167 | [package.extras] 168 | docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] 169 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 170 | testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] 171 | 172 | [[package]] 173 | name = "matplotlib-inline" 174 | version = "0.1.6" 175 | description = "Inline Matplotlib backend for Jupyter" 176 | category = "dev" 177 | optional = false 178 | python-versions = ">=3.5" 179 | 180 | [package.dependencies] 181 | traitlets = "*" 182 | 183 | [[package]] 184 | name = "parso" 185 | version = "0.8.3" 186 | description = "A Python Parser" 187 | category = "dev" 188 | optional = false 189 | python-versions = ">=3.6" 190 | 191 | [package.extras] 192 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 193 | testing = ["docopt", "pytest (<6.0.0)"] 194 | 195 | [[package]] 196 | name = "pexpect" 197 | version = "4.8.0" 198 | description = "Pexpect allows easy control of interactive console applications." 199 | category = "dev" 200 | optional = false 201 | python-versions = "*" 202 | 203 | [package.dependencies] 204 | ptyprocess = ">=0.5" 205 | 206 | [[package]] 207 | name = "pickleshare" 208 | version = "0.7.5" 209 | description = "Tiny 'shelve'-like database with concurrency support" 210 | category = "dev" 211 | optional = false 212 | python-versions = "*" 213 | 214 | [[package]] 215 | name = "prometheus-client" 216 | version = "0.15.0" 217 | description = "Python client for the Prometheus monitoring system." 218 | category = "main" 219 | optional = false 220 | python-versions = ">=3.6" 221 | 222 | [package.extras] 223 | twisted = ["twisted"] 224 | 225 | [[package]] 226 | name = "prompt-toolkit" 227 | version = "3.0.36" 228 | description = "Library for building powerful interactive command lines in Python" 229 | category = "dev" 230 | optional = false 231 | python-versions = ">=3.6.2" 232 | 233 | [package.dependencies] 234 | wcwidth = "*" 235 | 236 | [[package]] 237 | name = "ptyprocess" 238 | version = "0.7.0" 239 | description = "Run a subprocess in a pseudo terminal" 240 | category = "dev" 241 | optional = false 242 | python-versions = "*" 243 | 244 | [[package]] 245 | name = "pure-eval" 246 | version = "0.2.2" 247 | description = "Safely evaluate AST nodes without side effects" 248 | category = "dev" 249 | optional = false 250 | python-versions = "*" 251 | 252 | [package.extras] 253 | tests = ["pytest"] 254 | 255 | [[package]] 256 | name = "pygments" 257 | version = "2.13.0" 258 | description = "Pygments is a syntax highlighting package written in Python." 259 | category = "main" 260 | optional = false 261 | python-versions = ">=3.6" 262 | 263 | [package.extras] 264 | plugins = ["importlib-metadata"] 265 | 266 | [[package]] 267 | name = "requests" 268 | version = "2.28.1" 269 | description = "Python HTTP for Humans." 270 | category = "main" 271 | optional = false 272 | python-versions = ">=3.7, <4" 273 | 274 | [package.dependencies] 275 | certifi = ">=2017.4.17" 276 | charset-normalizer = ">=2,<3" 277 | idna = ">=2.5,<4" 278 | urllib3 = ">=1.21.1,<1.27" 279 | 280 | [package.extras] 281 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 282 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 283 | 284 | [[package]] 285 | name = "rich" 286 | version = "12.6.0" 287 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 288 | category = "main" 289 | optional = false 290 | python-versions = ">=3.6.3,<4.0.0" 291 | 292 | [package.dependencies] 293 | commonmark = ">=0.9.0,<0.10.0" 294 | pygments = ">=2.6.0,<3.0.0" 295 | typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} 296 | 297 | [package.extras] 298 | jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] 299 | 300 | [[package]] 301 | name = "setuptools" 302 | version = "65.6.3" 303 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 304 | category = "dev" 305 | optional = false 306 | python-versions = ">=3.7" 307 | 308 | [package.extras] 309 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 310 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 311 | testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 312 | 313 | [[package]] 314 | name = "six" 315 | version = "1.16.0" 316 | description = "Python 2 and 3 compatibility utilities" 317 | category = "dev" 318 | optional = false 319 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 320 | 321 | [[package]] 322 | name = "stack-data" 323 | version = "0.6.2" 324 | description = "Extract data from python stack frames and tracebacks for informative displays" 325 | category = "dev" 326 | optional = false 327 | python-versions = "*" 328 | 329 | [package.dependencies] 330 | asttokens = ">=2.1.0" 331 | executing = ">=1.2.0" 332 | pure-eval = "*" 333 | 334 | [package.extras] 335 | tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] 336 | 337 | [[package]] 338 | name = "toml" 339 | version = "0.10.2" 340 | description = "Python Library for Tom's Obvious, Minimal Language" 341 | category = "dev" 342 | optional = false 343 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 344 | 345 | [[package]] 346 | name = "traitlets" 347 | version = "5.6.0" 348 | description = "Traitlets Python configuration system" 349 | category = "dev" 350 | optional = false 351 | python-versions = ">=3.7" 352 | 353 | [package.extras] 354 | docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] 355 | test = ["pre-commit", "pytest"] 356 | 357 | [[package]] 358 | name = "typing-extensions" 359 | version = "4.4.0" 360 | description = "Backported and Experimental Type Hints for Python 3.7+" 361 | category = "main" 362 | optional = false 363 | python-versions = ">=3.7" 364 | 365 | [[package]] 366 | name = "urllib3" 367 | version = "1.26.13" 368 | description = "HTTP library with thread-safe connection pooling, file post, and more." 369 | category = "main" 370 | optional = false 371 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 372 | 373 | [package.extras] 374 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 375 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 376 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 377 | 378 | [[package]] 379 | name = "wcwidth" 380 | version = "0.2.5" 381 | description = "Measures the displayed width of unicode strings in a terminal" 382 | category = "dev" 383 | optional = false 384 | python-versions = "*" 385 | 386 | [metadata] 387 | lock-version = "1.1" 388 | python-versions = "^3.8" 389 | content-hash = "6ee5cc7e55c2d818bf2919877e9cba6efabfa0983f99d7f4b9c6fb9b5e53edbf" 390 | 391 | [metadata.files] 392 | appnope = [ 393 | {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, 394 | {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, 395 | ] 396 | asttokens = [ 397 | {file = "asttokens-2.2.1-py2.py3-none-any.whl", hash = "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"}, 398 | {file = "asttokens-2.2.1.tar.gz", hash = "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3"}, 399 | ] 400 | backcall = [ 401 | {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, 402 | {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, 403 | ] 404 | certifi = [ 405 | {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, 406 | {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, 407 | ] 408 | charset-normalizer = [ 409 | {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, 410 | {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, 411 | ] 412 | click = [ 413 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 414 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 415 | ] 416 | colorama = [ 417 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 418 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 419 | ] 420 | commonmark = [ 421 | {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, 422 | {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, 423 | ] 424 | decorator = [ 425 | {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, 426 | {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, 427 | ] 428 | executing = [ 429 | {file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"}, 430 | {file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"}, 431 | ] 432 | idna = [ 433 | {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, 434 | {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, 435 | ] 436 | ipdb = [ 437 | {file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"}, 438 | ] 439 | ipython = [ 440 | {file = "ipython-8.7.0-py3-none-any.whl", hash = "sha256:352042ddcb019f7c04e48171b4dd78e4c4bb67bf97030d170e154aac42b656d9"}, 441 | {file = "ipython-8.7.0.tar.gz", hash = "sha256:882899fe78d5417a0aa07f995db298fa28b58faeba2112d2e3a4c95fe14bb738"}, 442 | ] 443 | jedi = [ 444 | {file = "jedi-0.18.2-py2.py3-none-any.whl", hash = "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e"}, 445 | {file = "jedi-0.18.2.tar.gz", hash = "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612"}, 446 | ] 447 | matplotlib-inline = [ 448 | {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, 449 | {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, 450 | ] 451 | parso = [ 452 | {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, 453 | {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, 454 | ] 455 | pexpect = [ 456 | {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, 457 | {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, 458 | ] 459 | pickleshare = [ 460 | {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, 461 | {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, 462 | ] 463 | prometheus-client = [ 464 | {file = "prometheus_client-0.15.0-py3-none-any.whl", hash = "sha256:db7c05cbd13a0f79975592d112320f2605a325969b270a94b71dcabc47b931d2"}, 465 | {file = "prometheus_client-0.15.0.tar.gz", hash = "sha256:be26aa452490cfcf6da953f9436e95a9f2b4d578ca80094b4458930e5f584ab1"}, 466 | ] 467 | prompt-toolkit = [ 468 | {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, 469 | {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, 470 | ] 471 | ptyprocess = [ 472 | {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, 473 | {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, 474 | ] 475 | pure-eval = [ 476 | {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, 477 | {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, 478 | ] 479 | pygments = [ 480 | {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, 481 | {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, 482 | ] 483 | requests = [ 484 | {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, 485 | {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, 486 | ] 487 | rich = [ 488 | {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, 489 | {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, 490 | ] 491 | setuptools = [ 492 | {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, 493 | {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, 494 | ] 495 | six = [ 496 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 497 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 498 | ] 499 | stack-data = [ 500 | {file = "stack_data-0.6.2-py3-none-any.whl", hash = "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8"}, 501 | {file = "stack_data-0.6.2.tar.gz", hash = "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815"}, 502 | ] 503 | toml = [ 504 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 505 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 506 | ] 507 | traitlets = [ 508 | {file = "traitlets-5.6.0-py3-none-any.whl", hash = "sha256:1410755385d778aed847d68deb99b3ba30fbbf489e17a1e8cbb753060d5cce73"}, 509 | {file = "traitlets-5.6.0.tar.gz", hash = "sha256:10b6ed1c9cedee83e795db70a8b9c2db157bb3778ec4587a349ecb7ef3b1033b"}, 510 | ] 511 | typing-extensions = [ 512 | {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, 513 | {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, 514 | ] 515 | urllib3 = [ 516 | {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, 517 | {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, 518 | ] 519 | wcwidth = [ 520 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 521 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 522 | ] 523 | --------------------------------------------------------------------------------